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/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/span_processor.js b/packages/dd-trace/src/span_processor.js index 8aa5a6e4fe2..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() @@ -31,6 +31,7 @@ class SpanProcessor { this._processTags = config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED ? processTags.serialized : false + this._nativeExport = config.OTEL_TRACES_EXPORTER !== 'otlp' } sample (span) { @@ -80,6 +81,10 @@ class SpanProcessor { 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/opentelemetry/traces.spec.js b/packages/dd-trace/test/opentelemetry/traces.spec.js index d448b10c03e..570a108f787 100644 --- a/packages/dd-trace/test/opentelemetry/traces.spec.js +++ b/packages/dd-trace/test/opentelemetry/traces.spec.js @@ -879,6 +879,50 @@ describe('OpenTelemetry Traces', () => { exporter.export([createMockSpan()]) }) + + describe('SDK adoption markers', () => { + /** + * @param {object} extraEnv + * @returns {{ resource: Record }} + */ + function exportAndCapture (extraEnv) { + let captured + const verify = mockOtlpExport((decoded) => { + const { resource } = decoded.resourceSpans[0] + captured = { + resource: Object.fromEntries(resource.attributes.map(attr => [attr.key, attr.value.stringValue])), + } + }) + + buildExporter({ OTEL_TRACES_EXPORTER: 'otlp', ...extraEnv }).export([createMockSpan()]) + 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 tags with the same keys override the resource', () => { + const { resource } = exportAndCapture({ + DD_TAGS: '_dd.sdk.otlp_export:false,datadog.sdk.semantics:otel', + }) + + assert.strictEqual(resource['_dd.sdk.otlp_export'], 'true') + assert.strictEqual(resource['datadog.sdk.semantics'], 'datadog') + }) + }) }) describe('Telemetry Metrics', () => { diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index 4e83ca142c9..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,12 +342,54 @@ describe('SpanProcessor', () => { sinon.assert.calledWith(spanFormat.getCall(3), finishedSpan, false, processor._processTags) }) + 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] + trace.finished = [finishedSpan, finishedSpan] + processor.process(finishedSpan) + + trace.started = [finishedSpan] + trace.finished = [finishedSpan] + processor.process(finishedSpan) + + 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 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) + trace.started = [finishedSpan] + trace.finished = [finishedSpan] + processor.process(finishedSpan) + + 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] @@ -366,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) @@ -393,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]