Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a host resource detector #5399

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ updates:
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /detectors/host
labels:
- dependencies
- go
- Skip Changelog
schedule:
interval: weekly
day: sunday
- package-ecosystem: gomod
directory: /exporters/autoexport
labels:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108)
- `NewSDK` in `go.opentelemetry.io/contrib/config` now returns a configured SDK with a valid `MeterProvider`. (#4804)
- Add the new `go.opentelemetry.io/contrib/detectors/host` package to provide a resource detector for physical hosts. (#5399)

### Fixed

Expand Down
35 changes: 35 additions & 0 deletions detectors/host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Host Resource detector

The host resource detector supports detecting host-specific attributes on physical hosts.

## Usage

```golang
// Instantiate a new host resource detector
hostResourceDetector := host.NewResourceDetector()
resource, err := hostResourceDetector.Detect(context.Background())
```

To populate optional attributes, the resource detector constructor accepts functional options `WithIPAddresses` to enable `host.ip`, and `WithMACAddresses` to enable `host.mac`.

```golang
// Instantiate a new host resource detector with all opt-in attributes
hostResourceDetector := host.NewResourceDetector(
WithIPAddresses(),
WithMACAddresses(),
)
resource, err := hostResourceDetector.Detect(context.Background())
```

## Supported attributes

According to [semantic conventions for host resources](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/host.md), each of the following attributes is added if it is available:
pyohannes marked this conversation as resolved.
Show resolved Hide resolved

* `host.arch`
* `host.id`
* `host.name`

The following attributes require an explicit opt-in during the initialization of the host resource detector:

* `host.ip`
* `host.mac`
20 changes: 20 additions & 0 deletions detectors/host/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module go.opentelemetry.io/contrib/detectors/host

go 1.21

require (
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.25.0
go.opentelemetry.io/otel/sdk v1.25.0
golang.org/x/sys v0.18.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
27 changes: 27 additions & 0 deletions detectors/host/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
137 changes: 137 additions & 0 deletions detectors/host/host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package host // import "go.opentelemetry.io/contrib/detectors/host"

import (
"context"
"net"
"os"
"runtime"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

type config struct {
optInIPAddresses bool
optInMACAddresses bool
}

func newConfig(options ...Option) *config {
c := &config{}
for _, option := range options {
option.apply(c)
}

return c
}

// Option applies an Azure VM detector configuration option.
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
type Option interface {
apply(*config)
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
}

type optionFunc func(*config)

func (fn optionFunc) apply(c *config) {
fn(c)
}

// WithIPAddresses adds the optional attribute `host.ip`.
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
func WithIPAddresses() Option {
return optionFunc(func(c *config) {
c.optInIPAddresses = true
})
}

// WithMACAddresses adds the optional attribute `host.ip`.
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
func WithMACAddresses() Option {
return optionFunc(func(c *config) {
c.optInMACAddresses = true
})
}

type resourceDetector struct {
config *config
}

// NewResourceDetector returns a resource detector that will detect host resources.
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
func NewResourceDetector(opts ...Option) resource.Detector {
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
c := newConfig(opts...)
return &resourceDetector{config: c}
}

// Detect detects associated resources when running on a physical host.
func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resource, error) {
attributes := []attribute.KeyValue{
semconv.HostArchKey.String(runtime.GOARCH),
}

hostName, err := os.Hostname()
if err == nil {
attributes = append(attributes, semconv.HostName(hostName))
}

machineId, err := getHostId()
if err == nil {
attributes = append(attributes, semconv.HostID(machineId))
}

if detector.config.optInIPAddresses {
ipAddresses := getIPAddresses()
if len(ipAddresses) > 0 {
attributes = append(attributes, semconv.HostIP(ipAddresses...))
}
}

if detector.config.optInMACAddresses {
macAddresses := getMACAddresses()
if len(macAddresses) > 0 {
attributes = append(attributes, semconv.HostMac(macAddresses...))
}
}

return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil
}

func getIPAddresses() []string {
var ipAddresses []string

ifaces, err := net.Interfaces()
if err == nil {
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
}

addrs, err := iface.Addrs()
if err != nil {
continue

Check warning on line 111 in detectors/host/host.go

View check run for this annotation

Codecov / codecov/patch

detectors/host/host.go#L111

Added line #L111 was not covered by tests
}
for _, addr := range addrs {
ipAddresses = append(ipAddresses, addr.String())
}
}
}

return ipAddresses
}

func getMACAddresses() []string {
var macAddresses []string

ifaces, err := net.Interfaces()
if err == nil {
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
}

macAddresses = append(macAddresses, iface.HardwareAddr.String())
}
}

return macAddresses
}
21 changes: 21 additions & 0 deletions detectors/host/host_id_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build darwin
// +build darwin

package host // import "go.opentelemetry.io/contrib/detectors/host"

import (
"os/exec"
"strings"
)

func getHostId() (string, error) {
machineId, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
if err != nil {
return "", err
}

return strings.Trim(string(machineId), "\n"), nil
}
21 changes: 21 additions & 0 deletions detectors/host/host_id_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build linux
// +build linux

package host // import "go.opentelemetry.io/contrib/detectors/host"

import (
"os"
"strings"
)

func getHostId() (string, error) {
machineId, err := os.ReadFile("/etc/machine-id")
if err != nil {
return "", err

Check warning on line 17 in detectors/host/host_id_linux.go

View check run for this annotation

Codecov / codecov/patch

detectors/host/host_id_linux.go#L17

Added line #L17 was not covered by tests
}

return strings.Trim(string(machineId), "\n"), nil
}
24 changes: 24 additions & 0 deletions detectors/host/host_id_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build windows
// +build windows

package host // import "go.opentelemetry.io/contrib/detectors/host"

import (
"golang.org/x/sys/windows/registry"
)

func getHostId() (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, registry.QUERY_VALUE)
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}

defer key.Close()

machineId, _, err := key.GetStringValue("MachineGuid")

return machineId, err
}
73 changes: 73 additions & 0 deletions detectors/host/host_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package host

import (
"context"
"os"
"runtime"
"testing"

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

func Test_Detect(t *testing.T) {
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
detector := NewResourceDetector()

hostResource, err := detector.Detect(context.Background())

assert.True(t, err == nil)
pyohannes marked this conversation as resolved.
Show resolved Hide resolved

hostName, _ := os.Hostname()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should not be ignored. The test should be failed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adapted the tests so that the host.name is only added to the list of expected attributes when os.Hostname() doesn't return an error.

This covers cases where os.Hostname() returns an error, but a host.id might still be obtained.

I'm happy to both make the tests and detector logic fail when os.Hostname() fails, if folks think that's better.


attributes := []attribute.KeyValue{
semconv.HostArchKey.String(runtime.GOARCH),
semconv.HostName(hostName),
}

// The host id is added conditionally, as it might not be available under all circumstances (for example in Windows containers)
machineId, err := getHostId()
if err == nil {
attributes = append(attributes, semconv.HostID(machineId))
}

expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)

assert.Equal(t, expectedResource, hostResource)
}

func Test_Detect_WithOptIns(t *testing.T) {
pyohannes marked this conversation as resolved.
Show resolved Hide resolved
detector := NewResourceDetector(
WithIPAddresses(),
WithMACAddresses(),
)

hostResource, err := detector.Detect(context.Background())

assert.True(t, err == nil)

hostName, _ := os.Hostname()

attributes := []attribute.KeyValue{
semconv.HostArchKey.String(runtime.GOARCH),
semconv.HostName(hostName),
}

// The host id is added conditionally, as it might not be available under all circumstances (for example in Windows containers)
machineId, err := getHostId()
if err == nil {
attributes = append(attributes, semconv.HostID(machineId))
}

attributes = append(attributes, semconv.HostIP(getIPAddresses()...))
attributes = append(attributes, semconv.HostMac(getMACAddresses()...))

expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)

assert.Equal(t, expectedResource, hostResource)
}