From 4a89d29a58cbb3b02bad2cf9fc021c3239e72e0c Mon Sep 17 00:00:00 2001 From: Pablo Baeyens Date: Thu, 7 Dec 2023 11:03:32 +0100 Subject: [PATCH] [processor/resourcedetection] Add host.mac to system detector (#29588) **Description:** Adds support for `host.mac` detection to the `system` detector on the resource detection processor. This convention is defined in the specification [on the host document](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/resource/host.md). **Link to tracking Issue:** Fixes #29587 and therefore fixes #22045 **Testing:** Unit tests; manually Tested on my laptop with the following configuration: ``` receivers: hostmetrics: collection_interval: 10s scrapers: load: processors: resourcedetection: detectors: ["system"] system: resource_attributes: host.mac: enabled: true exporters: debug: verbosity: detailed service: pipelines: metrics: receivers: [hostmetrics] processors: [resourcedetection] exporters: [debug] ``` --------- Co-authored-by: Curtis Robert --- .chloggen/mx-psi_host.mac.yaml | 27 ++++++++++++ internal/metadataproviders/system/metadata.go | 20 +++++++++ .../resourcedetectionprocessor/README.md | 1 + .../internal/metadata/generated_config.go | 4 ++ .../metadata/generated_config_test.go | 2 + .../internal/metadata/generated_resource.go | 7 ++++ .../metadata/generated_resource_test.go | 8 +++- .../internal/metadata/testdata/config.yaml | 4 ++ .../internal/system/metadata.yaml | 4 ++ .../internal/system/system.go | 25 +++++++++++ .../internal/system/system_test.go | 41 +++++++++++++++++++ 11 files changed, 142 insertions(+), 1 deletion(-) create mode 100755 .chloggen/mx-psi_host.mac.yaml diff --git a/.chloggen/mx-psi_host.mac.yaml b/.chloggen/mx-psi_host.mac.yaml new file mode 100755 index 000000000000..23b74d8d396c --- /dev/null +++ b/.chloggen/mx-psi_host.mac.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: resourcedetectionprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add detection of host.mac to system detector. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [29587] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/internal/metadataproviders/system/metadata.go b/internal/metadataproviders/system/metadata.go index e97b79b2092a..116ba76d9bf6 100644 --- a/internal/metadataproviders/system/metadata.go +++ b/internal/metadataproviders/system/metadata.go @@ -66,6 +66,9 @@ type Provider interface { // HostIPs returns the host's IP interfaces HostIPs() ([]net.IP, error) + + // HostMACs returns the host's MAC addresses + HostMACs() ([]net.HardwareAddr, error) } type systemMetadataProvider struct { @@ -196,3 +199,20 @@ func (p systemMetadataProvider) HostIPs() (ips []net.IP, err error) { } return ips, err } + +func (p systemMetadataProvider) HostMACs() (macs []net.HardwareAddr, err error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + for _, iface := range ifaces { + // skip if the interface is down or is a loopback interface + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + macs = append(macs, iface.HardwareAddr) + } + return macs, err +} diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index 1e2deb0057ad..8937c1c7a740 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -49,6 +49,7 @@ Queries the host machine to retrieve the following resource attributes: * host.name * host.id * host.ip + * host.mac * host.cpu.vendor.id * host.cpu.family * host.cpu.model.id diff --git a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config.go b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config.go index 121383329fc0..e21708c5c6e3 100644 --- a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config.go +++ b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config.go @@ -34,6 +34,7 @@ type ResourceAttributesConfig struct { HostCPUVendorID ResourceAttributeConfig `mapstructure:"host.cpu.vendor.id"` HostID ResourceAttributeConfig `mapstructure:"host.id"` HostIP ResourceAttributeConfig `mapstructure:"host.ip"` + HostMac ResourceAttributeConfig `mapstructure:"host.mac"` HostName ResourceAttributeConfig `mapstructure:"host.name"` OsDescription ResourceAttributeConfig `mapstructure:"os.description"` OsType ResourceAttributeConfig `mapstructure:"os.type"` @@ -68,6 +69,9 @@ func DefaultResourceAttributesConfig() ResourceAttributesConfig { HostIP: ResourceAttributeConfig{ Enabled: false, }, + HostMac: ResourceAttributeConfig{ + Enabled: false, + }, HostName: ResourceAttributeConfig{ Enabled: true, }, diff --git a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config_test.go b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config_test.go index 22c74d9ed2f0..b3892d04ee88 100644 --- a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config_test.go +++ b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config_test.go @@ -34,6 +34,7 @@ func TestResourceAttributesConfig(t *testing.T) { HostCPUVendorID: ResourceAttributeConfig{Enabled: true}, HostID: ResourceAttributeConfig{Enabled: true}, HostIP: ResourceAttributeConfig{Enabled: true}, + HostMac: ResourceAttributeConfig{Enabled: true}, HostName: ResourceAttributeConfig{Enabled: true}, OsDescription: ResourceAttributeConfig{Enabled: true}, OsType: ResourceAttributeConfig{Enabled: true}, @@ -51,6 +52,7 @@ func TestResourceAttributesConfig(t *testing.T) { HostCPUVendorID: ResourceAttributeConfig{Enabled: false}, HostID: ResourceAttributeConfig{Enabled: false}, HostIP: ResourceAttributeConfig{Enabled: false}, + HostMac: ResourceAttributeConfig{Enabled: false}, HostName: ResourceAttributeConfig{Enabled: false}, OsDescription: ResourceAttributeConfig{Enabled: false}, OsType: ResourceAttributeConfig{Enabled: false}, diff --git a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource.go b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource.go index 0391f5824501..b4470e936493 100644 --- a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource.go +++ b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource.go @@ -84,6 +84,13 @@ func (rb *ResourceBuilder) SetHostIP(val []any) { } } +// SetHostMac sets provided value as "host.mac" attribute. +func (rb *ResourceBuilder) SetHostMac(val []any) { + if rb.config.HostMac.Enabled { + rb.res.Attributes().PutEmptySlice("host.mac").FromRaw(val) + } +} + // SetHostName sets provided value as "host.name" attribute. func (rb *ResourceBuilder) SetHostName(val string) { if rb.config.HostName.Enabled { diff --git a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource_test.go b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource_test.go index 38e5c08065bb..2d9e2811c358 100644 --- a/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource_test.go +++ b/processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource_test.go @@ -22,6 +22,7 @@ func TestResourceBuilder(t *testing.T) { rb.SetHostCPUVendorID("host.cpu.vendor.id-val") rb.SetHostID("host.id-val") rb.SetHostIP([]any{"host.ip-item1", "host.ip-item2"}) + rb.SetHostMac([]any{"host.mac-item1", "host.mac-item2"}) rb.SetHostName("host.name-val") rb.SetOsDescription("os.description-val") rb.SetOsType("os.type-val") @@ -33,7 +34,7 @@ func TestResourceBuilder(t *testing.T) { case "default": assert.Equal(t, 2, res.Attributes().Len()) case "all_set": - assert.Equal(t, 12, res.Attributes().Len()) + assert.Equal(t, 13, res.Attributes().Len()) case "none_set": assert.Equal(t, 0, res.Attributes().Len()) return @@ -86,6 +87,11 @@ func TestResourceBuilder(t *testing.T) { if ok { assert.EqualValues(t, []any{"host.ip-item1", "host.ip-item2"}, val.Slice().AsRaw()) } + val, ok = res.Attributes().Get("host.mac") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, []any{"host.mac-item1", "host.mac-item2"}, val.Slice().AsRaw()) + } val, ok = res.Attributes().Get("host.name") assert.True(t, ok) if ok { diff --git a/processor/resourcedetectionprocessor/internal/system/internal/metadata/testdata/config.yaml b/processor/resourcedetectionprocessor/internal/system/internal/metadata/testdata/config.yaml index d258480c7b00..8bd597a02e25 100644 --- a/processor/resourcedetectionprocessor/internal/system/internal/metadata/testdata/config.yaml +++ b/processor/resourcedetectionprocessor/internal/system/internal/metadata/testdata/config.yaml @@ -19,6 +19,8 @@ all_set: enabled: true host.ip: enabled: true + host.mac: + enabled: true host.name: enabled: true os.description: @@ -45,6 +47,8 @@ none_set: enabled: false host.ip: enabled: false + host.mac: + enabled: false host.name: enabled: false os.description: diff --git a/processor/resourcedetectionprocessor/internal/system/metadata.yaml b/processor/resourcedetectionprocessor/internal/system/metadata.yaml index 3b5acbcbef55..93de1f2f9d3c 100644 --- a/processor/resourcedetectionprocessor/internal/system/metadata.yaml +++ b/processor/resourcedetectionprocessor/internal/system/metadata.yaml @@ -27,6 +27,10 @@ resource_attributes: description: IP addresses for the host type: slice enabled: false + host.mac: + description: MAC addresses for the host + type: slice + enabled: false host.cpu.vendor.id: description: The host.cpu.vendor.id type: string diff --git a/processor/resourcedetectionprocessor/internal/system/system.go b/processor/resourcedetectionprocessor/internal/system/system.go index a62eab4287e7..7d46a902fdd3 100644 --- a/processor/resourcedetectionprocessor/internal/system/system.go +++ b/processor/resourcedetectionprocessor/internal/system/system.go @@ -7,7 +7,9 @@ import ( "context" "errors" "fmt" + "net" "strconv" + "strings" "github.com/shirou/gopsutil/v3/cpu" "go.opentelemetry.io/collector/featuregate" @@ -69,6 +71,17 @@ func NewDetector(p processor.CreateSettings, dcfg internal.DetectorConfig) (inte }, nil } +// toIEEERA converts a MAC address to IEEE RA format. +// Per the spec: "MAC Addresses MUST be represented in IEEE RA hexadecimal form: as hyphen-separated +// octets in uppercase hexadecimal form from most to least significant." +// Golang returns MAC addresses as colon-separated octets in lowercase hexadecimal form from most +// to least significant, so we need to: +// - Replace colons with hyphens +// - Convert to uppercase +func toIEEERA(mac net.HardwareAddr) string { + return strings.ToUpper(strings.ReplaceAll(mac.String(), ":", "-")) +} + // Detect detects system metadata and returns a resource with the available ones func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schemaURL string, err error) { var hostname string @@ -94,6 +107,17 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem } } + var hostMACAttribute []any + if d.cfg.ResourceAttributes.HostMac.Enabled { + hostMACs, errMACs := d.provider.HostMACs() + if errMACs != nil { + return pcommon.NewResource(), "", fmt.Errorf("failed to get host MAC addresses: %w", errMACs) + } + for _, mac := range hostMACs { + hostMACAttribute = append(hostMACAttribute, toIEEERA(mac)) + } + } + osDescription, err := d.provider.OSDescription(ctx) if err != nil { return pcommon.NewResource(), "", fmt.Errorf("failed getting OS description: %w", err) @@ -119,6 +143,7 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem } d.rb.SetHostArch(hostArch) d.rb.SetHostIP(hostIPAttribute) + d.rb.SetHostMac(hostMACAttribute) d.rb.SetOsDescription(osDescription) if len(cpuInfo) > 0 { err = setHostCPUInfo(d, cpuInfo[0]) diff --git a/processor/resourcedetectionprocessor/internal/system/system_test.go b/processor/resourcedetectionprocessor/internal/system/system_test.go index 497f134ddf8f..13c53071dde1 100644 --- a/processor/resourcedetectionprocessor/internal/system/system_test.go +++ b/processor/resourcedetectionprocessor/internal/system/system_test.go @@ -72,9 +72,17 @@ func (m *mockMetadata) HostIPs() ([]net.IP, error) { return args.Get(0).([]net.IP), args.Error(1) } +func (m *mockMetadata) HostMACs() ([]net.HardwareAddr, error) { + args := m.MethodCalled("HostMACs") + return args.Get(0).([]net.HardwareAddr), args.Error(1) +} + var ( testIPsAttribute = []any{"192.168.1.140", "fe80::abc2:4a28:737a:609e"} testIPsAddresses = []net.IP{net.ParseIP(testIPsAttribute[0].(string)), net.ParseIP(testIPsAttribute[1].(string))} + + testMACsAttribute = []any{"00-00-00-00-00-01", "DE-AD-BE-EF-00-00"} + testMACsAddresses = []net.HardwareAddr{{0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00}} ) func TestNewDetector(t *testing.T) { @@ -104,11 +112,34 @@ func TestNewDetector(t *testing.T) { } } +func TestToIEEERA(t *testing.T) { + tests := []struct { + addr net.HardwareAddr + expected string + }{ + { + addr: testMACsAddresses[0], + expected: testMACsAttribute[0].(string), + }, + { + addr: testMACsAddresses[1], + expected: testMACsAttribute[1].(string), + }, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + assert.Equal(t, tt.expected, toIEEERA(tt.addr)) + }) + } +} + func allEnabledConfig() metadata.ResourceAttributesConfig { cfg := metadata.DefaultResourceAttributesConfig() cfg.HostArch.Enabled = true cfg.HostID.Enabled = true cfg.HostIP.Enabled = true + cfg.HostMac.Enabled = true cfg.OsDescription.Enabled = true return cfg } @@ -121,6 +152,7 @@ func TestDetectFQDNAvailable(t *testing.T) { md.On("HostID").Return("2", nil) md.On("HostArch").Return("amd64", nil) md.On("HostIPs").Return(testIPsAddresses, nil) + md.On("HostMACs").Return(testMACsAddresses, nil) detector := newTestDetector(md, []string{"dns"}, allEnabledConfig()) res, schemaURL, err := detector.Detect(context.Background()) @@ -135,6 +167,7 @@ func TestDetectFQDNAvailable(t *testing.T) { conventions.AttributeHostID: "2", conventions.AttributeHostArch: conventions.AttributeHostArchAMD64, "host.ip": testIPsAttribute, + "host.mac": testMACsAttribute, } assert.Equal(t, expected, res.Attributes().AsRaw()) @@ -174,6 +207,7 @@ func TestEnableHostID(t *testing.T) { mdHostname.On("HostID").Return("3", nil) mdHostname.On("HostArch").Return("amd64", nil) mdHostname.On("HostIPs").Return(testIPsAddresses, nil) + mdHostname.On("HostMACs").Return(testMACsAddresses, nil) detector := newTestDetector(mdHostname, []string{"dns", "os"}, allEnabledConfig()) res, schemaURL, err := detector.Detect(context.Background()) @@ -188,6 +222,7 @@ func TestEnableHostID(t *testing.T) { conventions.AttributeHostID: "3", conventions.AttributeHostArch: conventions.AttributeHostArchAMD64, "host.ip": testIPsAttribute, + "host.mac": testMACsAttribute, } assert.Equal(t, expected, res.Attributes().AsRaw()) @@ -201,6 +236,7 @@ func TestUseHostname(t *testing.T) { mdHostname.On("HostID").Return("1", nil) mdHostname.On("HostArch").Return("amd64", nil) mdHostname.On("HostIPs").Return(testIPsAddresses, nil) + mdHostname.On("HostMACs").Return(testMACsAddresses, nil) detector := newTestDetector(mdHostname, []string{"os"}, allEnabledConfig()) res, schemaURL, err := detector.Detect(context.Background()) @@ -215,6 +251,7 @@ func TestUseHostname(t *testing.T) { conventions.AttributeHostID: "1", conventions.AttributeHostArch: conventions.AttributeHostArchAMD64, "host.ip": testIPsAttribute, + "host.mac": testMACsAttribute, } assert.Equal(t, expected, res.Attributes().AsRaw()) @@ -230,6 +267,7 @@ func TestDetectError(t *testing.T) { mdFQDN.On("HostID").Return("", errors.New("err")) mdFQDN.On("HostArch").Return("amd64", nil) mdFQDN.On("HostIPs").Return(testIPsAddresses, nil) + mdFQDN.On("HostMACs").Return(testMACsAddresses, nil) detector := newTestDetector(mdFQDN, []string{"dns"}, allEnabledConfig()) res, schemaURL, err := detector.Detect(context.Background()) @@ -245,6 +283,7 @@ func TestDetectError(t *testing.T) { mdHostname.On("HostID").Return("", errors.New("err")) mdHostname.On("HostArch").Return("amd64", nil) mdHostname.On("HostIPs").Return(testIPsAddresses, nil) + mdHostname.On("HostMACs").Return(testMACsAddresses, nil) detector = newTestDetector(mdHostname, []string{"os"}, allEnabledConfig()) res, schemaURL, err = detector.Detect(context.Background()) @@ -275,6 +314,7 @@ func TestDetectError(t *testing.T) { mdHostID.On("HostID").Return("", errors.New("err")) mdHostID.On("HostArch").Return("arm64", nil) mdHostID.On("HostIPs").Return(testIPsAddresses, nil) + mdHostID.On("HostMACs").Return(testMACsAddresses, nil) detector = newTestDetector(mdHostID, []string{"os"}, allEnabledConfig()) res, schemaURL, err = detector.Detect(context.Background()) @@ -286,6 +326,7 @@ func TestDetectError(t *testing.T) { conventions.AttributeOSType: "linux", conventions.AttributeHostArch: conventions.AttributeHostArchARM64, "host.ip": testIPsAttribute, + "host.mac": testMACsAttribute, }, res.Attributes().AsRaw()) }