1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-10-31 00:07:40 +02:00

add host.id to resource auto-detection (#3812)

* add platform specific hostIDReaders

* add WithHostID option to Resource

* add changelog entry

* Apply suggestions from code review

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* linting

* combine platform specific readers and tests

This allows us to run tests for the BSD, Darwin, and Linux readers
on all platforms.

* add todo to use assert.AnError after resource.Detect error handling is updated

* move HostID test utilities to host_id_test

* return assert.AnError from mockHostIDProviderWithError

* use assert.ErrorIs

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com>
This commit is contained in:
Matthew Wear
2023-03-21 12:45:30 -07:00
committed by GitHub
parent 1eab60f714
commit 282a47e3d3
12 changed files with 622 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added
- The `WithHostID` option to `go.opentelemetry.io/otel/sdk/resource`. (#3812)
- The `WithoutTimestamps` option to `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric` to sets all timestamps to zero. (#3828)
- The new `Exemplar` type is added to `go.opentelemetry.io/otel/sdk/metric/metricdata`.
Both the `DataPoint` and `HistogramDataPoint` types from that package have a new field of `Exemplars` containing the sampled exemplars for their timeseries. (#3849)

View File

@@ -71,6 +71,11 @@ func WithHost() Option {
return WithDetectors(host{})
}
// WithHostID adds host ID information to the configured resource.
func WithHostID() Option {
return WithDetectors(hostIDDetector{})
}
// WithTelemetrySDK adds TelemetrySDK version info to the configured resource.
func WithTelemetrySDK() Option {
return WithDetectors(telemetrySDK{})

142
sdk/resource/host_id.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource // import "go.opentelemetry.io/otel/sdk/resource"
import (
"context"
"errors"
"os"
"os/exec"
"strings"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
type hostIDProvider func() (string, error)
var defaultHostIDProvider hostIDProvider = platformHostIDReader.read
var hostID = defaultHostIDProvider
type hostIDReader interface {
read() (string, error)
}
type fileReader func(string) (string, error)
type commandExecutor func(string, ...string) (string, error)
func readFile(filename string) (string, error) {
b, err := os.ReadFile(filename)
if err != nil {
return "", nil
}
return string(b), nil
}
// nolint: unused // This is used by the hostReaderBSD, gated by build tags.
func execCommand(name string, arg ...string) (string, error) {
cmd := exec.Command(name, arg...)
b, err := cmd.Output()
if err != nil {
return "", err
}
return string(b), nil
}
// hostIDReaderBSD implements hostIDReader.
type hostIDReaderBSD struct {
execCommand commandExecutor
readFile fileReader
}
// read attempts to read the machine-id from /etc/hostid. If not found it will
// execute `kenv -q smbios.system.uuid`. If neither location yields an id an
// error will be returned.
func (r *hostIDReaderBSD) read() (string, error) {
if result, err := r.readFile("/etc/hostid"); err == nil {
return strings.TrimSpace(result), nil
}
if result, err := r.execCommand("kenv", "-q", "smbios.system.uuid"); err == nil {
return strings.TrimSpace(result), nil
}
return "", errors.New("host id not found in: /etc/hostid or kenv")
}
// hostIDReaderDarwin implements hostIDReader.
type hostIDReaderDarwin struct {
execCommand commandExecutor
}
// read executes `ioreg -rd1 -c "IOPlatformExpertDevice"` and parses host id
// from the IOPlatformUUID line. If the command fails or the uuid cannot be
// parsed an error will be returned.
func (r *hostIDReaderDarwin) read() (string, error) {
result, err := r.execCommand("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
if err != nil {
return "", err
}
lines := strings.Split(result, "\n")
for _, line := range lines {
if strings.Contains(line, "IOPlatformUUID") {
parts := strings.Split(line, " = ")
if len(parts) == 2 {
return strings.Trim(parts[1], "\""), nil
}
break
}
}
return "", errors.New("could not parse IOPlatformUUID")
}
type hostIDReaderLinux struct {
readFile fileReader
}
// read attempts to read the machine-id from /etc/machine-id followed by
// /var/lib/dbus/machine-id. If neither location yields an ID an error will
// be returned.
func (r *hostIDReaderLinux) read() (string, error) {
if result, err := r.readFile("/etc/machine-id"); err == nil {
return strings.TrimSpace(result), nil
}
if result, err := r.readFile("/var/lib/dbus/machine-id"); err == nil {
return strings.TrimSpace(result), nil
}
return "", errors.New("host id not found in: /etc/machine-id or /var/lib/dbus/machine-id")
}
type hostIDDetector struct{}
// Detect returns a *Resource containing the platform specific host id.
func (hostIDDetector) Detect(ctx context.Context) (*Resource, error) {
hostID, err := hostID()
if err != nil {
return nil, err
}
return NewWithAttributes(
semconv.SchemaURL,
semconv.HostID(hostID),
), nil
}

View File

@@ -0,0 +1,28 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build dragonfly || freebsd || netbsd || openbsd || solaris
// +build dragonfly freebsd netbsd openbsd solaris
package resource // import "go.opentelemetry.io/otel/sdk/resource"
import (
"errors"
"strings"
)
var platformHostIDReader hostIDReader = &hostIDReaderBSD{
execCommand: execCommand,
readFile: readFile,
}

View File

@@ -0,0 +1,19 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource // import "go.opentelemetry.io/otel/sdk/resource"
var platformHostIDReader hostIDReader = &hostIDReaderDarwin{
execCommand: execCommand,
}

View File

@@ -0,0 +1,37 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource_test
import (
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/sdk/resource"
)
func mockHostIDProvider() {
resource.SetHostIDProvider(
func() (string, error) { return "f2c668b579780554f70f72a063dc0864", nil },
)
}
func mockHostIDProviderWithError() {
resource.SetHostIDProvider(
func() (string, error) { return "", assert.AnError },
)
}
func restoreHostIDProvider() {
resource.SetDefaultHostIDProvider()
}

View File

@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package resource // import "go.opentelemetry.io/otel/sdk/resource"
var platformHostIDReader hostIDReader = &hostIDReaderLinux{
readFile: readFile,
}

View File

@@ -0,0 +1,222 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
var (
expectedHostID = "f2c668b579780554f70f72a063dc0864"
readFileNoError = func(filename string) (string, error) {
return expectedHostID + "\n", nil
}
readFileError = func(filename string) (string, error) {
return "", errors.New("not found")
}
execCommandNoError = func(string, ...string) (string, error) {
return expectedHostID + "\n", nil
}
execCommandError = func(string, ...string) (string, error) {
return "", errors.New("not found")
}
)
func SetDefaultHostIDProvider() {
SetHostIDProvider(defaultHostIDProvider)
}
func SetHostIDProvider(hostIDProvider hostIDProvider) {
hostID = hostIDProvider
}
func TestHostIDReaderBSD(t *testing.T) {
tt := []struct {
name string
fileReader fileReader
commandExecutor commandExecutor
expectedHostID string
expectError bool
}{
{
name: "hostIDReaderBSD valid primary",
fileReader: readFileNoError,
commandExecutor: execCommandError,
expectedHostID: expectedHostID,
expectError: false,
},
{
name: "hostIDReaderBSD invalid primary",
fileReader: readFileError,
commandExecutor: execCommandNoError,
expectedHostID: expectedHostID,
expectError: false,
},
{
name: "hostIDReaderBSD invalid primary and secondary",
fileReader: readFileError,
commandExecutor: execCommandError,
expectedHostID: "",
expectError: true,
},
}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
reader := hostIDReaderBSD{
readFile: tc.fileReader,
execCommand: tc.commandExecutor,
}
hostID, err := reader.read()
require.Equal(t, tc.expectError, err != nil)
require.Equal(t, tc.expectedHostID, hostID)
})
}
}
func TestHostIDReaderLinux(t *testing.T) {
readFilePrimaryError := func(filename string) (string, error) {
if filename == "/var/lib/dbus/machine-id" {
return readFileNoError(filename)
}
return readFileError(filename)
}
tt := []struct {
name string
fileReader fileReader
expectedHostID string
expectError bool
}{
{
name: "hostIDReaderLinux valid primary",
fileReader: readFileNoError,
expectedHostID: expectedHostID,
expectError: false,
},
{
name: "hostIDReaderLinux invalid primary",
fileReader: readFilePrimaryError,
expectedHostID: expectedHostID,
expectError: false,
},
{
name: "hostIDReaderLinux invalid primary and secondary",
fileReader: readFileError,
expectedHostID: "",
expectError: true,
},
}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
reader := hostIDReaderLinux{
readFile: tc.fileReader,
}
hostID, err := reader.read()
require.Equal(t, tc.expectError, err != nil)
require.Equal(t, tc.expectedHostID, hostID)
})
}
}
func TestHostIDReaderDarwin(t *testing.T) {
validOutput := `+-o J316sAP <class IOPlatformExpertDevice, id 0x10000024d, registered, matched, active, busy 0 (132196 ms), retain 37>
{
"IOPolledInterface" = "AppleARMWatchdogTimerHibernateHandler is not serializable"
"#address-cells" = <02000000>
"AAPL,phandle" = <01000000>
"serial-number" = <94e1c79ec04cd3f153f600000000000000000000000000000000000000000000>
"IOBusyInterest" = "IOCommand is not serializable"
"target-type" = <"J316s">
"platform-name" = <7436303030000000000000000000000000000000000000000000000000000000>
"secure-root-prefix" = <"md">
"name" = <"device-tree">
"region-info" = <4c4c2f4100000000000000000000000000000000000000000000000000000000>
"manufacturer" = <"Apple Inc.">
"compatible" = <"J316sAP","MacBookPro18,1","AppleARM">
"config-number" = <00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>
"IOPlatformSerialNumber" = "HDWLIF2LM7"
"regulatory-model-number" = <4132343835000000000000000000000000000000000000000000000000000000>
"time-stamp" = <"Fri Aug 5 20:25:38 PDT 2022">
"clock-frequency" = <00366e01>
"model" = <"MacBookPro18,1">
"mlb-serial-number" = <5c92d268d6cd789e475ffafc0d363fc950000000000000000000000000000000>
"model-number" = <5a31345930303136430000000000000000000000000000000000000000000000>
"IONWInterrupts" = "IONWInterrupts"
"model-config" = <"ICT;MoPED=0x03D053A605C84ED11C455A18D6C643140B41A239">
"device_type" = <"bootrom">
"#size-cells" = <02000000>
"IOPlatformUUID" = "81895B8D-9EF9-4EBB-B5DE-B00069CF53F0"
}
`
execCommandValid := func(string, ...string) (string, error) {
return validOutput, nil
}
execCommandInvalid := func(string, ...string) (string, error) {
return "wasn't expecting this", nil
}
tt := []struct {
name string
fileReader fileReader
commandExecutor commandExecutor
expectedHostID string
expectError bool
}{
{
name: "hostIDReaderDarwin valid output",
commandExecutor: execCommandValid,
expectedHostID: "81895B8D-9EF9-4EBB-B5DE-B00069CF53F0",
expectError: false,
},
{
name: "hostIDReaderDarwin invalid output",
commandExecutor: execCommandInvalid,
expectedHostID: "",
expectError: true,
},
{
name: "hostIDReaderDarwin error",
commandExecutor: execCommandError,
expectedHostID: "",
expectError: true,
},
}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
reader := hostIDReaderDarwin{
execCommand: tc.commandExecutor,
}
hostID, err := reader.read()
require.Equal(t, tc.expectError, err != nil)
require.Equal(t, tc.expectedHostID, hostID)
})
}
}

View File

@@ -0,0 +1,36 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !darwin
// +build !dragonfly
// +build !freebsd
// +build !linux
// +build !netbsd
// +build !openbsd
// +build !solaris
// +build !windows
package resource // import "go.opentelemetry.io/otel/sdk/resource"
// hostIDReaderUnsupported is a placeholder implementation for operating systems
// for which this project currently doesn't support host.id
// attribute detection. See build tags declaration early on this file
// for a list of unsupported OSes.
type hostIDReaderUnsupported struct{}
func (*hostIDReaderUnsupported) read() (string, error) {
return "<unknown>", nil
}
var platformHostIDReader hostIDReader = &hostIDReaderUnsupported{}

View File

@@ -0,0 +1,48 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
// +build windows
package resource // import "go.opentelemetry.io/otel/sdk/resource"
import (
"golang.org/x/sys/windows/registry"
)
// implements hostIDReader
type hostIDReaderWindows struct{}
// read reads MachineGuid from the windows registry key:
// SOFTWARE\Microsoft\Cryptography
func (*hostIDReaderWindows) read() (string, error) {
k, err := registry.OpenKey(
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`,
registry.QUERY_VALUE|registry.WOW64_64KEY,
)
if err != nil {
return "", err
}
defer k.Close()
guid, _, err := k.GetStringValue("MachineGuid")
if err != nil {
return "", err
}
return guid, nil
}
var platformHostIDReader hostIDReader = &hostIDReaderWindows{}

View File

@@ -0,0 +1,32 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
// +build windows
package resource // import "go.opentelemetry.io/otel/sdk/resource"
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestReader(t *testing.T) {
reader := &hostIDReaderWindows{}
result, err := reader.read()
require.NoError(t, err)
require.NotEmpty(t, result)
}

View File

@@ -471,6 +471,36 @@ func TestNewWrapedError(t *testing.T) {
assert.NotErrorIs(t, err, errors.New("false positive error"))
}
func TestWithHostID(t *testing.T) {
mockHostIDProvider()
t.Cleanup(restoreHostIDProvider)
ctx := context.Background()
res, err := resource.New(ctx,
resource.WithHostID(),
)
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"host.id": "f2c668b579780554f70f72a063dc0864",
}, toMap(res))
}
func TestWithHostIDError(t *testing.T) {
mockHostIDProviderWithError()
t.Cleanup(restoreHostIDProvider)
ctx := context.Background()
res, err := resource.New(ctx,
resource.WithHostID(),
)
assert.ErrorIs(t, err, assert.AnError)
require.EqualValues(t, map[string]string{}, toMap(res))
}
func TestWithOSType(t *testing.T) {
mockRuntimeProviders()
t.Cleanup(restoreAttributesProviders)