Skip to content

Commit 622b919

Browse files
authored
feat(otel-logs): map instrumentation scope name to otel.scope.name (#52945)
## Summary - Adds `otel.scope.name` to the OTLP → DD log translation in `opentelemetry-mapping-go/otlp/logs`, covering the **Datadog Agent OTLP receiver** and **DDOT** ingestion paths - Counterpart to [ddoghq/dd-source#3393](https://github.com/ddoghq/dd-source/pull/3393), which adds the same mapping for the direct `otlp.datad0g.com/v1/logs` intake path - The field is `otel.scope.name` in `AdditionalProperties`, consistent with the existing `otel.*` namespace for other log-record-level fields ## Test plan - [ ] `go test ./pkg/opentelemetry-mapping-go/otlp/logs/...` passes (new "scope name" test case added) - [ ] Verify `otel.scope.name` appears in Logs Explorer for logs ingested via Agent OTLP receiver 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: munir.abdinur <munir.abdinur@datadoghq.com>
1 parent d17fdd9 commit 622b919

3 files changed

Lines changed: 160 additions & 4 deletions

File tree

pkg/opentelemetry-mapping-go/otlp/logs/transform.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ const (
4242
otelSeverityNumber = otelNamespace + ".severity_number"
4343
otelSeverityText = otelNamespace + ".severity_text"
4444
otelTimestamp = otelNamespace + ".timestamp"
45+
otelScopeName = otelNamespace + ".scope.name"
46+
otelScopeVersion = otelNamespace + ".scope.version"
47+
otelLibraryName = otelNamespace + ".library.name" // deprecated alias for otel.scope.name
48+
otelLibraryVersion = otelNamespace + ".library.version" // deprecated alias for otel.scope.version
4549
otelEventName = otelNamespace + ".event_name"
4650
)
4751
const (
@@ -92,6 +96,16 @@ func transform(lr plog.LogRecord, host, service string, res pcommon.Resource, sc
9296
l.Message = v.AsString()
9397
case "status", "severity", "level", "syslog.severity":
9498
status = v.AsString()
99+
case otelLibraryName:
100+
// Deprecated alias: only takes effect if the canonical otel.scope.name attribute
101+
// isn't also present, since attribute iteration order isn't guaranteed.
102+
if _, ok := lr.Attributes().Get(otelScopeName); !ok {
103+
l.AdditionalProperties[otelScopeName] = v.AsString()
104+
}
105+
case otelLibraryVersion:
106+
if _, ok := lr.Attributes().Get(otelScopeVersion); !ok {
107+
l.AdditionalProperties[otelScopeVersion] = v.AsString()
108+
}
95109
case "traceid", "trace_id", "contextmap.traceid", "oteltraceid":
96110
traceID, err := decodeTraceID(v.AsString())
97111
if err != nil {
@@ -127,18 +141,39 @@ func transform(lr plog.LogRecord, host, service string, res pcommon.Resource, sc
127141
return true
128142
})
129143
res.Attributes().Range(func(k string, v pcommon.Value) bool {
130-
// "hostname" and "service" are reserved keywords in HTTPLogItem
131-
// Prefix the keys so they aren't overwritten when marshalling
132-
if k == "hostname" || k == "service" {
144+
switch k {
145+
case "hostname", "service":
146+
// "hostname" and "service" are reserved keywords in HTTPLogItem
147+
// Prefix the keys so they aren't overwritten when marshalling
133148
l.AdditionalProperties["otel."+k] = v.AsString()
134-
} else {
149+
case otelLibraryName:
150+
if _, ok := res.Attributes().Get(otelScopeName); !ok {
151+
l.AdditionalProperties[otelScopeName] = v.AsString()
152+
}
153+
case otelLibraryVersion:
154+
if _, ok := res.Attributes().Get(otelScopeVersion); !ok {
155+
l.AdditionalProperties[otelScopeVersion] = v.AsString()
156+
}
157+
default:
135158
l.AdditionalProperties[k] = v.AsString()
136159
}
137160
return true
138161
})
139162
for k, v := range scope.Attributes().Range {
140163
l.AdditionalProperties[k] = v.AsString()
141164
}
165+
// The instrumentation scope's Name/Version are canonical but processed last, so only
166+
// fill them in if a log or resource attribute hasn't already set otel.scope.name/version.
167+
if name := scope.Name(); name != "" {
168+
if _, ok := l.AdditionalProperties[otelScopeName]; !ok {
169+
l.AdditionalProperties[otelScopeName] = name
170+
}
171+
}
172+
if version := scope.Version(); version != "" {
173+
if _, ok := l.AdditionalProperties[otelScopeVersion]; !ok {
174+
l.AdditionalProperties[otelScopeVersion] = version
175+
}
176+
}
142177
if traceID := lr.TraceID(); !traceID.IsEmpty() {
143178
l.AdditionalProperties[ddTraceID] = strconv.FormatUint(traceIDToUint64(traceID), 10)
144179
l.AdditionalProperties[otelTraceID] = hex.EncodeToString(traceID[:])

pkg/opentelemetry-mapping-go/otlp/logs/transform_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,113 @@ func generateTranslatorTestCases(traceID [16]byte, spanID [8]byte, ddTr uint64,
672672
},
673673
},
674674
},
675+
{
676+
name: "scope name",
677+
args: args{
678+
lr: func() plog.LogRecord {
679+
l := plog.NewLogRecord()
680+
l.Body().SetStr("hello world")
681+
l.SetSeverityNumber(5)
682+
return l
683+
}(),
684+
res: pcommon.NewResource(),
685+
scope: func() pcommon.InstrumentationScope {
686+
s := pcommon.NewInstrumentationScope()
687+
s.SetName("go.opentelemetry.io/contrib/bridges/otelslog")
688+
return s
689+
}(),
690+
},
691+
want: datadogV2.HTTPLogItem{
692+
Ddtags: datadog.PtrString("otel_source:test"),
693+
Message: *datadog.PtrString("hello world"),
694+
AdditionalProperties: map[string]interface{}{
695+
"status": "debug",
696+
otelSeverityNumber: "5",
697+
otelScopeName: "go.opentelemetry.io/contrib/bridges/otelslog",
698+
},
699+
},
700+
},
701+
{
702+
name: "scope version",
703+
args: args{
704+
lr: func() plog.LogRecord {
705+
l := plog.NewLogRecord()
706+
l.Body().SetStr("hello world")
707+
l.SetSeverityNumber(5)
708+
return l
709+
}(),
710+
res: pcommon.NewResource(),
711+
scope: func() pcommon.InstrumentationScope {
712+
s := pcommon.NewInstrumentationScope()
713+
s.SetName("go.opentelemetry.io/contrib/bridges/otelslog")
714+
s.SetVersion("v1.2.3")
715+
return s
716+
}(),
717+
},
718+
want: datadogV2.HTTPLogItem{
719+
Ddtags: datadog.PtrString("otel_source:test"),
720+
Message: *datadog.PtrString("hello world"),
721+
AdditionalProperties: map[string]interface{}{
722+
"status": "debug",
723+
otelSeverityNumber: "5",
724+
otelScopeName: "go.opentelemetry.io/contrib/bridges/otelslog",
725+
otelScopeVersion: "v1.2.3",
726+
},
727+
},
728+
},
729+
{
730+
name: "library name/version aliases remapped to scope",
731+
args: args{
732+
lr: func() plog.LogRecord {
733+
l := plog.NewLogRecord()
734+
l.Body().SetStr("hello world")
735+
l.SetSeverityNumber(5)
736+
l.Attributes().PutStr(otelLibraryName, "my-library")
737+
l.Attributes().PutStr(otelLibraryVersion, "v0.1.0")
738+
return l
739+
}(),
740+
res: pcommon.NewResource(),
741+
scope: pcommon.NewInstrumentationScope(),
742+
},
743+
want: datadogV2.HTTPLogItem{
744+
Ddtags: datadog.PtrString("otel_source:test"),
745+
Message: *datadog.PtrString("hello world"),
746+
AdditionalProperties: map[string]interface{}{
747+
"status": "debug",
748+
otelSeverityNumber: "5",
749+
otelScopeName: "my-library",
750+
otelScopeVersion: "v0.1.0",
751+
},
752+
},
753+
},
754+
{
755+
name: "explicit otel.scope.name attribute takes precedence over library alias and scope metadata",
756+
args: args{
757+
lr: func() plog.LogRecord {
758+
l := plog.NewLogRecord()
759+
l.Body().SetStr("hello world")
760+
l.SetSeverityNumber(5)
761+
l.Attributes().PutStr(otelScopeName, "explicit-scope-name")
762+
l.Attributes().PutStr(otelLibraryName, "library-alias-name")
763+
return l
764+
}(),
765+
res: pcommon.NewResource(),
766+
scope: func() pcommon.InstrumentationScope {
767+
s := pcommon.NewInstrumentationScope()
768+
s.SetName("scope-metadata-name")
769+
return s
770+
}(),
771+
},
772+
want: datadogV2.HTTPLogItem{
773+
Ddtags: datadog.PtrString("otel_source:test"),
774+
Message: *datadog.PtrString("hello world"),
775+
AdditionalProperties: map[string]interface{}{
776+
"status": "debug",
777+
otelSeverityNumber: "5",
778+
otelScopeName: "explicit-scope-name",
779+
},
780+
},
781+
},
675782
{
676783
name: "array attribute with strings",
677784
args: args{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Each section from every release note are combined when the
2+
# CHANGELOG.rst is rendered. So the text needs to be worded so that
3+
# it does not depend on any information only available in another
4+
# section. This may mean repeating some details, but each section
5+
# must be readable independently of the other.
6+
#
7+
# Each section note must be formatted as reStructuredText.
8+
---
9+
enhancements:
10+
- |
11+
OTLP ingest and DDOT: Logs received through the OTLP receiver now map the instrumentation
12+
scope name and version to ``otel.scope.name`` and ``otel.scope.version``. Incoming
13+
``otel.library.name``/``otel.library.version`` attributes (the deprecated
14+
OpenTelemetry predecessors) are remapped to the canonical ``otel.scope.*`` keys.

0 commit comments

Comments
 (0)