Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
const { getProtobufTypes } = require('../otlp/protobuf_loader')
const { AUTO_KEEP } = require('../../../../../ext/priority')
const { VERSION } = require('../../../../../version')
const { SAMPLING_PRIORITY_KEY } = require('../../constants')
const id = require('../../id')
const { eventTimeNano } = require('../../encode/tags-processors')

Expand Down Expand Up @@ -52,6 +54,7 @@ const TRACE_ID_128 = '_dd.p.tid'
* @property {number} start - Start time in nanoseconds since epoch
* @property {number} duration - Duration in nanoseconds
* @property {DDSpanEvent[]} [span_events] - Span events
* @property {string} [trace_state] - W3C tracestate for OTLP export
*/

// Map DD span.kind string values to OTLP SpanKind numeric values
Expand Down Expand Up @@ -161,11 +164,13 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
#transformSpan (span, traceIdHigh) {
const parentId = span.parent_id
const links = this.#extractLinks(span.meta?.['_dd.span_links'])
const samplingPriority = span.metrics?.[SAMPLING_PRIORITY_KEY]

return {
traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
spanId: this.#idToBytes(span.span_id, 8),
parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
traceState: span.trace_state,
name: span.resource,
kind: this.#mapSpanKind(span.meta?.['span.kind']),
startTimeUnixNano: span.start,
Expand All @@ -177,6 +182,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
links: links.length ? links : undefined,
droppedLinksCount: 0,
status: this.#mapStatus(span),
flags: typeof samplingPriority === 'number' ? (samplingPriority >= AUTO_KEEP ? 1 : 0) : undefined,
}
}

Expand Down
97 changes: 2 additions & 95 deletions packages/dd-trace/src/opentracing/propagation/text_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const telemetryMetrics = require('../../telemetry/metrics')
const { DD_MAJOR } = require('../../../../../version')

const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority')
const { formatTraceState, hasTraceTagReplacement } = require('./tracecontext')
const TraceState = require('./tracestate')

const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
Expand All @@ -66,19 +67,11 @@ const tagValueExpr = /^[\x20-\x2B\x2D-\x7E]*$/ // ASCII minus commas
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
const invalidHeaderValueCharExpr = /[^\t\x20-\x7E\x80-\xFF]/
const traceparentExpr = /^([a-f0-9]{2})-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})(-.*)?$/i
// Origin value in tracestate replaces '~', ',' and ';' with '_"
const tracestateOriginFilter = /[^\x20-\x2B\x2D-\x3A\x3C-\x7D]/g
// Tag keys in tracestate replace ' ', ',' and '=' with '_'
const tracestateTagKeyFilter = /[^\x21-\x2B\x2D-\x3C\x3E-\x7E]/g
// Tag values in tracestate replace ',', '~' and ';' with '_'
const tracestateTagValueFilter = /[^\x20-\x2B\x2D-\x3A\x3C-\x7D]/g
const invalidSegment = /^0+$/
const zeroTraceId = '0000000000000000'
const hex16 = /^[0-9A-Fa-f]{16}$/
const percentByte = /%([0-9A-Fa-f]{2})/g

let updateOtelTraceState

/**
* @typedef {object} B3Context
* @property {string} [flags]
Expand All @@ -87,18 +80,6 @@ let updateOtelTraceState
* @property {string} [traceId]
*/

/**
* @param {Array<string | undefined>} traceTagReplacements
* @param {string} key
* @returns {boolean}
*/
function hasTraceTagReplacement (traceTagReplacements, key) {
for (let index = 0; index < traceTagReplacements.length; index += 2) {
if (traceTagReplacements[index] === key) return true
}
return false
}

/**
* @param {string | undefined} traceId
* @param {string | undefined} spanId
Expand Down Expand Up @@ -605,82 +586,8 @@ class TextMapPropagator {
}

carrier ??= {}
const {
_sampling: { priority, mechanism },
_tracestate,
_trace: { origin },
} = spanContext
const ts = traceTagReplacements
? TraceState.fromString(_tracestate?.toString())
: _tracestate ?? new TraceState()

writeTraceparent(carrier, spanContext.toTraceparent())

updateOtelTraceState ??= require('../../otel-sampling').updateOtelTraceState
updateOtelTraceState(spanContext, ts)

ts.forVendor('dd', state => {
if (!spanContext._isRemote) {
// SpanContext was created by a ddtrace span.
// Last datadog span id should be set to the current span.
state.set('p', spanContext._spanId)
} else if (spanContext._trace.tags[tags.DD_PARENT_ID]) {
// Propagate the last Datadog span id set on the remote span.
state.set('p', spanContext._trace.tags[tags.DD_PARENT_ID])
}
state.set('s', priority)
if (mechanism) {
state.set('t.dm', `-${mechanism}`)
}

if (typeof origin === 'string') {
const originValue = origin
.replaceAll(tracestateOriginFilter, '_')
.replaceAll('=', '~')

state.set('o', originValue)
}

for (const key of Object.keys(spanContext._trace.tags)) {
if (traceTagReplacements && hasTraceTagReplacement(traceTagReplacements, key)) continue
const tagValueRaw = spanContext._trace.tags[key]
if (!tagValueRaw || !key.startsWith('_dd.p.')) continue

const tagKey = 't.' + key.slice(6)
.replaceAll(tracestateTagKeyFilter, '_')

const tagValue = tagValueRaw
.toString()
.replaceAll(tracestateTagValueFilter, '_')
.replaceAll('=', '~')

state.set(tagKey, tagValue)
}

if (traceTagReplacements) {
for (let index = 0; index < traceTagReplacements.length; index += 2) {
const key = traceTagReplacements[index]
if (!key.startsWith('_dd.p.')) continue

const tagKey = 't.' + key.slice(6)
.replaceAll(tracestateTagKeyFilter, '_')
const tagValueRaw = traceTagReplacements[index + 1]
if (!tagValueRaw) {
state.delete(tagKey)
continue
}

const tagValue = tagValueRaw
.toString()
.replaceAll(tracestateTagValueFilter, '_')
.replaceAll('=', '~')

state.set(tagKey, tagValue)
}
}
})

writeTracestate(carrier, ts.toString())
writeTracestate(carrier, formatTraceState(spanContext, traceTagReplacements))

return carrier
}
Expand Down
111 changes: 111 additions & 0 deletions packages/dd-trace/src/opentracing/propagation/tracecontext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict'

const tags = require('../../../../../ext/tags')
const TraceState = require('./tracestate')

// Origin value in tracestate replaces '~', ',' and ';' with '_'.
const tracestateOriginFilter = /[^\x20-\x2B\x2D-\x3A\x3C-\x7D]/g
// Tag keys in tracestate replace ' ', ',' and '=' with '_'.
const tracestateTagKeyFilter = /[^\x21-\x2B\x2D-\x3C\x3E-\x7E]/g
// Tag values in tracestate replace ',', '~' and ';' with '_'.
const tracestateTagValueFilter = /[^\x20-\x2B\x2D-\x3A\x3C-\x7D]/g

let updateOtelTraceState

/**
* @param {Array<string | undefined>} traceTagReplacements
* @param {string} key
* @returns {boolean}
*/
function hasTraceTagReplacement (traceTagReplacements, key) {
for (let index = 0; index < traceTagReplacements.length; index += 2) {
if (traceTagReplacements[index] === key) return true
}
return false
}

/**
* Builds the W3C tracestate for propagation or OTLP export from the live span context.
*
* @param {import('../span_context')} spanContext
* @param {Array<string | undefined>} [traceTagReplacements]
* @returns {string}
*/
function formatTraceState (spanContext, traceTagReplacements) {
const {
_sampling: { priority, mechanism },
_tracestate,
_trace: { origin },
} = spanContext
const traceState = traceTagReplacements
? TraceState.fromString(_tracestate?.toString())
: _tracestate ?? new TraceState()

updateOtelTraceState ??= require('../../otel-sampling').updateOtelTraceState
updateOtelTraceState(spanContext, traceState)

traceState.forVendor('dd', state => {
if (!spanContext._isRemote) {
state.set('p', spanContext._spanId)
} else if (spanContext._trace.tags[tags.DD_PARENT_ID]) {
state.set('p', spanContext._trace.tags[tags.DD_PARENT_ID])
}
state.set('s', priority)
if (mechanism) {
state.set('t.dm', `-${mechanism}`)
}

if (typeof origin === 'string') {
const originValue = origin
.replaceAll(tracestateOriginFilter, '_')
.replaceAll('=', '~')

state.set('o', originValue)
}

for (const key of Object.keys(spanContext._trace.tags)) {
if (traceTagReplacements && hasTraceTagReplacement(traceTagReplacements, key)) continue
const tagValueRaw = spanContext._trace.tags[key]
if (!tagValueRaw || !key.startsWith('_dd.p.')) continue

const tagKey = 't.' + key.slice(6)
.replaceAll(tracestateTagKeyFilter, '_')

const tagValue = tagValueRaw
.toString()
.replaceAll(tracestateTagValueFilter, '_')
.replaceAll('=', '~')

state.set(tagKey, tagValue)
}

if (traceTagReplacements) {
for (let index = 0; index < traceTagReplacements.length; index += 2) {
const key = traceTagReplacements[index]
if (!key.startsWith('_dd.p.')) continue

const tagKey = 't.' + key.slice(6)
.replaceAll(tracestateTagKeyFilter, '_')
const tagValueRaw = traceTagReplacements[index + 1]
if (!tagValueRaw) {
state.delete(tagKey)
continue
}

const tagValue = tagValueRaw
.toString()
.replaceAll(tracestateTagValueFilter, '_')
.replaceAll('=', '~')

state.set(tagKey, tagValue)
}
}
})

return traceState.toString()
}

module.exports = {
formatTraceState,
hasTraceTagReplacement,
}
13 changes: 10 additions & 3 deletions packages/dd-trace/src/opentracing/tracer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.experimental.exporter !== 'electron') {
const exportOtlpTraces = config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility &&
config.experimental.exporter !== 'electron'
if (exportOtlpTraces) {
const { createOtlpTraceExporter } = require('../opentelemetry/trace')
this._exporter = createOtlpTraceExporter(config)
} else {
Expand All @@ -53,7 +54,13 @@ 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,
exportOtlpTraces
)
this._url = this._exporter._url
this._enableGetRumData = config.experimental.enableGetRumData
this._traceId128BitGenerationEnabled = config.traceId128BitGenerationEnabled
Expand Down
28 changes: 26 additions & 2 deletions packages/dd-trace/src/span_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,40 @@ 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 { formatTraceState } = require('./opentracing/propagation/tracecontext')
const { APM_TRACING_ENABLED_KEY } = require('./constants')

const startedSpans = new WeakSet()
const finishedSpans = new WeakSet()

/**
* Adds first-class OTLP trace context to a DD-formatted span.
*
* @param {import('./opentracing/span')} span
* @param {boolean} isFirstSpanInChunk
* @param {string | false} processTagsValue
* @returns {import('./span_format').FormattedSpan}
*/
function formatOtlpSpan (span, isFirstSpanInChunk, processTagsValue) {
const formattedSpan = spanFormat(span, isFirstSpanInChunk, processTagsValue)
formattedSpan.trace_state = formatTraceState(span.context())
return formattedSpan
}

class SpanProcessor {
constructor (exporter, prioritySampler, config, otlpStatsExporter) {
/**
* @param {object} exporter
* @param {import('./priority_sampler')} prioritySampler
* @param {object} config
* @param {import('./opentelemetry/metrics/otlp_span_stats_exporter').OtlpStatsExporter} [otlpStatsExporter]
* @param {boolean} [exportOtlpTraces]
*/
constructor (exporter, prioritySampler, config, otlpStatsExporter, exportOtlpTraces) {
this._exporter = exporter
this._prioritySampler = prioritySampler
this._config = config
this._killAll = false
this._formatSpan = exportOtlpTraces ? formatOtlpSpan : spanFormat

if (config.stats?.DD_TRACE_STATS_COMPUTATION_ENABLED && !config.appsec?.standalone?.enabled) {
const { SpanStatsProcessor } = require('./span_stats')
Expand Down Expand Up @@ -56,12 +79,13 @@ class SpanProcessor {

let isFirstSpanInChunk = true
const stampApmDisabled = this._config.apmTracingEnabled === false
const formatSpan = this._formatSpan

for (const span of started) {
if (span._duration === undefined) {
active.push(span)
} else {
const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags)
const formattedSpan = formatSpan(span, isFirstSpanInChunk, this._processTags)
if (stampApmDisabled) {
formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0
}
Expand Down
15 changes: 15 additions & 0 deletions packages/dd-trace/test/opentelemetry/traces.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,21 @@ describe('OpenTelemetry Traces', () => {
assert.strictEqual(otlpSpan.parentSpanId.length, 16, 'parentSpanId must be 16 hex chars (8 bytes)')
})

it('exports W3C tracestate and the sampled flag as first-class OTLP fields', () => {
const transformer = new OtlpTraceTransformer({})
const traceState = 'dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668'
const span = createMockSpan({
trace_state: traceState,
metrics: { _sampling_priority_v1: 1 },
})

const decoded = decodePayload(transformer.transformSpans([span]))
const otlpSpan = decoded.resourceSpans[0].scopeSpans[0].spans[0]

assert.strictEqual(otlpSpan.traceState, traceState)
assert.strictEqual(otlpSpan.flags, 1)
})

it('maps span kind correctly', () => {
const transformer = new OtlpTraceTransformer({})

Expand Down
Loading
Loading