Skip to content

Commit f3458d3

Browse files
committed
[receiver/datadog] Support 128 bits TraceID coming from Datadog
With this commit, we add support for 128 bits TraceIDs coming from Datadog instrumented services. This can happen when an OTel instrumented service calls a downstream Datadog instrumented one. Datadog instrumentation libraries store the 128 bits TraceID into two different fields: * TraceID: lower 64 bits of the 128 bits TraceID * _dd.p.tid: upper 64 bits of the 128 bits TraceID This commit adds logic that reconstructs the 128 bits TraceID. Before this commit, only the lower 64 bits were used as TraceID. Fixes open-telemetry#36926
1 parent 3ebd0dd commit f3458d3

File tree

3 files changed

+102
-4
lines changed

3 files changed

+102
-4
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: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: datadogreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Support 128 bits TraceID coming from Datadog
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: [36926]
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: [user]

receiver/datadogreceiver/internal/translator/traces_translator.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"go.opentelemetry.io/collector/pdata/pcommon"
2121
"go.opentelemetry.io/collector/pdata/ptrace"
2222
semconv "go.opentelemetry.io/collector/semconv/v1.16.0"
23-
"go.opentelemetry.io/otel/trace"
23+
oteltrace "go.opentelemetry.io/otel/trace"
2424
"google.golang.org/protobuf/proto"
2525

2626
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator/header"
@@ -90,6 +90,13 @@ func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces {
9090
// now instead of being a span level attribute.
9191
groupByService := make(map[string]ptrace.SpanSlice)
9292

93+
// Datadog traces split a 128 bits trace id in two parts: TraceID and Tags._dd_p_tid. This happens if the
94+
// instrumented service received a TraceContext from an OTel instrumented service. When it happens, we need
95+
// to concatenate the two into newSpan.TraceID.
96+
// The following map keeps track of the TraceIDs we process as only the first span has the upper 64 bits from the
97+
// 128 bits trace ID.
98+
map64to128 := make(map[uint64]pcommon.TraceID)
99+
93100
for _, trace := range traces {
94101
for _, span := range trace {
95102
slice, exist := groupByService[span.Service]
@@ -101,7 +108,24 @@ func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces {
101108

102109
_ = tagsToSpanLinks(span.GetMeta(), newSpan.Links())
103110

104-
newSpan.SetTraceID(uInt64ToTraceID(0, span.TraceID))
111+
// Reconstructing the 128 bits TraceID, if available or cached.
112+
// Note: This may not be resilient to related spans being flushed separately in datadog's tracing libraries.
113+
// It might also not work if multiple datadog instrumented services are chained.
114+
if val, ok := span.Meta["_dd.p.tid"]; ok {
115+
hexTraceID := val + strconv.FormatUint(span.TraceID, 16)
116+
traceID, err := oteltrace.TraceIDFromHex(hexTraceID)
117+
if err != nil {
118+
panic(err)
119+
}
120+
newSpan.SetTraceID(pcommon.TraceID(traceID))
121+
122+
// Child spans don't have _dd.p.tid, we cache it.
123+
map64to128[span.TraceID] = pcommon.TraceID(traceID)
124+
} else if val, ok := map64to128[span.TraceID]; ok {
125+
newSpan.SetTraceID(val)
126+
} else {
127+
newSpan.SetTraceID(uInt64ToTraceID(0, span.TraceID))
128+
}
105129
newSpan.SetSpanID(uInt64ToSpanID(span.SpanID))
106130
newSpan.SetStartTimestamp(pcommon.Timestamp(span.Start))
107131
newSpan.SetEndTimestamp(pcommon.Timestamp(span.Start + span.Duration))
@@ -195,14 +219,14 @@ func tagsToSpanLinks(tags map[string]string, dest ptrace.SpanLinkSlice) error {
195219
link := dest.AppendEmpty()
196220

197221
// Convert trace id.
198-
rawTrace, errTrace := trace.TraceIDFromHex(span.TraceID)
222+
rawTrace, errTrace := oteltrace.TraceIDFromHex(span.TraceID)
199223
if errTrace != nil {
200224
return errTrace
201225
}
202226
link.SetTraceID(pcommon.TraceID(rawTrace))
203227

204228
// Convert span id.
205-
rawSpan, errSpan := trace.SpanIDFromHex(span.SpanID)
229+
rawSpan, errSpan := oteltrace.SpanIDFromHex(span.SpanID)
206230
if errSpan != nil {
207231
return errSpan
208232
}

receiver/datadogreceiver/internal/translator/traces_translator_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,50 @@ func TestUpsertHeadersAttributes(t *testing.T) {
221221
assert.True(t, ok)
222222
assert.Equal(t, "dotnet", val.Str())
223223
}
224+
225+
func TestToTraces64to128bits(t *testing.T) {
226+
// Test that we properly reconstruct a 128 bits TraceID from Datadog spans.
227+
// This is necessary when a datadog instrumented service has been called by an OTel instrumented one as Datadog will
228+
// split the TraceID into two different values:
229+
// * TraceID holds the lower 64 bits
230+
// * `_dd.p.tid` holds the upper 64 bits
231+
expectedTraceID := "f233b7e1421e8bde1d99f09757cf199d"
232+
spanIDParentOtel, _ := strconv.ParseUint("6b953724b399048a", 16, 32)
233+
spanIDParentDD, _ := strconv.ParseUint("039f8ec65ed09993", 16, 32)
234+
spanIDChildDD, _ := strconv.ParseUint("5ab19b8ebe922796", 16, 32)
235+
236+
spans := []pb.Span{
237+
{
238+
TraceID: 2133000431340558749,
239+
SpanID: spanIDParentDD,
240+
ParentID: spanIDParentOtel,
241+
Meta: map[string]string{
242+
"_dd.p.tid": "f233b7e1421e8bde",
243+
},
244+
},
245+
{
246+
TraceID: 2133000431340558749,
247+
SpanID: spanIDChildDD,
248+
ParentID: spanIDParentDD,
249+
},
250+
}
251+
payload := &pb.TracerPayload{
252+
Chunks: traceChunksFromSpans(spans),
253+
}
254+
255+
req := &http.Request{
256+
Header: http.Header{},
257+
}
258+
req.Header.Set(header.Lang, "go")
259+
260+
traces := ToTraces(payload, req)
261+
assert.Equal(t, 2, traces.SpanCount(), "Expected 2 spans")
262+
263+
for _, rs := range traces.ResourceSpans().All() {
264+
for _, ss := range rs.ScopeSpans().All() {
265+
for _, span := range ss.Spans().All() {
266+
assert.Equal(t, expectedTraceID, span.TraceID().String(), "Expected TraceID to be "+expectedTraceID)
267+
}
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)