Skip to content

Commit acf2647

Browse files
zacharycmontoyajessicagamio
authored andcommitted
fix(otlp): Ensure all OTLP spans get the full 128-bit trace IDs (#8618)
Previously the upper 64 bits (present in the '_dd.p.tid' tag) were only copied onto the first span of the trace chunk, but not the rest. * Move the conversion to 128-bit hex trace ID to the id.js file * Apply PR feedback to inline 128-bit trace ID for B3 propagation * Apply PR feedback to inline 128-bit trace ID for DatadogSpanContext.toTraceId() * Apply PR feedback to improve performance of the 128-bit trace ID lookup in #transformScopeSpans by relying on implementation detail that the '_dd.p.tid' trace tag is only applied to the first span
1 parent 6c8b36c commit acf2647

5 files changed

Lines changed: 132 additions & 16 deletions

File tree

packages/dd-trace/src/id.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ class Identifier {
7878
return this.toString()
7979
}
8080

81+
/**
82+
* Returns the full hex trace ID. When this is a 64-bit identifier and `traceIdHigh`
83+
* is provided, prepends it to form the 128-bit trace ID. Otherwise returns
84+
* only this identifier's hex representation.
85+
*
86+
* @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits, or undefined
87+
* @returns {string}
88+
*/
89+
toTraceIdHex (traceIdHigh) {
90+
if (traceIdHigh && this.#buffer.length <= 8) {
91+
return traceIdHigh + this.toString(16)
92+
}
93+
return this.toString(16)
94+
}
95+
8196
/**
8297
* @param {Identifier} other
8398
* @returns {boolean}

packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const SPAN_KIND_CONSUMER = protoSpanKind.values.SPAN_KIND_CONSUMER
1616
// Cached zero Identifier used to detect zero IDs without re-allocating per span.
1717
const ZERO_ID = id('0')
1818

19+
// DD propagation tag carrying the upper 64 bits of a 128-bit trace ID as 16 hex chars.
20+
// span_format.js#extractChunkTags only copies this onto the first-in-chunk span, so the
21+
// transformer scans the batch to find it and applies it to every span's traceId.
22+
const TRACE_ID_128 = '_dd.p.tid'
23+
1924
/**
2025
* @typedef {import('../../id').Identifier} Identifier
2126
*
@@ -65,6 +70,7 @@ const STATUS_CODE_ERROR = 2
6570
const EXCLUDED_META_KEYS = new Set([
6671
'_dd.span_links',
6772
'span.kind',
73+
TRACE_ID_128,
6874
])
6975

7076
/**
@@ -113,6 +119,18 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
113119
* @returns {object[]} Array of scope span objects
114120
*/
115121
#transformScopeSpans (spans) {
122+
let traceKey
123+
let traceIdHigh
124+
const otlpSpans = spans.map((span) => {
125+
// `_dd.p.tid` lives only on the first-in-chunk span of each trace.
126+
// Reset at each trace boundary for batching of multiple traces.
127+
const key = span.trace_id.toString(16)
128+
if (key !== traceKey) {
129+
traceKey = key
130+
traceIdHigh = span.meta?.[TRACE_ID_128]?.toLowerCase()
131+
}
132+
return this.#transformSpan(span, traceIdHigh)
133+
})
116134
return [{
117135
scope: {
118136
name: 'dd-trace-js',
@@ -121,22 +139,23 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
121139
droppedAttributesCount: 0,
122140
},
123141
schemaUrl: '',
124-
spans: spans.map(span => this.#transformSpan(span)),
142+
spans: otlpSpans,
125143
}]
126144
}
127145

128146
/**
129147
* Transforms a single DD-formatted span to an OTLP Span object.
130148
*
131149
* @param {DDFormattedSpan} span - DD-formatted span to transform
150+
* @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits of the trace ID
132151
* @returns {object} OTLP Span object
133152
*/
134-
#transformSpan (span) {
153+
#transformSpan (span, traceIdHigh) {
135154
const parentId = span.parent_id
136155
const links = this.#extractLinks(span.meta?.['_dd.span_links'])
137156

138157
return {
139-
traceId: this.#idToBytes(span.trace_id, 16),
158+
traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
140159
spanId: this.#idToBytes(span.span_id, 8),
141160
parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
142161
name: span.resource,

packages/dd-trace/src/opentracing/propagation/text_map.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ class TextMapPropagator {
284284
(DD_MAJOR < 6 && this._hasPropagationStyle('inject', 'b3'))
285285
if (!hasB3multi) return
286286

287-
carrier[b3TraceKey] = this._getB3TraceId(spanContext)
287+
carrier[b3TraceKey] = spanContext._traceId.toTraceIdHex(spanContext._trace.tags['_dd.p.tid'])
288288
carrier[b3SpanKey] = spanContext._spanId.toString(16)
289289
carrier[b3SampledKey] = spanContext._sampling.priority >= AUTO_KEEP ? '1' : '0'
290290

@@ -303,7 +303,7 @@ class TextMapPropagator {
303303
(DD_MAJOR >= 6 && this._hasPropagationStyle('inject', 'b3'))
304304
if (!hasB3SingleHeader) return
305305

306-
const traceId = this._getB3TraceId(spanContext)
306+
const traceId = spanContext._traceId.toTraceIdHex(spanContext._trace.tags['_dd.p.tid'])
307307
const spanId = spanContext._spanId.toString(16)
308308
const sampled = spanContext._sampling.priority >= AUTO_KEEP ? '1' : '0'
309309

@@ -859,14 +859,6 @@ class TextMapPropagator {
859859
}
860860
}
861861

862-
_getB3TraceId (spanContext) {
863-
if (spanContext._traceId.toBuffer().length <= 8 && spanContext._trace.tags['_dd.p.tid']) {
864-
return spanContext._trace.tags['_dd.p.tid'] + spanContext._traceId.toString(16)
865-
}
866-
867-
return spanContext._traceId.toString(16)
868-
}
869-
870862
/**
871863
* @param {number} traceparentSampled
872864
* @param {number|undefined} tracestateSamplingPriority

packages/dd-trace/src/opentracing/span_context.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ class DatadogSpanContext {
4646

4747
toTraceId (get128bitId = false) {
4848
if (get128bitId) {
49-
return this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128]
50-
? this._trace.tags[TRACE_ID_128] + this._traceId.toString(16).padStart(16, '0')
51-
: this._traceId.toString(16).padStart(32, '0')
49+
return this._traceId.toTraceIdHex(this._trace.tags[TRACE_ID_128]).padStart(32, '0')
5250
}
5351
return this._traceId.toString(10)
5452
}

packages/dd-trace/test/opentelemetry/traces.spec.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,98 @@ describe('OpenTelemetry Traces', () => {
462462
['/api/first', '/api/second']
463463
)
464464
})
465+
466+
describe('128-bit trace ID handling', () => {
467+
// DD splits 128-bit trace IDs: low 64 bits live on the span Identifier,
468+
// upper 64 bits live in trace-level tags as `_dd.p.tid` (16 hex chars).
469+
// span_format.js#extractChunkTags only copies trace-level tags onto the
470+
// first-in-chunk span, so the transformer has to look across the batch
471+
// to find `_dd.p.tid` and then apply it to every span.
472+
473+
it('reconstructs the full 128-bit traceId for every span in a batch from _dd.p.tid', () => {
474+
const transformer = new OtlpTraceTransformer({})
475+
const lowHex = 'abcdef0123456789'
476+
const tidHigh = '1234567890abcdef'
477+
const traceIdLow = id(lowHex)
478+
479+
const firstSpan = createMockSpan({
480+
trace_id: traceIdLow,
481+
meta: { 'span.kind': 'internal', '_dd.p.tid': tidHigh },
482+
})
483+
const secondSpan = createMockSpan({
484+
trace_id: traceIdLow,
485+
span_id: id('bbbbbbbbbbbbbbbb'),
486+
meta: { 'span.kind': 'internal' },
487+
})
488+
const thirdSpan = createMockSpan({
489+
trace_id: traceIdLow,
490+
span_id: id('cccccccccccccccc'),
491+
meta: { 'span.kind': 'internal' },
492+
})
493+
494+
const decoded = decodePayload(transformer.transformSpans([firstSpan, secondSpan, thirdSpan]))
495+
const otlpSpans = decoded.resourceSpans[0].scopeSpans[0].spans
496+
497+
const expectedTraceId = tidHigh + lowHex
498+
for (const otlpSpan of otlpSpans) {
499+
assert.strictEqual(otlpSpan.traceId, expectedTraceId)
500+
}
501+
})
502+
503+
it('drops _dd.p.tid from OTLP attributes once consumed into traceId', () => {
504+
const transformer = new OtlpTraceTransformer({})
505+
const span = createMockSpan({
506+
trace_id: id('abcdef0123456789'),
507+
meta: { 'span.kind': 'internal', '_dd.p.tid': '1234567890abcdef' },
508+
})
509+
510+
const decoded = decodePayload(transformer.transformSpans([span]))
511+
const attrs = extractAttrs(decoded.resourceSpans[0].scopeSpans[0].spans[0].attributes)
512+
513+
assert.strictEqual(attrs['_dd.p.tid'], undefined)
514+
})
515+
516+
it('zero-pads traceId to 32 hex chars when no _dd.p.tid is present', () => {
517+
const transformer = new OtlpTraceTransformer({})
518+
const span = createMockSpan({
519+
trace_id: id('abcdef0123456789'),
520+
meta: { 'span.kind': 'internal' },
521+
})
522+
523+
const decoded = decodePayload(transformer.transformSpans([span]))
524+
const otlpSpan = decoded.resourceSpans[0].scopeSpans[0].spans[0]
525+
526+
assert.strictEqual(otlpSpan.traceId, '0000000000000000abcdef0123456789')
527+
})
528+
529+
it('lowercases an uppercase _dd.p.tid so the OTLP traceId is canonical lowercase hex', () => {
530+
const transformer = new OtlpTraceTransformer({})
531+
const lowHex = 'abcdef0123456789'
532+
const span = createMockSpan({
533+
trace_id: id(lowHex),
534+
meta: { 'span.kind': 'internal', '_dd.p.tid': '1234567890ABCDEF' },
535+
})
536+
537+
const decoded = decodePayload(transformer.transformSpans([span]))
538+
const otlpSpan = decoded.resourceSpans[0].scopeSpans[0].spans[0]
539+
540+
assert.strictEqual(otlpSpan.traceId, '1234567890abcdef' + lowHex)
541+
})
542+
543+
it('uses a full 16-byte trace_id buffer directly without consulting _dd.p.tid', () => {
544+
const transformer = new OtlpTraceTransformer({})
545+
const unusedTidHigh = '1000000000000000'
546+
const span = createMockSpan({
547+
trace_id: id('1234567890abcdef1234567890abcdef'),
548+
meta: { 'span.kind': 'internal', '_dd.p.tid': unusedTidHigh },
549+
})
550+
551+
const decoded = decodePayload(transformer.transformSpans([span]))
552+
const otlpSpan = decoded.resourceSpans[0].scopeSpans[0].spans[0]
553+
554+
assert.strictEqual(otlpSpan.traceId, '1234567890abcdef1234567890abcdef')
555+
})
556+
})
465557
})
466558

467559
describe('Exporter', () => {

0 commit comments

Comments
 (0)