Skip to content

Commit 7621965

Browse files
[processor/resourcedetection]: add additional os properties (#40257)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Add properties which are missing in https://github.com/open-telemetry/semantic-conventions/blob/45e91aedff7d347955ba09269b871f8fd9049ce3/docs/resource/os.md, and the logic for getting `os.name` and `os.build.id` is also comes from here. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Implementation #39941 <!--Describe what testing was performed and which tests were added.--> #### Testing `TestDetectOSNameAndBuildID` in `processor/resourcedetectionprocessor/internal/system/system_test.go` <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.-->
1 parent 2be002e commit 7621965

File tree

11 files changed

+219
-1
lines changed

11 files changed

+219
-1
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: resourcedetectionprocessor
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Add additional OS properties to resource detection: `os.build.id` and `os.name`"
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [39941]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

internal/metadataproviders/system/metadata.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package system // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/system"
55

66
import (
7+
"bytes"
78
"context"
9+
"encoding/xml"
810
"fmt"
911
"net"
1012
"os"
@@ -77,6 +79,12 @@ type Provider interface {
7779

7880
// CPUInfo returns the host's CPU info
7981
CPUInfo(ctx context.Context) ([]cpu.InfoStat, error)
82+
83+
// OSName returns the OS name according to semantic conventions.
84+
OSName(ctx context.Context) (string, error)
85+
86+
// OSBuildID returns the OS build ID according to semantic conventions.
87+
OSBuildID(ctx context.Context) (string, error)
8088
}
8189

8290
type systemMetadataProvider struct {
@@ -178,6 +186,85 @@ func (p systemMetadataProvider) OSDescription(ctx context.Context) (string, erro
178186
return p.fromOption(ctx, resource.WithOSDescription(), string(conventions.OSDescriptionKey))
179187
}
180188

189+
// OSName returns the OS name from host metadata.
190+
func (p systemMetadataProvider) OSName(ctx context.Context) (string, error) {
191+
info, err := host.InfoWithContext(ctx)
192+
if err != nil {
193+
return "", fmt.Errorf("OSName failed to get platform: %w", err)
194+
}
195+
return info.Platform, nil
196+
}
197+
198+
// OSBuildID returns the OS build ID based on the platform.
199+
func (p systemMetadataProvider) OSBuildID(ctx context.Context) (string, error) {
200+
info, err := host.InfoWithContext(ctx)
201+
if err != nil {
202+
return "", fmt.Errorf("OSBuildID failed to get host info: %w", err)
203+
}
204+
switch runtime.GOOS {
205+
case "darwin":
206+
// macOS: read ProductBuildVersion from SystemVersion.plist
207+
data, err := os.ReadFile("/System/Library/CoreServices/SystemVersion.plist")
208+
if err != nil {
209+
return info.KernelVersion, nil
210+
}
211+
if v := parsePlistValue(data, "ProductBuildVersion"); v != "" {
212+
return v, nil
213+
}
214+
return info.KernelVersion, nil
215+
case "linux":
216+
// Linux: read BUILD_ID from /etc/os-release
217+
data, err := os.ReadFile("/etc/os-release")
218+
if err != nil {
219+
return info.KernelVersion, nil
220+
}
221+
if v := parseOSReleaseValue(data, "BUILD_ID"); v != "" {
222+
return v, nil
223+
}
224+
return info.KernelVersion, nil
225+
default:
226+
return info.KernelVersion, nil
227+
}
228+
}
229+
230+
// parsePlistValue parses the XML plist and returns the <string> value after the given <key>.
231+
func parsePlistValue(data []byte, key string) string {
232+
decoder := xml.NewDecoder(bytes.NewReader(data))
233+
var currKey string
234+
for {
235+
tok, err := decoder.Token()
236+
if err != nil {
237+
break
238+
}
239+
if se, ok := tok.(xml.StartElement); ok {
240+
if se.Name.Local == "key" {
241+
var k string
242+
if err2 := decoder.DecodeElement(&k, &se); err2 == nil {
243+
currKey = k
244+
}
245+
} else if se.Name.Local == "string" && currKey == key {
246+
var v string
247+
if err2 := decoder.DecodeElement(&v, &se); err2 == nil {
248+
return v
249+
}
250+
}
251+
}
252+
}
253+
return ""
254+
}
255+
256+
// parseOSReleaseValue parses key=value pairs from os-release data.
257+
func parseOSReleaseValue(data []byte, key string) string {
258+
lines := strings.Split(string(data), "\n")
259+
prefix := key + "="
260+
for _, line := range lines {
261+
if strings.HasPrefix(line, prefix) {
262+
return strings.Trim(line[len(prefix):], `"`)
263+
}
264+
}
265+
return ""
266+
}
267+
181268
func (systemMetadataProvider) HostArch() (string, error) {
182269
return internal.GOARCHtoHostArch(runtime.GOARCH), nil
183270
}

processor/resourcedetectionprocessor/internal/system/documentation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
| host.ip | IP addresses for the host | Any Slice | false |
2020
| host.mac | MAC addresses for the host | Any Slice | false |
2121
| host.name | The host.name | Any Str | true |
22+
| os.build.id | The os.build.id | Any Str | false |
2223
| os.description | Human readable OS version information. | Any Str | false |
24+
| os.name | The os.name | Any Str | false |
2325
| os.type | The os.type | Any Str | true |
2426
| os.version | The os.version | Any Str | false |

processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_config_test.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

processor/resourcedetectionprocessor/internal/system/internal/metadata/generated_resource_test.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

processor/resourcedetectionprocessor/internal/system/internal/metadata/testdata/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ all_set:
2323
enabled: true
2424
host.name:
2525
enabled: true
26+
os.build.id:
27+
enabled: true
2628
os.description:
2729
enabled: true
30+
os.name:
31+
enabled: true
2832
os.type:
2933
enabled: true
3034
os.version:
@@ -53,8 +57,12 @@ none_set:
5357
enabled: false
5458
host.name:
5559
enabled: false
60+
os.build.id:
61+
enabled: false
5662
os.description:
5763
enabled: false
64+
os.name:
65+
enabled: false
5866
os.type:
5967
enabled: false
6068
os.version:

processor/resourcedetectionprocessor/internal/system/metadata.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ resource_attributes:
2323
description: The os.version
2424
type: string
2525
enabled: false
26+
os.build.id:
27+
description: The os.build.id
28+
type: string
29+
enabled: false
30+
os.name:
31+
description: The os.name
32+
type: string
33+
enabled: false
2634
host.arch:
2735
description: The host.arch
2836
type: string

processor/resourcedetectionprocessor/internal/system/system.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,20 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem
153153
d.rb.SetHostIP(hostIPAttribute)
154154
d.rb.SetHostMac(hostMACAttribute)
155155
d.rb.SetOsDescription(osDescription)
156+
if d.cfg.ResourceAttributes.OsName.Enabled {
157+
if osName, err2 := d.provider.OSName(ctx); err2 == nil {
158+
d.rb.SetOsName(osName)
159+
} else {
160+
d.logger.Warn("failed to get OS name", zap.Error(err2))
161+
}
162+
}
163+
if d.cfg.ResourceAttributes.OsBuildID.Enabled {
164+
if osBuildID, err2 := d.provider.OSBuildID(ctx); err2 == nil {
165+
d.rb.SetOsBuildID(osBuildID)
166+
} else {
167+
d.logger.Warn("failed to get OS build id", zap.Error(err2))
168+
}
169+
}
156170
if len(cpuInfo) > 0 {
157171
setHostCPUInfo(d, cpuInfo[0])
158172
}

processor/resourcedetectionprocessor/internal/system/system_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ func (m *mockMetadata) CPUInfo(_ context.Context) ([]cpu.InfoStat, error) {
8888
return args.Get(0).([]cpu.InfoStat), args.Error(1)
8989
}
9090

91+
// OSName returns a mock OS name.
92+
func (m *mockMetadata) OSName(_ context.Context) (string, error) {
93+
args := m.MethodCalled("OSName")
94+
return args.String(0), args.Error(1)
95+
}
96+
97+
// OSBuildID returns a mock OS build ID.
98+
func (m *mockMetadata) OSBuildID(_ context.Context) (string, error) {
99+
args := m.MethodCalled("OSBuildID")
100+
return args.String(0), args.Error(1)
101+
}
102+
91103
var (
92104
testIPsAttribute = []any{"192.168.1.140", "fe80::abc2:4a28:737a:609e"}
93105
testIPsAddresses = []net.IP{net.ParseIP(testIPsAttribute[0].(string)), net.ParseIP(testIPsAttribute[1].(string))}
@@ -405,6 +417,28 @@ func TestDetectCPUInfo(t *testing.T) {
405417
assert.Equal(t, expected, res.Attributes().AsRaw())
406418
}
407419

420+
func TestDetectOSNameAndBuildID(t *testing.T) {
421+
md := &mockMetadata{}
422+
md.On("FQDN").Return("fqdn", nil)
423+
md.On("OSDescription").Return("desc", nil)
424+
md.On("OSType").Return("type", nil)
425+
md.On("OSVersion").Return("ver", nil)
426+
md.On("OSName").Return("MyOS", nil)
427+
md.On("OSBuildID").Return("Build123", nil)
428+
md.On("HostArch").Return("amd64", nil)
429+
430+
cfg := metadata.DefaultResourceAttributesConfig()
431+
cfg.OsName.Enabled = true
432+
cfg.OsBuildID.Enabled = true
433+
detector := newTestDetector(md, []string{"dns"}, cfg)
434+
res, _, err := detector.Detect(context.Background())
435+
require.NoError(t, err)
436+
attrs := res.Attributes().AsRaw()
437+
assert.Equal(t, "MyOS", attrs["os.name"])
438+
assert.Equal(t, "Build123", attrs["os.build.id"])
439+
md.AssertExpectations(t)
440+
}
441+
408442
func newTestDetector(mock *mockMetadata, hostnameSources []string, resCfg metadata.ResourceAttributesConfig) *Detector {
409443
return &Detector{
410444
provider: mock,

0 commit comments

Comments
 (0)