From 1d7e8faffa8e2f04f48489218ef9ec808aa99b84 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 24 Sep 2026 14:51:31 -0400 Subject: [PATCH 1/6] feat(tracing): add OTLP adoption markers to trace payloads Declare the export mode and semantics mode on the wire so the backend can measure OTLP adoption instead of inferring it at ingest time. Native v0.4/v0.5 payloads carry _dd.sdk.otlp_export=false once per payload; OTLP exports carry _dd.sdk.otlp_export=true and datadog.sdk.semantics as resource attributes. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/dd-trace/src/constants.js | 2 + packages/dd-trace/src/encode/0.4.js | 35 +++++++- packages/dd-trace/src/encode/0.5.js | 10 ++- .../dd-trace/src/opentelemetry/trace/index.js | 5 ++ .../opentelemetry/trace/otlp_transformer.js | 6 +- packages/dd-trace/test/encode/0.4.spec.js | 84 +++++++++++++++---- packages/dd-trace/test/encode/0.5.spec.js | 41 ++++++++- .../test/opentelemetry/traces.spec.js | 71 ++++++++++++++++ 8 files changed, 232 insertions(+), 22 deletions(-) diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index 685485c3dce..b0e2131ff7b 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -22,6 +22,8 @@ module.exports = { SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate', SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second', SVC_SRC_KEY: '_dd.svc_src', + SDK_OTLP_EXPORT_KEY: '_dd.sdk.otlp_export', + SDK_SEMANTICS_KEY: 'datadog.sdk.semantics', DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent', DATADOG_MINI_AGENT_PATH: '/tmp/datadog/mini_agent_ready', DECISION_MAKER_KEY: '_dd.p.dm', diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index 24c489294a2..16713a60f11 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -1,6 +1,7 @@ 'use strict' const getConfig = require('../config') +const { SDK_OTLP_EXPORT_KEY } = require('../constants') const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack') const log = require('../log') const { normalizeSpan, eventTimeNano } = require('./tags-processors') @@ -224,6 +225,17 @@ function escapeJsonString (value) { return '"' + value + '"' } +/** + * Copies `meta` rather than mutating it so the formatted span stays untouched. Reaching a native + * encoder means the payload is not OTLP, so the export marker is always "false" here. + * + * @param {Record} [meta] + * @returns {Record} + */ +function withPayloadTags (meta) { + return { ...meta, [SDK_OTLP_EXPORT_KEY]: 'false' } +} + function lazyEncodedTraceBufferLogger (bytes, start, end) { const hex = bytes.buffer.subarray(start, end).toString('hex').match(/../g).join(' ') return `Adding encoded trace to buffer: ${hex}` @@ -315,6 +327,7 @@ class AgentEncoder { _encode (bytes, trace) { bytes.writeArrayPrefix(trace) + let payloadTagsPending = this._claimPayloadTags(trace) const formatSpan = this.#formatSpan const stringMap = this._stringMap // Snapshot the string buffer so we can detect a mid-encode resize and @@ -430,7 +443,12 @@ class AgentEncoder { } bytes.writeIntOrFloat(span.duration) - this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta) + if (payloadTagsPending) { + payloadTagsPending = false + this.#encodeMetaEntries(bytes, KEY_META_PREFIX, withPayloadTags(span.meta)) + } else { + this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta) + } this.#encodeMetaEntries(bytes, KEY_METRICS_PREFIX, span.metrics) if (span.span_events) { @@ -467,7 +485,20 @@ class AgentEncoder { } } + /** + * Claims the payload-scoped tags for this trace when it is the first non-empty trace since the + * last reset, so they are written exactly once per payload. + * + * @param {object[]} trace + */ + _claimPayloadTags (trace) { + if (!this._payloadTagsPending || trace.length === 0) return false + this._payloadTagsPending = false + return true + } + _reset () { + this._payloadTagsPending = true this._traceCount = 0 this._traceBytes.reset() this._stringCount = 0 @@ -976,4 +1007,4 @@ function memoizedLogDebug (key, message, value) { } } -module.exports = { AgentEncoder, stringifySpanEvents } +module.exports = { AgentEncoder, stringifySpanEvents, withPayloadTags } diff --git a/packages/dd-trace/src/encode/0.5.js b/packages/dd-trace/src/encode/0.5.js index 6271537d025..5c269f356a9 100644 --- a/packages/dd-trace/src/encode/0.5.js +++ b/packages/dd-trace/src/encode/0.5.js @@ -2,7 +2,7 @@ const { MAX_SIZE, OverflowError } = require('../msgpack') const { normalizeSpan } = require('./tags-processors') -const { AgentEncoder: BaseEncoder, stringifySpanEvents } = require('./0.4') +const { AgentEncoder: BaseEncoder, stringifySpanEvents, withPayloadTags } = require('./0.4') const ARRAY_OF_TWO = 0x92 const ARRAY_OF_TWELVE = 0x9C @@ -56,6 +56,7 @@ class AgentEncoder extends BaseEncoder { _encode (bytes, trace) { bytes.writeArrayPrefix(trace) + let payloadTagsPending = this._claimPayloadTags(trace) const stringMap = this._stringMap for (let span of trace) { @@ -84,7 +85,12 @@ class AgentEncoder extends BaseEncoder { bytes.writeIntOrFloat(span.start || 0) bytes.writeIntOrFloat(span.duration || 0) bytes.writeIntOrFloat(span.error) - this._encodeMap(bytes, span.meta || {}) + if (payloadTagsPending) { + payloadTagsPending = false + this._encodeMap(bytes, withPayloadTags(span.meta)) + } else { + this._encodeMap(bytes, span.meta || {}) + } this._encodeMap(bytes, span.metrics || {}) this._encodeString(bytes, span.type) } diff --git a/packages/dd-trace/src/opentelemetry/trace/index.js b/packages/dd-trace/src/opentelemetry/trace/index.js index 6c5a9ae7f46..6da102301cd 100644 --- a/packages/dd-trace/src/opentelemetry/trace/index.js +++ b/packages/dd-trace/src/opentelemetry/trace/index.js @@ -1,6 +1,7 @@ 'use strict' const { VERSION } = require('../../../../../version') +const { SDK_OTLP_EXPORT_KEY, SDK_SEMANTICS_KEY } = require('../../constants') const { registerResourceAttributeRefresh } = require('../resource-attributes') const OtlpHttpTraceExporter = require('./otlp_http_trace_exporter') @@ -46,6 +47,10 @@ function buildResourceAttributes (config) { const { service, version, env, ...filteredTags } = config.tags Object.assign(resourceAttributes, filteredTags) + // Tracer-owned adoption markers; set after the global tags so a user tag can't contradict them. + resourceAttributes[SDK_OTLP_EXPORT_KEY] = 'true' + resourceAttributes[SDK_SEMANTICS_KEY] = config.DD_TRACE_OTEL_SEMANTICS_ENABLED ? 'otel' : 'datadog' + if (config.OTEL_TRACES_SPAN_METRICS_ENABLED) { resourceAttributes['_dd.stats_computed'] = 'true' } diff --git a/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js index baa45a98231..94e0e147dab 100644 --- a/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +++ b/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js @@ -5,6 +5,7 @@ const { getProtobufTypes } = require('../otlp/protobuf_loader') const { VERSION } = require('../../../../../version') const id = require('../../id') const { eventTimeNano } = require('../../encode/tags-processors') +const { SDK_OTLP_EXPORT_KEY, SDK_SEMANTICS_KEY } = require('../../constants') const { protoSpanKind } = getProtobufTypes() const SPAN_KIND_UNSPECIFIED = protoSpanKind.values.SPAN_KIND_UNSPECIFIED @@ -67,11 +68,14 @@ const SPAN_KIND_MAP = { const STATUS_CODE_UNSET = 0 const STATUS_CODE_ERROR = 2 -// DD meta keys that are mapped to dedicated OTLP span fields and should not appear as attributes +// DD meta keys that are mapped to dedicated OTLP span fields, or are tracer-owned resource +// attributes that a user tag must not contradict, and should not appear as span attributes const EXCLUDED_META_KEYS = new Set([ '_dd.span_links', 'span.kind', TRACE_ID_128, + SDK_OTLP_EXPORT_KEY, + SDK_SEMANTICS_KEY, ]) // DD-only error tags that should not appear as attributes when OTel trace semantics are enabled. diff --git a/packages/dd-trace/test/encode/0.4.spec.js b/packages/dd-trace/test/encode/0.4.spec.js index 689e862c2aa..d1e73b2aa67 100644 --- a/packages/dd-trace/test/encode/0.4.spec.js +++ b/packages/dd-trace/test/encode/0.4.spec.js @@ -122,7 +122,7 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { bar: 'baz' }) + assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.sdk.otlp_export': 'false' }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) @@ -200,6 +200,50 @@ describe('encode', () => { assert.throws(() => encoder.encode(data), /something else/) }) + describe('_dd.sdk.otlp_export', () => { + const makeTrace = (length) => Array.from({ length }, () => ({ ...data[0], meta: { bar: 'baz' } })) + const markers = (payload) => msgpack.decode(payload, { useBigInt64: true }) + .map(trace => trace.map(span => span.meta['_dd.sdk.otlp_export'])) + + it('should be written once per payload, on the first span of the first non-empty trace', () => { + encoder.encode([]) + encoder.encode(makeTrace(3)) + encoder.encode(makeTrace(2)) + + assert.deepStrictEqual(markers(encoder.makePayload()), [ + [], + ['false', undefined, undefined], + [undefined, undefined], + ]) + }) + + it('should be written again on the first span after a flush', () => { + encoder.encode(makeTrace(2)) + encoder.makePayload() + encoder.encode(makeTrace(2)) + + assert.deepStrictEqual(markers(encoder.makePayload()), [['false', undefined]]) + }) + + it('should be written again on the first span after a reset', () => { + encoder.encode(makeTrace(1)) + encoder.reset() + encoder.encode(makeTrace(1)) + + assert.deepStrictEqual(markers(encoder.makePayload()), [['false']]) + }) + + it('should override a user tag with the same key without mutating the span', () => { + const trace = makeTrace(1) + trace[0].meta['_dd.sdk.otlp_export'] = 'true' + + encoder.encode(trace) + + assert.deepStrictEqual(markers(encoder.makePayload()), [['false']]) + assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.sdk.otlp_export': 'true' }) + }) + }) + it('should reset after making a payload', () => { encoder.encode(data) encoder.makePayload() @@ -380,7 +424,7 @@ describe('encode', () => { it('should relearn after 32 misses with fewer than 32 hits', () => { const retained = [] - for (let index = 0; index < 27; index++) { + for (let index = 0; index < 26; index++) { retained.push(`insufficient-retained-${index}`) } const write = sinon.spy(encoder._stringBytes, 'write') @@ -395,10 +439,10 @@ describe('encode', () => { encodePayload() cacheLowReusePayload(retained, 'second-miss') - data[0].meta = metaFromKeys([retained[26]]) + data[0].meta = metaFromKeys([retained[25]]) encodePayload() - assert.strictEqual(write.withArgs(retained[26]).callCount, 3) + assert.strictEqual(write.withArgs(retained[25]).callCount, 3) }) it('should relearn after a low-cardinality payload stops using the cache', () => { @@ -420,7 +464,7 @@ describe('encode', () => { it('should disable cross-payload caching after two low-reuse payloads', () => { const retained = [] - for (let index = 0; index < 27; index++) { + for (let index = 0; index < 26; index++) { retained.push(`disabled-retained-${index}`) } const value = 'stable-after-two-low-reuse-payloads' @@ -476,8 +520,9 @@ describe('encode', () => { }) it('should keep cross-payload caching after 32 misses with 32 hits', () => { + // 27 retained keys + the 4 span strings + the payload-scoped `_dd.sdk.otlp_export` key = 32 hits const retained = [] - for (let index = 0; index < 28; index++) { + for (let index = 0; index < 27; index++) { retained.push(`useful-retained-${index}`) } const write = sinon.spy(encoder._stringBytes, 'write') @@ -485,15 +530,15 @@ describe('encode', () => { cacheStringPayloads(retained, 8) cacheLowReusePayload(retained, 'useful-miss') - data[0].meta = metaFromKeys([retained[27]]) + data[0].meta = metaFromKeys([retained[26]]) encodePayload() - assert.strictEqual(write.withArgs(retained[27]).callCount, 2) + assert.strictEqual(write.withArgs(retained[26]).callCount, 2) }) it('should clear a low-reuse strike after a useful payload', () => { const retained = [] - for (let index = 0; index < 27; index++) { + for (let index = 0; index < 26; index++) { retained.push(`recovered-retained-${index}`) } const value = 'stable-after-low-reuse-recovery' @@ -572,7 +617,7 @@ describe('encode', () => { const buffer = encoder.makePayload() const [decodedPayload] = msgpack.decode(buffer, { useBigInt64: true }) - decodedPayload.forEach(decodedData => { + decodedPayload.forEach((decodedData, index) => { assertObjectContains(decodedData, { name: 'bigger name than expected', resource: 'test-r', @@ -582,9 +627,10 @@ describe('encode', () => { }) assert.strictEqual(decodedData.start, 123) assert.strictEqual(decodedData.duration, 456) - assert.deepStrictEqual(decodedData.meta, { - bar: 'baz', - }) + assert.deepStrictEqual( + decodedData.meta, + index === 0 ? { bar: 'baz', '_dd.sdk.otlp_export': 'false' } : { bar: 'baz' } + ) assert.deepStrictEqual(decodedData.metrics, { example: 1, moreExample: 2, @@ -735,7 +781,11 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) + assert.deepStrictEqual(trace[0].meta, { + bar: 'baz', + '_dd.span_links': encodedLink, + '_dd.sdk.otlp_export': 'false', + }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) @@ -757,7 +807,11 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) + assert.deepStrictEqual(trace[0].meta, { + bar: 'baz', + '_dd.span_links': encodedLink, + '_dd.sdk.otlp_export': 'false', + }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) diff --git a/packages/dd-trace/test/encode/0.5.spec.js b/packages/dd-trace/test/encode/0.5.spec.js index 075d7d9ede4..e2f940c0c5d 100644 --- a/packages/dd-trace/test/encode/0.5.spec.js +++ b/packages/dd-trace/test/encode/0.5.spec.js @@ -65,7 +65,10 @@ describe('encode 0.5', () => { assert.strictEqual(trace[0][6], BigInt(data[0].start)) assert.strictEqual(trace[0][7], BigInt(data[0].duration)) assert.strictEqual(trace[0][8], 0) - assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz') }) + assert.deepStrictEqual(trace[0][9], { + [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), + [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), + }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset }) @@ -100,6 +103,35 @@ describe('encode 0.5', () => { }) }) + describe('_dd.sdk.otlp_export', () => { + const makeTrace = (length) => Array.from({ length }, () => ({ ...data[0], meta: { bar: 'baz' } })) + const markers = (payload) => { + const [stringMap, traces] = msgpack.decode(payload, { useBigInt64: true }) + const key = stringMap.indexOf('_dd.sdk.otlp_export') + return traces.map(trace => trace.map(span => (key in span[9] ? stringMap[span[9][key]] : undefined))) + } + + it('should be written once per payload, on the first span of the first non-empty trace', () => { + encoder.encode([]) + encoder.encode(makeTrace(3)) + encoder.encode(makeTrace(2)) + + assert.deepStrictEqual(markers(encoder.makePayload()), [ + [], + ['false', undefined, undefined], + [undefined, undefined], + ]) + }) + + it('should be written again on the first span after a flush', () => { + encoder.encode(makeTrace(2)) + encoder.makePayload() + encoder.encode(makeTrace(2)) + + assert.deepStrictEqual(markers(encoder.makePayload()), [['false', undefined]]) + }) + }) + it('should encode span links', () => { const traceIdHigh = id('10') const traceId = id('1234abcd1234abcd') @@ -134,6 +166,7 @@ describe('encode 0.5', () => { assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), [stringMap.indexOf('_dd.span_links')]: stringMap.indexOf(encodedLink), + [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset @@ -167,6 +200,7 @@ describe('encode 0.5', () => { assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), [stringMap.indexOf('_dd.span_links')]: stringMap.indexOf(encodedLink), + [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset @@ -294,7 +328,10 @@ describe('encode 0.5', () => { assert.strictEqual(trace[0][6], BigInt(data[0].start)) assert.strictEqual(trace[0][7], BigInt(data[0].duration)) assert.strictEqual(trace[0][8], 0) - assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz') }) + assert.deepStrictEqual(trace[0][9], { + [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), + [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), + }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset // Everything works the same as without meta_struct, and nothing else is added diff --git a/packages/dd-trace/test/opentelemetry/traces.spec.js b/packages/dd-trace/test/opentelemetry/traces.spec.js index d448b10c03e..7371890145d 100644 --- a/packages/dd-trace/test/opentelemetry/traces.spec.js +++ b/packages/dd-trace/test/opentelemetry/traces.spec.js @@ -369,6 +369,24 @@ describe('OpenTelemetry Traces', () => { assert(keys.includes('keep.this'), 'Other meta keys should be present') }) + it('excludes the resource-scoped SDK adoption markers from span attributes', () => { + const transformer = new OtlpTraceTransformer({}) + const span = createMockSpan({ + meta: { + '_dd.sdk.otlp_export': 'false', + 'datadog.sdk.semantics': 'otel', + 'keep.this': 'value', + }, + }) + + const decoded = decodePayload(transformer.transformSpans([span])) + const keys = decoded.resourceSpans[0].scopeSpans[0].spans[0].attributes.map(a => a.key) + + assert(!keys.includes('_dd.sdk.otlp_export')) + assert(!keys.includes('datadog.sdk.semantics')) + assert(keys.includes('keep.this')) + }) + it('includes resource, service, type, and operation name as attributes', () => { const transformer = new OtlpTraceTransformer({}) const span = createMockSpan() @@ -879,6 +897,59 @@ describe('OpenTelemetry Traces', () => { exporter.export([createMockSpan()]) }) + + describe('SDK adoption markers', () => { + /** + * @param {object} extraEnv + * @param {object[]} spans + * @returns {{ resource: Record, spanKeys: string[] }} + */ + function exportAndCapture (extraEnv, spans = [createMockSpan()]) { + let captured + const verify = mockOtlpExport((decoded) => { + const { resource, scopeSpans } = decoded.resourceSpans[0] + captured = { + resource: Object.fromEntries(resource.attributes.map(attr => [attr.key, attr.value.stringValue])), + spanKeys: scopeSpans[0].spans.flatMap(span => span.attributes.map(attr => attr.key)), + } + }) + + buildExporter({ OTEL_TRACES_EXPORTER: 'otlp', ...extraEnv }).export(spans) + verify() + + return captured + } + + it('declares OTLP export and Datadog semantics on the resource by default', () => { + const { resource } = exportAndCapture({}) + + assert.strictEqual(resource['_dd.sdk.otlp_export'], 'true') + assert.strictEqual(resource['datadog.sdk.semantics'], 'datadog') + }) + + it('declares OTel semantics on the resource when DD_TRACE_OTEL_SEMANTICS_ENABLED is set', () => { + const { resource } = exportAndCapture({ DD_TRACE_OTEL_SEMANTICS_ENABLED: 'true' }) + + assert.strictEqual(resource['_dd.sdk.otlp_export'], 'true') + assert.strictEqual(resource['datadog.sdk.semantics'], 'otel') + }) + + it('does not let global or span tags with the same keys contradict the resource', () => { + const span = createMockSpan({ + meta: { '_dd.sdk.otlp_export': 'false', 'datadog.sdk.semantics': 'otel', 'keep.this': 'value' }, + }) + + const { resource, spanKeys } = exportAndCapture({ + DD_TAGS: '_dd.sdk.otlp_export:false,datadog.sdk.semantics:otel', + }, [span]) + + assert.strictEqual(resource['_dd.sdk.otlp_export'], 'true') + assert.strictEqual(resource['datadog.sdk.semantics'], 'datadog') + assert(!spanKeys.includes('_dd.sdk.otlp_export')) + assert(!spanKeys.includes('datadog.sdk.semantics')) + assert(spanKeys.includes('keep.this')) + }) + }) }) describe('Telemetry Metrics', () => { From c3f1b8f9fe50e24e46b0f967def73c066eb447c1 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 24 Sep 2026 15:41:23 -0400 Subject: [PATCH 2/6] refactor(tracing): set native OTLP export marker with process tags Move _dd.sdk.otlp_export=false out of the v0.4/v0.5 encoders and into span formatting, next to _dd.tags.process, so it is written on the first span of each chunk without copying meta or tracking payload state. It is skipped when traces are exported over OTLP, which the tracer already decides when it picks the exporter. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/dd-trace/src/encode/0.4.js | 35 +------- packages/dd-trace/src/encode/0.5.js | 10 +-- packages/dd-trace/src/opentracing/tracer.js | 9 +- packages/dd-trace/src/span_format.js | 11 ++- packages/dd-trace/src/span_processor.js | 5 +- packages/dd-trace/test/encode/0.4.spec.js | 84 ++++--------------- packages/dd-trace/test/encode/0.5.spec.js | 41 +-------- .../dd-trace/test/opentracing/tracer.spec.js | 6 +- packages/dd-trace/test/span_format.spec.js | 11 +++ packages/dd-trace/test/span_processor.spec.js | 26 ++++++ 10 files changed, 77 insertions(+), 161 deletions(-) diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index 16713a60f11..24c489294a2 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -1,7 +1,6 @@ 'use strict' const getConfig = require('../config') -const { SDK_OTLP_EXPORT_KEY } = require('../constants') const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack') const log = require('../log') const { normalizeSpan, eventTimeNano } = require('./tags-processors') @@ -225,17 +224,6 @@ function escapeJsonString (value) { return '"' + value + '"' } -/** - * Copies `meta` rather than mutating it so the formatted span stays untouched. Reaching a native - * encoder means the payload is not OTLP, so the export marker is always "false" here. - * - * @param {Record} [meta] - * @returns {Record} - */ -function withPayloadTags (meta) { - return { ...meta, [SDK_OTLP_EXPORT_KEY]: 'false' } -} - function lazyEncodedTraceBufferLogger (bytes, start, end) { const hex = bytes.buffer.subarray(start, end).toString('hex').match(/../g).join(' ') return `Adding encoded trace to buffer: ${hex}` @@ -327,7 +315,6 @@ class AgentEncoder { _encode (bytes, trace) { bytes.writeArrayPrefix(trace) - let payloadTagsPending = this._claimPayloadTags(trace) const formatSpan = this.#formatSpan const stringMap = this._stringMap // Snapshot the string buffer so we can detect a mid-encode resize and @@ -443,12 +430,7 @@ class AgentEncoder { } bytes.writeIntOrFloat(span.duration) - if (payloadTagsPending) { - payloadTagsPending = false - this.#encodeMetaEntries(bytes, KEY_META_PREFIX, withPayloadTags(span.meta)) - } else { - this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta) - } + this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta) this.#encodeMetaEntries(bytes, KEY_METRICS_PREFIX, span.metrics) if (span.span_events) { @@ -485,20 +467,7 @@ class AgentEncoder { } } - /** - * Claims the payload-scoped tags for this trace when it is the first non-empty trace since the - * last reset, so they are written exactly once per payload. - * - * @param {object[]} trace - */ - _claimPayloadTags (trace) { - if (!this._payloadTagsPending || trace.length === 0) return false - this._payloadTagsPending = false - return true - } - _reset () { - this._payloadTagsPending = true this._traceCount = 0 this._traceBytes.reset() this._stringCount = 0 @@ -1007,4 +976,4 @@ function memoizedLogDebug (key, message, value) { } } -module.exports = { AgentEncoder, stringifySpanEvents, withPayloadTags } +module.exports = { AgentEncoder, stringifySpanEvents } diff --git a/packages/dd-trace/src/encode/0.5.js b/packages/dd-trace/src/encode/0.5.js index 5c269f356a9..6271537d025 100644 --- a/packages/dd-trace/src/encode/0.5.js +++ b/packages/dd-trace/src/encode/0.5.js @@ -2,7 +2,7 @@ const { MAX_SIZE, OverflowError } = require('../msgpack') const { normalizeSpan } = require('./tags-processors') -const { AgentEncoder: BaseEncoder, stringifySpanEvents, withPayloadTags } = require('./0.4') +const { AgentEncoder: BaseEncoder, stringifySpanEvents } = require('./0.4') const ARRAY_OF_TWO = 0x92 const ARRAY_OF_TWELVE = 0x9C @@ -56,7 +56,6 @@ class AgentEncoder extends BaseEncoder { _encode (bytes, trace) { bytes.writeArrayPrefix(trace) - let payloadTagsPending = this._claimPayloadTags(trace) const stringMap = this._stringMap for (let span of trace) { @@ -85,12 +84,7 @@ class AgentEncoder extends BaseEncoder { bytes.writeIntOrFloat(span.start || 0) bytes.writeIntOrFloat(span.duration || 0) bytes.writeIntOrFloat(span.error) - if (payloadTagsPending) { - payloadTagsPending = false - this._encodeMap(bytes, withPayloadTags(span.meta)) - } else { - this._encodeMap(bytes, span.meta || {}) - } + this._encodeMap(bytes, span.meta || {}) this._encodeMap(bytes, span.metrics || {}) this._encodeString(bytes, span.type) } diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index d109da9bcac..f8e1a9a6bce 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -39,8 +39,9 @@ class DatadogTracer { // silently lose all test spans. The same applies to the Electron exporter: // spans must reach the Electron SDK's IPC bridge, not an OTLP endpoint, // even when OTEL_* vars are set for unrelated telemetry. - if (config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility && - config.tracing.DD_TRACE_EXPERIMENTAL_EXPORTER !== 'electron') { + const otlpTraceExport = config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility && + config.tracing.DD_TRACE_EXPERIMENTAL_EXPORTER !== 'electron' + if (otlpTraceExport) { const { createOtlpTraceExporter } = require('../opentelemetry/trace') this._exporter = createOtlpTraceExporter(config) } else { @@ -53,7 +54,9 @@ class DatadogTracer { const { createOtlpSpanStatsExporter } = require('../opentelemetry/metrics') otlpStatsExporter = createOtlpSpanStatsExporter(config) } - this._processor = new SpanProcessor(this._exporter, this._prioritySampler, config, otlpStatsExporter) + this._processor = new SpanProcessor( + this._exporter, this._prioritySampler, config, otlpStatsExporter, otlpTraceExport + ) this._url = this._exporter._url this._enableGetRumData = config.rum.DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED this._traceId128BitGenerationEnabled = config.traceId128BitGenerationEnabled diff --git a/packages/dd-trace/src/span_format.js b/packages/dd-trace/src/span_format.js index 998f0c60a30..817170d61a4 100644 --- a/packages/dd-trace/src/span_format.js +++ b/packages/dd-trace/src/span_format.js @@ -28,7 +28,7 @@ const PROCESS_ID = constants.PROCESS_ID const ERROR_MESSAGE = constants.ERROR_MESSAGE const ERROR_STACK = constants.ERROR_STACK const ERROR_TYPE = constants.ERROR_TYPE -const { IGNORE_OTEL_ERROR } = constants +const { IGNORE_OTEL_ERROR, SDK_OTLP_EXPORT_KEY } = constants /** * @typedef {object} FormattedSpan @@ -55,14 +55,14 @@ const { IGNORE_OTEL_ERROR } = constants * @property {Record} [attributes] */ -function format (span, isFirstSpanInChunk = false, tagForFirstSpanInChunk = false) { +function format (span, isFirstSpanInChunk = false, tagForFirstSpanInChunk = false, nativeExport = false) { const formatted = formatSpan(span) extractSpanLinks(formatted, span) extractSpanEvents(formatted, span) extractRootTags(formatted, span) if (isFirstSpanInChunk) { - extractChunkTags(formatted, span, tagForFirstSpanInChunk) + extractChunkTags(formatted, span, tagForFirstSpanInChunk, nativeExport) } extractTags(formatted, span) @@ -321,8 +321,11 @@ function extractRootTags (formattedSpan, span) { metrics[TOP_LEVEL_KEY] = 1 } -function extractChunkTags (formattedSpan, span, tagForFirstSpanInChunk) { +function extractChunkTags (formattedSpan, span, tagForFirstSpanInChunk, nativeExport) { const meta = formattedSpan.meta + if (nativeExport) { + meta[SDK_OTLP_EXPORT_KEY] = 'false' + } if (typeof tagForFirstSpanInChunk === 'string') { meta[TRACING_FIELD_NAME] = tagForFirstSpanInChunk.length > MAX_META_VALUE_LENGTH ? `${tagForFirstSpanInChunk.slice(0, MAX_META_VALUE_LENGTH)}...` diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index 8aa5a6e4fe2..7514aab5bc9 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -13,7 +13,7 @@ const startedSpans = new WeakSet() const finishedSpans = new WeakSet() class SpanProcessor { - constructor (exporter, prioritySampler, config, otlpStatsExporter) { + constructor (exporter, prioritySampler, config, otlpStatsExporter, otlpTraceExport = false) { this._exporter = exporter this._prioritySampler = prioritySampler this._config = config @@ -31,6 +31,7 @@ class SpanProcessor { this._processTags = config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED ? processTags.serialized : false + this._nativeExport = !otlpTraceExport } sample (span) { @@ -76,7 +77,7 @@ class SpanProcessor { if (span._duration === undefined) { active.push(span) } else if (!discard) { - const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags) + const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags, this._nativeExport) if (stampApmDisabled) { formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0 } diff --git a/packages/dd-trace/test/encode/0.4.spec.js b/packages/dd-trace/test/encode/0.4.spec.js index d1e73b2aa67..689e862c2aa 100644 --- a/packages/dd-trace/test/encode/0.4.spec.js +++ b/packages/dd-trace/test/encode/0.4.spec.js @@ -122,7 +122,7 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.sdk.otlp_export': 'false' }) + assert.deepStrictEqual(trace[0].meta, { bar: 'baz' }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) @@ -200,50 +200,6 @@ describe('encode', () => { assert.throws(() => encoder.encode(data), /something else/) }) - describe('_dd.sdk.otlp_export', () => { - const makeTrace = (length) => Array.from({ length }, () => ({ ...data[0], meta: { bar: 'baz' } })) - const markers = (payload) => msgpack.decode(payload, { useBigInt64: true }) - .map(trace => trace.map(span => span.meta['_dd.sdk.otlp_export'])) - - it('should be written once per payload, on the first span of the first non-empty trace', () => { - encoder.encode([]) - encoder.encode(makeTrace(3)) - encoder.encode(makeTrace(2)) - - assert.deepStrictEqual(markers(encoder.makePayload()), [ - [], - ['false', undefined, undefined], - [undefined, undefined], - ]) - }) - - it('should be written again on the first span after a flush', () => { - encoder.encode(makeTrace(2)) - encoder.makePayload() - encoder.encode(makeTrace(2)) - - assert.deepStrictEqual(markers(encoder.makePayload()), [['false', undefined]]) - }) - - it('should be written again on the first span after a reset', () => { - encoder.encode(makeTrace(1)) - encoder.reset() - encoder.encode(makeTrace(1)) - - assert.deepStrictEqual(markers(encoder.makePayload()), [['false']]) - }) - - it('should override a user tag with the same key without mutating the span', () => { - const trace = makeTrace(1) - trace[0].meta['_dd.sdk.otlp_export'] = 'true' - - encoder.encode(trace) - - assert.deepStrictEqual(markers(encoder.makePayload()), [['false']]) - assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.sdk.otlp_export': 'true' }) - }) - }) - it('should reset after making a payload', () => { encoder.encode(data) encoder.makePayload() @@ -424,7 +380,7 @@ describe('encode', () => { it('should relearn after 32 misses with fewer than 32 hits', () => { const retained = [] - for (let index = 0; index < 26; index++) { + for (let index = 0; index < 27; index++) { retained.push(`insufficient-retained-${index}`) } const write = sinon.spy(encoder._stringBytes, 'write') @@ -439,10 +395,10 @@ describe('encode', () => { encodePayload() cacheLowReusePayload(retained, 'second-miss') - data[0].meta = metaFromKeys([retained[25]]) + data[0].meta = metaFromKeys([retained[26]]) encodePayload() - assert.strictEqual(write.withArgs(retained[25]).callCount, 3) + assert.strictEqual(write.withArgs(retained[26]).callCount, 3) }) it('should relearn after a low-cardinality payload stops using the cache', () => { @@ -464,7 +420,7 @@ describe('encode', () => { it('should disable cross-payload caching after two low-reuse payloads', () => { const retained = [] - for (let index = 0; index < 26; index++) { + for (let index = 0; index < 27; index++) { retained.push(`disabled-retained-${index}`) } const value = 'stable-after-two-low-reuse-payloads' @@ -520,9 +476,8 @@ describe('encode', () => { }) it('should keep cross-payload caching after 32 misses with 32 hits', () => { - // 27 retained keys + the 4 span strings + the payload-scoped `_dd.sdk.otlp_export` key = 32 hits const retained = [] - for (let index = 0; index < 27; index++) { + for (let index = 0; index < 28; index++) { retained.push(`useful-retained-${index}`) } const write = sinon.spy(encoder._stringBytes, 'write') @@ -530,15 +485,15 @@ describe('encode', () => { cacheStringPayloads(retained, 8) cacheLowReusePayload(retained, 'useful-miss') - data[0].meta = metaFromKeys([retained[26]]) + data[0].meta = metaFromKeys([retained[27]]) encodePayload() - assert.strictEqual(write.withArgs(retained[26]).callCount, 2) + assert.strictEqual(write.withArgs(retained[27]).callCount, 2) }) it('should clear a low-reuse strike after a useful payload', () => { const retained = [] - for (let index = 0; index < 26; index++) { + for (let index = 0; index < 27; index++) { retained.push(`recovered-retained-${index}`) } const value = 'stable-after-low-reuse-recovery' @@ -617,7 +572,7 @@ describe('encode', () => { const buffer = encoder.makePayload() const [decodedPayload] = msgpack.decode(buffer, { useBigInt64: true }) - decodedPayload.forEach((decodedData, index) => { + decodedPayload.forEach(decodedData => { assertObjectContains(decodedData, { name: 'bigger name than expected', resource: 'test-r', @@ -627,10 +582,9 @@ describe('encode', () => { }) assert.strictEqual(decodedData.start, 123) assert.strictEqual(decodedData.duration, 456) - assert.deepStrictEqual( - decodedData.meta, - index === 0 ? { bar: 'baz', '_dd.sdk.otlp_export': 'false' } : { bar: 'baz' } - ) + assert.deepStrictEqual(decodedData.meta, { + bar: 'baz', + }) assert.deepStrictEqual(decodedData.metrics, { example: 1, moreExample: 2, @@ -781,11 +735,7 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { - bar: 'baz', - '_dd.span_links': encodedLink, - '_dd.sdk.otlp_export': 'false', - }) + assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) @@ -807,11 +757,7 @@ describe('encode', () => { assert.strictEqual(trace[0].start, 123) assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) - assert.deepStrictEqual(trace[0].meta, { - bar: 'baz', - '_dd.span_links': encodedLink, - '_dd.sdk.otlp_export': 'false', - }) + assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) }) diff --git a/packages/dd-trace/test/encode/0.5.spec.js b/packages/dd-trace/test/encode/0.5.spec.js index e2f940c0c5d..075d7d9ede4 100644 --- a/packages/dd-trace/test/encode/0.5.spec.js +++ b/packages/dd-trace/test/encode/0.5.spec.js @@ -65,10 +65,7 @@ describe('encode 0.5', () => { assert.strictEqual(trace[0][6], BigInt(data[0].start)) assert.strictEqual(trace[0][7], BigInt(data[0].duration)) assert.strictEqual(trace[0][8], 0) - assert.deepStrictEqual(trace[0][9], { - [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), - [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), - }) + assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz') }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset }) @@ -103,35 +100,6 @@ describe('encode 0.5', () => { }) }) - describe('_dd.sdk.otlp_export', () => { - const makeTrace = (length) => Array.from({ length }, () => ({ ...data[0], meta: { bar: 'baz' } })) - const markers = (payload) => { - const [stringMap, traces] = msgpack.decode(payload, { useBigInt64: true }) - const key = stringMap.indexOf('_dd.sdk.otlp_export') - return traces.map(trace => trace.map(span => (key in span[9] ? stringMap[span[9][key]] : undefined))) - } - - it('should be written once per payload, on the first span of the first non-empty trace', () => { - encoder.encode([]) - encoder.encode(makeTrace(3)) - encoder.encode(makeTrace(2)) - - assert.deepStrictEqual(markers(encoder.makePayload()), [ - [], - ['false', undefined, undefined], - [undefined, undefined], - ]) - }) - - it('should be written again on the first span after a flush', () => { - encoder.encode(makeTrace(2)) - encoder.makePayload() - encoder.encode(makeTrace(2)) - - assert.deepStrictEqual(markers(encoder.makePayload()), [['false', undefined]]) - }) - }) - it('should encode span links', () => { const traceIdHigh = id('10') const traceId = id('1234abcd1234abcd') @@ -166,7 +134,6 @@ describe('encode 0.5', () => { assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), [stringMap.indexOf('_dd.span_links')]: stringMap.indexOf(encodedLink), - [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset @@ -200,7 +167,6 @@ describe('encode 0.5', () => { assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), [stringMap.indexOf('_dd.span_links')]: stringMap.indexOf(encodedLink), - [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset @@ -328,10 +294,7 @@ describe('encode 0.5', () => { assert.strictEqual(trace[0][6], BigInt(data[0].start)) assert.strictEqual(trace[0][7], BigInt(data[0].duration)) assert.strictEqual(trace[0][8], 0) - assert.deepStrictEqual(trace[0][9], { - [stringMap.indexOf('bar')]: stringMap.indexOf('baz'), - [stringMap.indexOf('_dd.sdk.otlp_export')]: stringMap.indexOf('false'), - }) + assert.deepStrictEqual(trace[0][9], { [stringMap.indexOf('bar')]: stringMap.indexOf('baz') }) assert.deepStrictEqual(trace[0][10], { [stringMap.indexOf('example')]: 1 }) assert.strictEqual(stringMap[trace[0][11]], '') // unset // Everything works the same as without meta_struct, and nothing else is added diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index f0896d4b01d..cef75cc25e9 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -125,7 +125,7 @@ describe('Tracer', () => { sinon.assert.called(AgentExporter) sinon.assert.calledWith(AgentExporter, config, prioritySampler) - sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config) + sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config, undefined, false) }) it('should preserve the Electron exporter when the OTLP exporter is enabled', () => { @@ -135,7 +135,7 @@ describe('Tracer', () => { tracer = new Tracer(config) sinon.assert.calledWith(exporter, 'electron') - sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config) + sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config, undefined, false) }) it('should use the OTLP exporter with a non-Electron trace exporter', () => { @@ -145,7 +145,7 @@ describe('Tracer', () => { tracer = new Tracer(config) sinon.assert.calledWith(createOtlpTraceExporter, config) - sinon.assert.calledWith(SpanProcessor, otlpExporter, prioritySampler, config) + sinon.assert.calledWith(SpanProcessor, otlpExporter, prioritySampler, config, undefined, true) }) it('should allow to configure an alternative prioritySampler', () => { diff --git a/packages/dd-trace/test/span_format.spec.js b/packages/dd-trace/test/span_format.spec.js index a3285672d59..bc71b3a81b3 100644 --- a/packages/dd-trace/test/span_format.spec.js +++ b/packages/dd-trace/test/span_format.spec.js @@ -541,6 +541,17 @@ describe('spanFormat', () => { }) }) + it('should add the native export marker to the chunk root on native APM export', () => { + trace = spanFormat(span, true, false, true) + + assert.strictEqual(trace.meta['_dd.sdk.otlp_export'], 'false') + }) + + it('should not add the native export marker outside the chunk root or native APM export', () => { + assert.ok(!('_dd.sdk.otlp_export' in spanFormat(span, false, false, true).meta)) + assert.ok(!('_dd.sdk.otlp_export' in spanFormat(span, true, false, false).meta)) + }) + it('truncates overlong chunk tag keys and values to the agent limit', () => { const { MAX_META_KEY_LENGTH, MAX_META_VALUE_LENGTH } = require('../src/encode/tags-processors') const overlongChunkKey = `${'k'.repeat(MAX_META_KEY_LENGTH)}!` diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index 4e83ca142c9..27c52ad30bf 100644 --- a/packages/dd-trace/test/span_processor.spec.js +++ b/packages/dd-trace/test/span_processor.spec.js @@ -342,6 +342,32 @@ describe('SpanProcessor', () => { sinon.assert.calledWith(spanFormat.getCall(3), finishedSpan, false, processor._processTags) }) + it('should request the native export marker on the first span of each chunk', () => { + config.flushMinSpans = 2 + const processor = new SpanProcessor(exporter, prioritySampler, config) + trace.started = [activeSpan, finishedSpan, finishedSpan] + trace.finished = [finishedSpan, finishedSpan] + processor.process(finishedSpan) + + trace.started = [finishedSpan] + trace.finished = [finishedSpan] + processor.process(finishedSpan) + + sinon.assert.calledWithExactly(spanFormat.getCall(0), finishedSpan, true, false, true) + sinon.assert.calledWithExactly(spanFormat.getCall(1), finishedSpan, false, false, true) + sinon.assert.calledWithExactly(spanFormat.getCall(2), finishedSpan, true, false, true) + }) + + it('should not request the native export marker when traces are exported over OTLP', () => { + config.flushMinSpans = 1 + const processor = new SpanProcessor(exporter, prioritySampler, config, undefined, true) + trace.started = [finishedSpan] + trace.finished = [finishedSpan] + processor.process(finishedSpan) + + sinon.assert.calledWithExactly(spanFormat, finishedSpan, true, false, false) + }) + it('should add APM disabled marker to every span in a chunk when APM tracing is disabled', () => { config.apmTracingEnabled = false config.flushMinSpans = 2 From 294c33d894c9d7a1ea4ba3160794def4b5718637 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 24 Sep 2026 15:55:17 -0400 Subject: [PATCH 3/6] refactor(tracing): derive native marker gate from config, drop span filter Compute whether traces go out over OTLP in SpanProcessor from config, next to process tags, instead of widening its constructor. Stop stripping the adoption marker keys from OTLP span attributes, matching dd-trace-java; the resource values are still set after global tags so they cannot be overridden. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../opentelemetry/trace/otlp_transformer.js | 6 +--- packages/dd-trace/src/opentracing/tracer.js | 9 ++---- packages/dd-trace/src/span_processor.js | 6 ++-- .../test/opentelemetry/traces.spec.js | 31 ++----------------- .../dd-trace/test/opentracing/tracer.spec.js | 6 ++-- packages/dd-trace/test/span_processor.spec.js | 3 +- 6 files changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js index 94e0e147dab..baa45a98231 100644 --- a/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +++ b/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js @@ -5,7 +5,6 @@ const { getProtobufTypes } = require('../otlp/protobuf_loader') const { VERSION } = require('../../../../../version') const id = require('../../id') const { eventTimeNano } = require('../../encode/tags-processors') -const { SDK_OTLP_EXPORT_KEY, SDK_SEMANTICS_KEY } = require('../../constants') const { protoSpanKind } = getProtobufTypes() const SPAN_KIND_UNSPECIFIED = protoSpanKind.values.SPAN_KIND_UNSPECIFIED @@ -68,14 +67,11 @@ const SPAN_KIND_MAP = { const STATUS_CODE_UNSET = 0 const STATUS_CODE_ERROR = 2 -// DD meta keys that are mapped to dedicated OTLP span fields, or are tracer-owned resource -// attributes that a user tag must not contradict, and should not appear as span attributes +// DD meta keys that are mapped to dedicated OTLP span fields and should not appear as attributes const EXCLUDED_META_KEYS = new Set([ '_dd.span_links', 'span.kind', TRACE_ID_128, - SDK_OTLP_EXPORT_KEY, - SDK_SEMANTICS_KEY, ]) // DD-only error tags that should not appear as attributes when OTel trace semantics are enabled. diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index f8e1a9a6bce..d109da9bcac 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -39,9 +39,8 @@ class DatadogTracer { // silently lose all test spans. The same applies to the Electron exporter: // spans must reach the Electron SDK's IPC bridge, not an OTLP endpoint, // even when OTEL_* vars are set for unrelated telemetry. - const otlpTraceExport = config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility && - config.tracing.DD_TRACE_EXPERIMENTAL_EXPORTER !== 'electron' - if (otlpTraceExport) { + if (config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility && + config.tracing.DD_TRACE_EXPERIMENTAL_EXPORTER !== 'electron') { const { createOtlpTraceExporter } = require('../opentelemetry/trace') this._exporter = createOtlpTraceExporter(config) } else { @@ -54,9 +53,7 @@ class DatadogTracer { const { createOtlpSpanStatsExporter } = require('../opentelemetry/metrics') otlpStatsExporter = createOtlpSpanStatsExporter(config) } - this._processor = new SpanProcessor( - this._exporter, this._prioritySampler, config, otlpStatsExporter, otlpTraceExport - ) + this._processor = new SpanProcessor(this._exporter, this._prioritySampler, config, otlpStatsExporter) this._url = this._exporter._url this._enableGetRumData = config.rum.DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED this._traceId128BitGenerationEnabled = config.traceId128BitGenerationEnabled diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index 7514aab5bc9..ab400a9603c 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -13,7 +13,7 @@ const startedSpans = new WeakSet() const finishedSpans = new WeakSet() class SpanProcessor { - constructor (exporter, prioritySampler, config, otlpStatsExporter, otlpTraceExport = false) { + constructor (exporter, prioritySampler, config, otlpStatsExporter) { this._exporter = exporter this._prioritySampler = prioritySampler this._config = config @@ -31,7 +31,9 @@ class SpanProcessor { this._processTags = config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED ? processTags.serialized : false - this._nativeExport = !otlpTraceExport + // Mirrors the OTLP exporter selection in opentracing/tracer.js; OTLP exports carry the marker on the resource. + this._nativeExport = config.OTEL_TRACES_EXPORTER !== 'otlp' || Boolean(config.isCiVisibility) || + config.tracing?.DD_TRACE_EXPERIMENTAL_EXPORTER === 'electron' } sample (span) { diff --git a/packages/dd-trace/test/opentelemetry/traces.spec.js b/packages/dd-trace/test/opentelemetry/traces.spec.js index 7371890145d..580b379ea68 100644 --- a/packages/dd-trace/test/opentelemetry/traces.spec.js +++ b/packages/dd-trace/test/opentelemetry/traces.spec.js @@ -369,24 +369,6 @@ describe('OpenTelemetry Traces', () => { assert(keys.includes('keep.this'), 'Other meta keys should be present') }) - it('excludes the resource-scoped SDK adoption markers from span attributes', () => { - const transformer = new OtlpTraceTransformer({}) - const span = createMockSpan({ - meta: { - '_dd.sdk.otlp_export': 'false', - 'datadog.sdk.semantics': 'otel', - 'keep.this': 'value', - }, - }) - - const decoded = decodePayload(transformer.transformSpans([span])) - const keys = decoded.resourceSpans[0].scopeSpans[0].spans[0].attributes.map(a => a.key) - - assert(!keys.includes('_dd.sdk.otlp_export')) - assert(!keys.includes('datadog.sdk.semantics')) - assert(keys.includes('keep.this')) - }) - it('includes resource, service, type, and operation name as attributes', () => { const transformer = new OtlpTraceTransformer({}) const span = createMockSpan() @@ -934,20 +916,13 @@ describe('OpenTelemetry Traces', () => { assert.strictEqual(resource['datadog.sdk.semantics'], 'otel') }) - it('does not let global or span tags with the same keys contradict the resource', () => { - const span = createMockSpan({ - meta: { '_dd.sdk.otlp_export': 'false', 'datadog.sdk.semantics': 'otel', 'keep.this': 'value' }, - }) - - const { resource, spanKeys } = exportAndCapture({ + it('does not let global tags with the same keys override the resource', () => { + const { resource } = exportAndCapture({ DD_TAGS: '_dd.sdk.otlp_export:false,datadog.sdk.semantics:otel', - }, [span]) + }) assert.strictEqual(resource['_dd.sdk.otlp_export'], 'true') assert.strictEqual(resource['datadog.sdk.semantics'], 'datadog') - assert(!spanKeys.includes('_dd.sdk.otlp_export')) - assert(!spanKeys.includes('datadog.sdk.semantics')) - assert(spanKeys.includes('keep.this')) }) }) }) diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index cef75cc25e9..f0896d4b01d 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -125,7 +125,7 @@ describe('Tracer', () => { sinon.assert.called(AgentExporter) sinon.assert.calledWith(AgentExporter, config, prioritySampler) - sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config, undefined, false) + sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config) }) it('should preserve the Electron exporter when the OTLP exporter is enabled', () => { @@ -135,7 +135,7 @@ describe('Tracer', () => { tracer = new Tracer(config) sinon.assert.calledWith(exporter, 'electron') - sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config, undefined, false) + sinon.assert.calledWith(SpanProcessor, agentExporter, prioritySampler, config) }) it('should use the OTLP exporter with a non-Electron trace exporter', () => { @@ -145,7 +145,7 @@ describe('Tracer', () => { tracer = new Tracer(config) sinon.assert.calledWith(createOtlpTraceExporter, config) - sinon.assert.calledWith(SpanProcessor, otlpExporter, prioritySampler, config, undefined, true) + sinon.assert.calledWith(SpanProcessor, otlpExporter, prioritySampler, config) }) it('should allow to configure an alternative prioritySampler', () => { diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index 27c52ad30bf..b28361d0290 100644 --- a/packages/dd-trace/test/span_processor.spec.js +++ b/packages/dd-trace/test/span_processor.spec.js @@ -360,7 +360,8 @@ describe('SpanProcessor', () => { it('should not request the native export marker when traces are exported over OTLP', () => { config.flushMinSpans = 1 - const processor = new SpanProcessor(exporter, prioritySampler, config, undefined, true) + config.OTEL_TRACES_EXPORTER = 'otlp' + const processor = new SpanProcessor(exporter, prioritySampler, config) trace.started = [finishedSpan] trace.finished = [finishedSpan] processor.process(finishedSpan) From 3e9b4036450df4852020a5c67e26c117e745604e Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 24 Sep 2026 16:07:02 -0400 Subject: [PATCH 4/6] refactor(tracing): gate native marker on OTEL_TRACES_EXPORTER only The Test Optimization and Electron overrides send spans to intakes that do not feed the ingestion metric, so the extra conditions add nothing that intake uses. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/dd-trace/src/span_processor.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index ab400a9603c..fb01f8e2484 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -31,9 +31,7 @@ class SpanProcessor { this._processTags = config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED ? processTags.serialized : false - // Mirrors the OTLP exporter selection in opentracing/tracer.js; OTLP exports carry the marker on the resource. - this._nativeExport = config.OTEL_TRACES_EXPORTER !== 'otlp' || Boolean(config.isCiVisibility) || - config.tracing?.DD_TRACE_EXPERIMENTAL_EXPORTER === 'electron' + this._nativeExport = config.OTEL_TRACES_EXPORTER !== 'otlp' } sample (span) { From e7eb884d1138a253d84f9835ebf01784f789ee9a Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 24 Sep 2026 16:27:32 -0400 Subject: [PATCH 5/6] test(tracing): drop unused span capture from OTLP marker helper Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/dd-trace/test/opentelemetry/traces.spec.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/dd-trace/test/opentelemetry/traces.spec.js b/packages/dd-trace/test/opentelemetry/traces.spec.js index 580b379ea68..570a108f787 100644 --- a/packages/dd-trace/test/opentelemetry/traces.spec.js +++ b/packages/dd-trace/test/opentelemetry/traces.spec.js @@ -883,20 +883,18 @@ describe('OpenTelemetry Traces', () => { describe('SDK adoption markers', () => { /** * @param {object} extraEnv - * @param {object[]} spans - * @returns {{ resource: Record, spanKeys: string[] }} + * @returns {{ resource: Record }} */ - function exportAndCapture (extraEnv, spans = [createMockSpan()]) { + function exportAndCapture (extraEnv) { let captured const verify = mockOtlpExport((decoded) => { - const { resource, scopeSpans } = decoded.resourceSpans[0] + const { resource } = decoded.resourceSpans[0] captured = { resource: Object.fromEntries(resource.attributes.map(attr => [attr.key, attr.value.stringValue])), - spanKeys: scopeSpans[0].spans.flatMap(span => span.attributes.map(attr => attr.key)), } }) - buildExporter({ OTEL_TRACES_EXPORTER: 'otlp', ...extraEnv }).export(spans) + buildExporter({ OTEL_TRACES_EXPORTER: 'otlp', ...extraEnv }).export([createMockSpan()]) verify() return captured From 7e0f855d60c606c34db97c50a3e713b16ace2b54 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Mon, 28 Sep 2026 14:59:27 -0400 Subject: [PATCH 6/6] refactor(tracing): stamp native OTLP export marker in span processor Write `_dd.sdk.otlp_export: false` on the first span of each chunk in SpanProcessor#process, next to the `_dd.apm.enabled` stamp, instead of in span_format's extractChunkTags. Stamping after formatting means a span tag with the same key can't override the marker, and it removes the extra `nativeExport` parameter threaded through spanFormat. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/dd-trace/src/span_format.js | 11 ++--- packages/dd-trace/src/span_processor.js | 8 ++- packages/dd-trace/test/span_format.spec.js | 11 ----- packages/dd-trace/test/span_processor.spec.js | 49 ++++++++++++------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/packages/dd-trace/src/span_format.js b/packages/dd-trace/src/span_format.js index 817170d61a4..998f0c60a30 100644 --- a/packages/dd-trace/src/span_format.js +++ b/packages/dd-trace/src/span_format.js @@ -28,7 +28,7 @@ const PROCESS_ID = constants.PROCESS_ID const ERROR_MESSAGE = constants.ERROR_MESSAGE const ERROR_STACK = constants.ERROR_STACK const ERROR_TYPE = constants.ERROR_TYPE -const { IGNORE_OTEL_ERROR, SDK_OTLP_EXPORT_KEY } = constants +const { IGNORE_OTEL_ERROR } = constants /** * @typedef {object} FormattedSpan @@ -55,14 +55,14 @@ const { IGNORE_OTEL_ERROR, SDK_OTLP_EXPORT_KEY } = constants * @property {Record} [attributes] */ -function format (span, isFirstSpanInChunk = false, tagForFirstSpanInChunk = false, nativeExport = false) { +function format (span, isFirstSpanInChunk = false, tagForFirstSpanInChunk = false) { const formatted = formatSpan(span) extractSpanLinks(formatted, span) extractSpanEvents(formatted, span) extractRootTags(formatted, span) if (isFirstSpanInChunk) { - extractChunkTags(formatted, span, tagForFirstSpanInChunk, nativeExport) + extractChunkTags(formatted, span, tagForFirstSpanInChunk) } extractTags(formatted, span) @@ -321,11 +321,8 @@ function extractRootTags (formattedSpan, span) { metrics[TOP_LEVEL_KEY] = 1 } -function extractChunkTags (formattedSpan, span, tagForFirstSpanInChunk, nativeExport) { +function extractChunkTags (formattedSpan, span, tagForFirstSpanInChunk) { const meta = formattedSpan.meta - if (nativeExport) { - meta[SDK_OTLP_EXPORT_KEY] = 'false' - } if (typeof tagForFirstSpanInChunk === 'string') { meta[TRACING_FIELD_NAME] = tagForFirstSpanInChunk.length > MAX_META_VALUE_LENGTH ? `${tagForFirstSpanInChunk.slice(0, MAX_META_VALUE_LENGTH)}...` diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index fb01f8e2484..b202f3be89d 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -7,7 +7,7 @@ const SpanSampler = require('./span_sampler') const GitMetadataTagger = require('./git_metadata_tagger') const processTags = require('./process-tags') const { applyHttpOtelSemantics } = require('./plugins/util/http-otel-semantics') -const { APM_TRACING_ENABLED_KEY } = require('./constants') +const { APM_TRACING_ENABLED_KEY, SDK_OTLP_EXPORT_KEY } = require('./constants') const startedSpans = new WeakSet() const finishedSpans = new WeakSet() @@ -77,10 +77,14 @@ class SpanProcessor { if (span._duration === undefined) { active.push(span) } else if (!discard) { - const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags, this._nativeExport) + const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags) if (stampApmDisabled) { formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0 } + // Stamped after formatting so a span tag with the same key can't override it. + if (isFirstSpanInChunk && this._nativeExport) { + formattedSpan.meta[SDK_OTLP_EXPORT_KEY] = 'false' + } isFirstSpanInChunk = false // Span stats read Datadog HTTP tag names from the formatted span, so // record them before the OTel rename — an export-only transform. diff --git a/packages/dd-trace/test/span_format.spec.js b/packages/dd-trace/test/span_format.spec.js index bc71b3a81b3..a3285672d59 100644 --- a/packages/dd-trace/test/span_format.spec.js +++ b/packages/dd-trace/test/span_format.spec.js @@ -541,17 +541,6 @@ describe('spanFormat', () => { }) }) - it('should add the native export marker to the chunk root on native APM export', () => { - trace = spanFormat(span, true, false, true) - - assert.strictEqual(trace.meta['_dd.sdk.otlp_export'], 'false') - }) - - it('should not add the native export marker outside the chunk root or native APM export', () => { - assert.ok(!('_dd.sdk.otlp_export' in spanFormat(span, false, false, true).meta)) - assert.ok(!('_dd.sdk.otlp_export' in spanFormat(span, true, false, false).meta)) - }) - it('truncates overlong chunk tag keys and values to the agent limit', () => { const { MAX_META_KEY_LENGTH, MAX_META_VALUE_LENGTH } = require('../src/encode/tags-processors') const overlongChunkKey = `${'k'.repeat(MAX_META_KEY_LENGTH)}!` diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index b28361d0290..6c2e91c385b 100644 --- a/packages/dd-trace/test/span_processor.spec.js +++ b/packages/dd-trace/test/span_processor.spec.js @@ -9,7 +9,7 @@ const proxyquire = require('proxyquire') require('./setup/core') -const { APM_TRACING_ENABLED_KEY } = require('../src/constants') +const { APM_TRACING_ENABLED_KEY, SDK_OTLP_EXPORT_KEY } = require('../src/constants') const { AUTO_REJECT, USER_KEEP } = require('../../../ext/priority') describe('SpanProcessor', () => { @@ -68,7 +68,7 @@ describe('SpanProcessor', () => { }, appsec: {}, } - spanFormat = sinon.stub().returns({ formatted: true }) + spanFormat = sinon.stub().callsFake(() => ({ formatted: true, meta: {} })) sample = sinon.stub() SpanSampler = sinon.stub().returns({ @@ -189,9 +189,9 @@ describe('SpanProcessor', () => { processor.process(finishedSpan) sinon.assert.calledWith(exporter.export, [ - { formatted: true }, - { formatted: true }, - { formatted: true }, + { formatted: true, meta: { [SDK_OTLP_EXPORT_KEY]: 'false' } }, + { formatted: true, meta: {} }, + { formatted: true, meta: {} }, ]) assert.ok('started' in trace) @@ -247,7 +247,7 @@ describe('SpanProcessor', () => { processor.process(finishedSpan) - sinon.assert.calledWith(exporter.export, [{ formatted: true }]) + sinon.assert.calledWith(exporter.export, [{ formatted: true, meta: { [SDK_OTLP_EXPORT_KEY]: 'false' } }]) }) it('should configure span sampler correctly', () => { @@ -342,7 +342,7 @@ describe('SpanProcessor', () => { sinon.assert.calledWith(spanFormat.getCall(3), finishedSpan, false, processor._processTags) }) - it('should request the native export marker on the first span of each chunk', () => { + it('should add the native export marker to the first span of each chunk', () => { config.flushMinSpans = 2 const processor = new SpanProcessor(exporter, prioritySampler, config) trace.started = [activeSpan, finishedSpan, finishedSpan] @@ -353,12 +353,26 @@ describe('SpanProcessor', () => { trace.finished = [finishedSpan] processor.process(finishedSpan) - sinon.assert.calledWithExactly(spanFormat.getCall(0), finishedSpan, true, false, true) - sinon.assert.calledWithExactly(spanFormat.getCall(1), finishedSpan, false, false, true) - sinon.assert.calledWithExactly(spanFormat.getCall(2), finishedSpan, true, false, true) + const [firstChunk] = exporter.export.firstCall.args + const [secondChunk] = exporter.export.secondCall.args + assert.strictEqual(firstChunk[0].meta[SDK_OTLP_EXPORT_KEY], 'false') + assert.ok(!Object.hasOwn(firstChunk[1].meta, SDK_OTLP_EXPORT_KEY)) + assert.strictEqual(secondChunk[0].meta[SDK_OTLP_EXPORT_KEY], 'false') }) - it('should not request the native export marker when traces are exported over OTLP', () => { + it('should not let a span tag override the native export marker', () => { + config.flushMinSpans = 1 + const processor = new SpanProcessor(exporter, prioritySampler, config) + const formattedSpan = { meta: { [SDK_OTLP_EXPORT_KEY]: 'true' } } + spanFormat.returns(formattedSpan) + trace.started = [finishedSpan] + trace.finished = [finishedSpan] + processor.process(finishedSpan) + + assert.strictEqual(formattedSpan.meta[SDK_OTLP_EXPORT_KEY], 'false') + }) + + it('should not add the native export marker when traces are exported over OTLP', () => { config.flushMinSpans = 1 config.OTEL_TRACES_EXPORTER = 'otlp' const processor = new SpanProcessor(exporter, prioritySampler, config) @@ -366,15 +380,16 @@ describe('SpanProcessor', () => { trace.finished = [finishedSpan] processor.process(finishedSpan) - sinon.assert.calledWithExactly(spanFormat, finishedSpan, true, false, false) + const [chunk] = exporter.export.firstCall.args + assert.ok(!Object.hasOwn(chunk[0].meta, SDK_OTLP_EXPORT_KEY)) }) it('should add APM disabled marker to every span in a chunk when APM tracing is disabled', () => { config.apmTracingEnabled = false config.flushMinSpans = 2 const processor = new SpanProcessor(exporter, prioritySampler, config) - const firstFormatted = { metrics: {} } - const secondFormatted = { metrics: {} } + const firstFormatted = { meta: {}, metrics: {} } + const secondFormatted = { meta: {}, metrics: {} } spanFormat.onFirstCall().returns(firstFormatted) spanFormat.onSecondCall().returns(secondFormatted) trace.started = [activeSpan, finishedSpan, finishedSpan] @@ -393,8 +408,8 @@ describe('SpanProcessor', () => { // later in its own chunk. Both chunks must carry _dd.apm.enabled:0. config.apmTracingEnabled = false const processor = new SpanProcessor(exporter, prioritySampler, config) - const parentFormatted = { metrics: {} } - const childFormatted = { metrics: {} } + const parentFormatted = { meta: {}, metrics: {} } + const childFormatted = { meta: {}, metrics: {} } spanFormat.onFirstCall().returns(parentFormatted) spanFormat.onSecondCall().returns(childFormatted) @@ -420,7 +435,7 @@ describe('SpanProcessor', () => { it('should not add APM disabled marker when APM tracing is enabled', () => { config.apmTracingEnabled = true const processor = new SpanProcessor(exporter, prioritySampler, config) - const formattedSpan = { metrics: {} } + const formattedSpan = { meta: {}, metrics: {} } spanFormat.returns(formattedSpan) trace.started = [finishedSpan] trace.finished = [finishedSpan]