diff --git a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js index fbe77151d4c..3f65acdab0b 100644 --- a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js @@ -27,6 +27,7 @@ describe('EventBridge', () => { _traceFlags: { sampled: 1 }, + _baggageItems: {}, 'x-datadog-trace-id': traceId, 'x-datadog-parent-id': parentId, 'x-datadog-sampling-priority': '1', diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 1a5eeb61d03..1a2f990e1d7 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -462,6 +462,9 @@ class Config { this._setValue(defaults, 'appsec.stackTrace.maxDepth', 32) this._setValue(defaults, 'appsec.stackTrace.maxStackTraces', 2) this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs + this._setValue(defaults, 'baggageMaxBytes', 8192) + this._setValue(defaults, 'baggageMaxItems', 64) + this._setValue(defaults, 'ciVisibilityTestSessionName', '') this._setValue(defaults, 'clientIpEnabled', false) this._setValue(defaults, 'clientIpHeader', null) this._setValue(defaults, 'codeOriginForSpans.enabled', false) @@ -501,6 +504,7 @@ class Config { this._setValue(defaults, 'isManualApiEnabled', false) this._setValue(defaults, 'ciVisibilityTestSessionName', '') this._setValue(defaults, 'ciVisAgentlessLogSubmissionEnabled', false) + this._setValue(defaults, 'legacyBaggageEnabled', true) this._setValue(defaults, 'logInjection', false) this._setValue(defaults, 'lookup', undefined) this._setValue(defaults, 'memcachedCommandEnabled', false) @@ -545,8 +549,8 @@ class Config { this._setValue(defaults, 'traceId128BitGenerationEnabled', true) this._setValue(defaults, 'traceId128BitLoggingEnabled', false) this._setValue(defaults, 'tracePropagationExtractFirst', false) - this._setValue(defaults, 'tracePropagationStyle.inject', ['datadog', 'tracecontext']) - this._setValue(defaults, 'tracePropagationStyle.extract', ['datadog', 'tracecontext']) + this._setValue(defaults, 'tracePropagationStyle.inject', ['datadog', 'tracecontext', 'baggage']) + this._setValue(defaults, 'tracePropagationStyle.extract', ['datadog', 'tracecontext', 'baggage']) this._setValue(defaults, 'tracePropagationStyle.otelPropagators', false) this._setValue(defaults, 'tracing', true) this._setValue(defaults, 'url', undefined) @@ -626,6 +630,8 @@ class Config { DD_TRACE_AGENT_HOSTNAME, DD_TRACE_AGENT_PORT, DD_TRACE_AGENT_PROTOCOL_VERSION, + DD_TRACE_BAGGAGE_MAX_BYTES, + DD_TRACE_BAGGAGE_MAX_ITEMS, DD_TRACE_CLIENT_IP_ENABLED, DD_TRACE_CLIENT_IP_HEADER, DD_TRACE_ENABLED, @@ -635,6 +641,7 @@ class Config { DD_TRACE_GIT_METADATA_ENABLED, DD_TRACE_GLOBAL_TAGS, DD_TRACE_HEADER_TAGS, + DD_TRACE_LEGACY_BAGGAGE_ENABLED, DD_TRACE_MEMCACHED_COMMAND_ENABLED, DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP, DD_TRACE_PARTIAL_FLUSH_MIN_SPANS, @@ -706,6 +713,8 @@ class Config { this._envUnprocessed['appsec.stackTrace.maxStackTraces'] = DD_APPSEC_MAX_STACK_TRACES this._setValue(env, 'appsec.wafTimeout', maybeInt(DD_APPSEC_WAF_TIMEOUT)) this._envUnprocessed['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT + this._setValue(env, 'baggageMaxBytes', DD_TRACE_BAGGAGE_MAX_BYTES) + this._setValue(env, 'baggageMaxItems', DD_TRACE_BAGGAGE_MAX_ITEMS) this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED) this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER) this._setBoolean(env, 'codeOriginForSpans.enabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED) @@ -744,6 +753,7 @@ class Config { this._setArray(env, 'injectionEnabled', DD_INJECTION_ENABLED) this._setBoolean(env, 'isAzureFunction', getIsAzureFunction()) this._setBoolean(env, 'isGCPFunction', getIsGCPFunction()) + this._setBoolean(env, 'legacyBaggageEnabled', DD_TRACE_LEGACY_BAGGAGE_ENABLED) this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION) // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent this._setBoolean(env, 'memcachedCommandEnabled', DD_TRACE_MEMCACHED_COMMAND_ENABLED) @@ -877,6 +887,8 @@ class Config { this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled) this._setString(opts, 'clientIpHeader', options.clientIpHeader) + this._setValue(opts, 'baggageMaxBytes', options.baggageMaxBytes) + this._setValue(opts, 'baggageMaxItems', options.baggageMaxItems) this._setBoolean(opts, 'codeOriginForSpans.enabled', options.codeOriginForSpans?.enabled) this._setString(opts, 'dbmPropagationMode', options.dbmPropagationMode) if (options.dogstatsd) { @@ -914,6 +926,7 @@ class Config { } this._setString(opts, 'iast.telemetryVerbosity', options.iast && options.iast.telemetryVerbosity) this._setBoolean(opts, 'isCiVisibility', options.isCiVisibility) + this._setBoolean(opts, 'legacyBaggageEnabled', options.legacyBaggageEnabled) this._setBoolean(opts, 'logInjection', options.logInjection) this._setString(opts, 'lookup', options.lookup) this._setBoolean(opts, 'openAiLogsEnabled', options.openAiLogsEnabled) diff --git a/packages/dd-trace/src/noop/span.js b/packages/dd-trace/src/noop/span.js index bee3ce11702..0bdbf96ef66 100644 --- a/packages/dd-trace/src/noop/span.js +++ b/packages/dd-trace/src/noop/span.js @@ -16,6 +16,9 @@ class NoopSpan { setOperationName (name) { return this } setBaggageItem (key, value) { return this } getBaggageItem (key) {} + getAllBaggageItems () {} + removeBaggageItem (key) { return this } + removeAllBaggageItems () { return this } setTag (key, value) { return this } addTags (keyValueMap) { return this } addLink (link) { return this } diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index d2c216c138e..1028e8609a6 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -236,6 +236,29 @@ class Span { return this } + setBaggageItem (key, value) { + this._ddSpan._spanContext._baggageItems[key] = value + return this + } + + getBaggageItem (key) { + return this._ddSpan._spanContext._baggageItems[key] + } + + getAllBaggageItems () { + return JSON.stringify(this._ddSpan._spanContext._baggageItems) + } + + removeBaggageItem (key) { + delete this._ddSpan._spanContext._baggageItems[key] + return this + } + + removeAllBaggageItems () { + this._ddSpan._spanContext._baggageItems = {} + return this + } + end (timeInput) { if (this.ended) { api.diag.error('You can only call end() on a span once.') diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index 1346f85de72..6399fe99a4b 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -107,10 +107,28 @@ class TextMapPropagator { } } + _encodeOtelBaggageKey (key) { + let encoded = encodeURIComponent(key) + encoded = encoded.replaceAll('(', '%28') + encoded = encoded.replaceAll(')', '%29') + return encoded + } + _injectBaggageItems (spanContext, carrier) { - spanContext._baggageItems && Object.keys(spanContext._baggageItems).forEach(key => { - carrier[baggagePrefix + key] = String(spanContext._baggageItems[key]) - }) + if (this._config.legacyBaggageEnabled) { + spanContext._baggageItems && Object.keys(spanContext._baggageItems).forEach(key => { + carrier[baggagePrefix + key] = String(spanContext._baggageItems[key]) + }) + } + if (this._hasPropagationStyle('inject', 'baggage')) { + if (Object.keys(spanContext._baggageItems).length > this._config.baggageMaxItems) return + const baggage = Object.entries(spanContext._baggageItems) + .map(([key, value]) => + `${this._encodeOtelBaggageKey(String(key).trim())}=${encodeURIComponent(String(value).trim())}`).join(',') + const buf = Buffer.from(baggage) + if (buf.length > this._config.baggageMaxBytes) return + if (baggage) carrier.baggage = baggage + } } _injectTags (spanContext, carrier) { @@ -299,6 +317,11 @@ class TextMapPropagator { default: log.warn(`Unknown propagation style: ${extractor}`) } + + if (this._config.tracePropagationStyle.extract.includes('baggage') && carrier.baggage) { + spanContext = spanContext || new DatadogSpanContext() + this._extractBaggageItems(carrier, spanContext) + } } return spanContext || this._extractSqsdContext(carrier) @@ -310,7 +333,7 @@ class TextMapPropagator { if (!spanContext) return spanContext this._extractOrigin(carrier, spanContext) - this._extractBaggageItems(carrier, spanContext) + this._extractLegacyBaggageItems(carrier, spanContext) this._extractSamplingPriority(carrier, spanContext) this._extractTags(carrier, spanContext) @@ -444,7 +467,7 @@ class TextMapPropagator { } }) - this._extractBaggageItems(carrier, spanContext) + this._extractLegacyBaggageItems(carrier, spanContext) return spanContext } return null @@ -528,14 +551,43 @@ class TextMapPropagator { } } - _extractBaggageItems (carrier, spanContext) { - Object.keys(carrier).forEach(key => { - const match = key.match(baggageExpr) + _decodeOtelBaggageKey (key) { + let decoded = decodeURIComponent(key) + decoded = decoded.replaceAll('%28', '(') + decoded = decoded.replaceAll('%29', ')') + return decoded + } + + _extractLegacyBaggageItems (carrier, spanContext) { + if (this._config.legacyBaggageEnabled) { + Object.keys(carrier).forEach(key => { + const match = key.match(baggageExpr) - if (match) { - spanContext._baggageItems[match[1]] = carrier[key] + if (match) { + spanContext._baggageItems[match[1]] = carrier[key] + } + }) + } + } + + _extractBaggageItems (carrier, spanContext) { + const baggages = carrier.baggage.split(',') + for (const keyValue of baggages) { + if (!keyValue.includes('=')) { + spanContext._baggageItems = {} + return } - }) + let [key, value] = keyValue.split('=') + key = this._decodeOtelBaggageKey(key.trim()) + value = decodeURIComponent(value.trim()) + if (!key || !value) { + spanContext._baggageItems = {} + return + } + // the current code assumes precedence of ot-baggage- (legacy opentracing baggage) over baggage + if (key in spanContext._baggageItems) return + spanContext._baggageItems[key] = value + } } _extractSamplingPriority (carrier, spanContext) { diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 723597ff043..5a50166aa49 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -145,6 +145,18 @@ class DatadogSpan { return this._spanContext._baggageItems[key] } + getAllBaggageItems () { + return JSON.stringify(this._spanContext._baggageItems) + } + + removeBaggageItem (key) { + delete this._spanContext._baggageItems[key] + } + + removeAllBaggageItems () { + this._spanContext._baggageItems = {} + } + setTag (key, value) { this._addTags({ [key]: value }) return this diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 207c97080bb..24d77ffa131 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -36,6 +36,7 @@ class DatadogSpanContext { ? this._trace.tags[TRACE_ID_128] + this._traceId.toString(16).padStart(16, '0') : this._traceId.toString(16).padStart(32, '0') } + // if (!this._traceId) return '' return this._traceId.toString(10) } @@ -43,6 +44,7 @@ class DatadogSpanContext { if (get128bitId) { return this._spanId.toString(16).padStart(16, '0') } + // if (!this._spanId) return '' return this._spanId.toString(10) } diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 75a3e51c685..797827d4a56 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -229,8 +229,8 @@ describe('Config', () => { expect(config).to.have.property('spanRemoveIntegrationFromService', false) expect(config).to.have.property('instrumentation_config_id', undefined) expect(config).to.have.deep.property('serviceMapping', {}) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['datadog', 'tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['datadog', 'tracecontext']) + expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['datadog', 'tracecontext', 'baggage']) + expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['datadog', 'tracecontext', 'baggage']) expect(config).to.have.nested.property('experimental.runtimeId', false) expect(config).to.have.nested.property('experimental.exporter', undefined) expect(config).to.have.nested.property('experimental.enableGetRumData', false) diff --git a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js index 58ee69047ba..5f674ae6a45 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -46,7 +46,8 @@ describe('TextMapPropagator', () => { textMap = { 'x-datadog-trace-id': '123', 'x-datadog-parent-id': '456', - 'ot-baggage-foo': 'bar' + 'ot-baggage-foo': 'bar', + baggage: 'foo=bar' } baggageItems = {} }) @@ -67,18 +68,18 @@ describe('TextMapPropagator', () => { expect(carrier).to.have.property('x-datadog-trace-id', '123') expect(carrier).to.have.property('x-datadog-parent-id', '456') expect(carrier).to.have.property('ot-baggage-foo', 'bar') + expect(carrier).to.have.property('baggage', 'foo=bar') }) it('should handle non-string values', () => { const carrier = {} - const spanContext = createContext({ - baggageItems: { - number: 1.23, - bool: true, - array: ['foo', 'bar'], - object: {} - } - }) + const baggageItems = { + number: 1.23, + bool: true, + array: ['foo', 'bar'], + object: {} + } + const spanContext = createContext({ baggageItems }) propagator.inject(spanContext, carrier) @@ -86,6 +87,41 @@ describe('TextMapPropagator', () => { expect(carrier['ot-baggage-bool']).to.equal('true') expect(carrier['ot-baggage-array']).to.equal('foo,bar') expect(carrier['ot-baggage-object']).to.equal('[object Object]') + expect(carrier.baggage).to.be.equal('number=1.23,bool=true,array=foo%2Cbar,object=%5Bobject%20Object%5D') + }) + + it('should handle special characters in baggage', () => { + const carrier = {} + const baggageItems = { + '",;\\()/:<=>?@[]{}': '",;\\' + } + const spanContext = createContext({ baggageItems }) + + propagator.inject(spanContext, carrier) + expect(carrier.baggage).to.be.equal('%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C') + }) + + it('should not inject baggage when it contains too many key-value pairs', () => { + const carrier = {} + const baggageItems = {} + for (let i = 0; i < config.baggageMaxItems + 1; i++) { + baggageItems[`key-${i}`] = i + } + const spanContext = createContext({ baggageItems }) + + propagator.inject(spanContext, carrier) + expect(carrier.baggage).to.be.undefined + }) + + it('should not inject baggage when it contains too many bytes', () => { + const carrier = {} + const baggageItems = { + foo: Buffer.alloc(config.baggageMaxBytes).toString() + } + const spanContext = createContext({ baggageItems }) + + propagator.inject(spanContext, carrier) + expect(carrier.baggage).to.be.undefined }) it('should inject an existing sampling priority', () => { @@ -353,9 +389,57 @@ describe('TextMapPropagator', () => { expect(spanContext.toTraceId()).to.equal(carrier['x-datadog-trace-id']) expect(spanContext.toSpanId()).to.equal(carrier['x-datadog-parent-id']) expect(spanContext._baggageItems.foo).to.equal(carrier['ot-baggage-foo']) + expect(spanContext._baggageItems).to.deep.equal({ foo: 'bar' }) expect(spanContext._isRemote).to.equal(true) }) + it('should extract otel baggage items with special characters', () => { + process.env.DD_TRACE_BAGGAGE_ENABLED = true + config = new Config() + propagator = new TextMapPropagator(config) + const carrier = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456', + baggage: '%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C' + } + const spanContext = propagator.extract(carrier) + expect(spanContext._baggageItems).to.deep.equal({ '",;\\()/:<=>?@[]{}': '",;\\' }) + }) + + it('should not extract baggage when the header is malformed', () => { + const carrierA = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456', + baggage: 'no-equal-sign,foo=gets-dropped-because-previous-pair-is-malformed' + } + const spanContextA = propagator.extract(carrierA) + expect(spanContextA._baggageItems).to.deep.equal({}) + + const carrierB = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456', + baggage: 'foo=gets-dropped-because-subsequent-pair-is-malformed,=' + } + const spanContextB = propagator.extract(carrierB) + expect(spanContextB._baggageItems).to.deep.equal({}) + + const carrierC = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456', + baggage: '=no-key' + } + const spanContextC = propagator.extract(carrierC) + expect(spanContextC._baggageItems).to.deep.equal({}) + + const carrierD = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456', + baggage: 'no-value=' + } + const spanContextD = propagator.extract(carrierD) + expect(spanContextD._baggageItems).to.deep.equal({}) + }) + it('should convert signed IDs to unsigned', () => { textMap['x-datadog-trace-id'] = '-123' textMap['x-datadog-parent-id'] = '-456' diff --git a/packages/dd-trace/test/opentracing/span.spec.js b/packages/dd-trace/test/opentracing/span.spec.js index dbb248eb920..87d22114aa1 100644 --- a/packages/dd-trace/test/opentracing/span.spec.js +++ b/packages/dd-trace/test/opentracing/span.spec.js @@ -346,6 +346,40 @@ describe('Span', () => { }) }) + describe('getAllBaggageItems', () => { + it('should get all baggage items', () => { + span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' }) + expect(span.getAllBaggageItems()).to.equal(JSON.stringify({})) + + span._spanContext._baggageItems.foo = 'bar' + span._spanContext._baggageItems.raccoon = 'cute' + expect(span.getAllBaggageItems()).to.equal(JSON.stringify({ + foo: 'bar', + raccoon: 'cute' + })) + }) + }) + + describe('removeBaggageItem', () => { + it('should remove a baggage item', () => { + span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' }) + span._spanContext._baggageItems.foo = 'bar' + expect(span.getBaggageItem('foo')).to.equal('bar') + span.removeBaggageItem('foo') + expect(span.getBaggageItem('foo')).to.be.undefined + }) + }) + + describe('removeAllBaggageItems', () => { + it('should remove all baggage items', () => { + span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' }) + span._spanContext._baggageItems.foo = 'bar' + span._spanContext._baggageItems.raccoon = 'cute' + span.removeAllBaggageItems() + expect(span._spanContext._baggageItems).to.deep.equal({}) + }) + }) + describe('setTag', () => { it('should set a tag', () => { span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })