Skip to content

Commit d064e06

Browse files
authored
refactor(otel): extract bridge helpers into span-helpers.js (#8220)
Pulls the OTel-to-Datadog translation logic out of `span.js` and `tracer.js` into a single `span-helpers.js` module exposing: `normalizeLinkContext`, `setOtelAttribute`, `setOtelAttributes`, `addOtelLink`, `recordException`, `setStatus`. No behavior change. Consolidating these small helpers in one place keeps the OTel-bridge translation layer easy to reason about and lets future bridge objects reuse the same logic without duplication.
1 parent 658450e commit d064e06

3 files changed

Lines changed: 185 additions & 78 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
'use strict'
2+
3+
const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE, IGNORE_OTEL_ERROR } = require('../constants')
4+
const DatadogSpanContext = require('../opentracing/span_context')
5+
const TraceState = require('../opentracing/propagation/tracestate')
6+
7+
const id = require('../id')
8+
9+
/**
10+
* @typedef {{ toTraceId: (get128?: boolean) => string, toSpanId: (get128?: boolean) => string }} DatadogContextLike
11+
* @typedef {{ _ddContext: import('../opentracing/span_context') }} OtelBridgeSpanContextLike
12+
* @typedef {{
13+
* traceId: string,
14+
* spanId: string,
15+
* traceFlags?: number,
16+
* traceState?: { serialize: () => string }
17+
* }} OtelSpanContextLike
18+
* @typedef {DatadogContextLike | OtelBridgeSpanContextLike | OtelSpanContextLike} LinkContextLike
19+
* @typedef {{ context: LinkContextLike, attributes?: Record<string, unknown> }} OtelLink
20+
* @typedef {{
21+
* name?: string,
22+
* message?: string,
23+
* stack?: string,
24+
* type?: string,
25+
* escaped?: unknown
26+
* }} ExceptionLike
27+
* @typedef {{
28+
* addEvent: (name: string, attributes: Record<string, unknown>, timeInput?: unknown) => unknown
29+
* }} EventTarget
30+
*/
31+
32+
/**
33+
* Normalize any Datadog/OTel span-context shape to a `DatadogSpanContext`.
34+
*
35+
* @param {LinkContextLike | undefined | null} context
36+
* @returns {import('../opentracing/span_context') | undefined}
37+
*/
38+
function normalizeLinkContext (context) {
39+
if (!context) return
40+
41+
const bridgeCtx = /** @type {OtelBridgeSpanContextLike} */ (context)
42+
if (bridgeCtx._ddContext) return bridgeCtx._ddContext
43+
44+
const ddCtx = /** @type {DatadogContextLike} */ (context)
45+
if (typeof ddCtx.toTraceId === 'function' && typeof ddCtx.toSpanId === 'function') {
46+
return /** @type {import('../opentracing/span_context')} */ (/** @type {unknown} */ (context))
47+
}
48+
49+
const otelCtx = /** @type {OtelSpanContextLike} */ (context)
50+
if (typeof otelCtx.traceId !== 'string' || typeof otelCtx.spanId !== 'string') return
51+
52+
let sampling
53+
if (typeof otelCtx.traceFlags === 'number') {
54+
sampling = { priority: otelCtx.traceFlags & 1 }
55+
}
56+
57+
let tracestate
58+
if (otelCtx.traceState?.serialize) {
59+
tracestate = TraceState.fromString(otelCtx.traceState.serialize())
60+
}
61+
62+
return new DatadogSpanContext({
63+
traceId: id(otelCtx.traceId, 16),
64+
spanId: id(otelCtx.spanId, 16),
65+
sampling,
66+
tracestate,
67+
})
68+
}
69+
70+
/**
71+
* @param {import('../opentracing/span')} ddSpan
72+
* @param {string} key
73+
* @param {unknown} value
74+
* @returns {void}
75+
*/
76+
function setOtelAttribute (ddSpan, key, value) {
77+
if (key === 'http.response.status_code') {
78+
ddSpan.setTag('http.status_code', String(value))
79+
}
80+
81+
ddSpan.setTag(key, value)
82+
}
83+
84+
/**
85+
* @param {import('../opentracing/span')} ddSpan
86+
* @param {Record<string, unknown>} attributes
87+
* @returns {void}
88+
*/
89+
function setOtelAttributes (ddSpan, attributes) {
90+
if ('http.response.status_code' in attributes) {
91+
attributes['http.status_code'] = String(attributes['http.response.status_code'])
92+
}
93+
94+
ddSpan.addTags(attributes)
95+
}
96+
97+
/**
98+
* Accepts both `{ context, attributes }` and the deprecated `(context, attrs)` form.
99+
*
100+
* @param {import('../opentracing/span')} ddSpan
101+
* @param {LinkContextLike | OtelLink} link
102+
* @param {Record<string, unknown>} [attrs]
103+
* @returns {void}
104+
*/
105+
function addOtelLink (ddSpan, link, attrs) {
106+
// TODO: Drop the (context, attrs) form in v6.0.0.
107+
const linkObj = link && typeof link === 'object' && 'context' in link
108+
? /** @type {OtelLink} */ (link)
109+
: { context: /** @type {LinkContextLike} */ (link), attributes: attrs ?? {} }
110+
111+
const ddSpanContext = normalizeLinkContext(linkObj.context)
112+
if (!ddSpanContext) return
113+
114+
ddSpan.addLink({ context: ddSpanContext, attributes: linkObj.attributes })
115+
}
116+
117+
/**
118+
* @param {import('../opentracing/span')} ddSpan
119+
* @param {EventTarget} eventTarget
120+
* @param {ExceptionLike} exception
121+
* @param {unknown} [timeInput]
122+
* @returns {void}
123+
*/
124+
function recordException (ddSpan, eventTarget, exception, timeInput) {
125+
ddSpan.addTags({
126+
[ERROR_TYPE]: exception.name,
127+
[ERROR_MESSAGE]: exception.message,
128+
[ERROR_STACK]: exception.stack,
129+
[IGNORE_OTEL_ERROR]: ddSpan.context()._tags[IGNORE_OTEL_ERROR] ?? true,
130+
})
131+
132+
/** @type {Record<string, unknown>} */
133+
const attributes = {}
134+
if (exception.message) attributes['exception.message'] = exception.message
135+
if (exception.type) attributes['exception.type'] = exception.type
136+
if (exception.escaped) attributes['exception.escaped'] = exception.escaped
137+
if (exception.stack) attributes['exception.stacktrace'] = exception.stack
138+
139+
eventTarget.addEvent(exception.name ?? 'Error', attributes, timeInput)
140+
}
141+
142+
/**
143+
* First-call-wins; no-op on ended spans. Only `code === 2` emits Datadog error tags.
144+
*
145+
* @param {import('../opentracing/span')} ddSpan
146+
* @param {{ ended: boolean, _hasStatus: boolean }} bridgeSpan
147+
* @param {{ code?: number, message?: string }} [status]
148+
* @returns {void}
149+
*/
150+
function setStatus (ddSpan, bridgeSpan, { code, message } = {}) {
151+
if (bridgeSpan.ended || bridgeSpan._hasStatus || !code) return
152+
153+
bridgeSpan._hasStatus = true
154+
155+
if (code === 2) {
156+
ddSpan.addTags({
157+
[ERROR_MESSAGE]: message,
158+
[IGNORE_OTEL_ERROR]: false,
159+
})
160+
}
161+
}
162+
163+
module.exports = {
164+
addOtelLink,
165+
normalizeLinkContext,
166+
recordException,
167+
setOtelAttribute,
168+
setOtelAttributes,
169+
setStatus,
170+
}

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

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ const { timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/co
99

1010
const tracer = require('../../')
1111
const DatadogSpan = require('../opentracing/span')
12-
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('../constants')
1312
const { SERVICE_NAME, RESOURCE_NAME, SPAN_KIND } = require('../../../../ext/tags')
1413
const kinds = require('../../../../ext/kinds')
1514

1615
const id = require('../id')
1716
const SpanContext = require('./span_context')
17+
const {
18+
addOtelLink,
19+
recordException,
20+
setOtelAttribute,
21+
setOtelAttributes,
22+
setStatus,
23+
} = require('./span-helpers')
1824

1925
// The one built into OTel rounds so we lose sub-millisecond precision.
2026
function hrTimeToMilliseconds (time) {
@@ -195,33 +201,17 @@ class Span {
195201
}
196202

197203
setAttribute (key, value) {
198-
if (key === 'http.response.status_code') {
199-
this._ddSpan.setTag('http.status_code', value.toString())
200-
}
201-
202-
this._ddSpan.setTag(key, value)
204+
setOtelAttribute(this._ddSpan, key, value)
203205
return this
204206
}
205207

206208
setAttributes (attributes) {
207-
if ('http.response.status_code' in attributes) {
208-
attributes['http.status_code'] = attributes['http.response.status_code'].toString()
209-
}
210-
211-
this._ddSpan.addTags(attributes)
209+
setOtelAttributes(this._ddSpan, attributes)
212210
return this
213211
}
214212

215213
addLink (link, attrs) {
216-
// TODO: Remove this once we remove addLink(context, attrs) in v6.0.0
217-
if (link instanceof SpanContext) {
218-
link = { context: link, attributes: attrs ?? {} }
219-
}
220-
221-
const { context, attributes } = link
222-
// Extract dd context
223-
const ddSpanContext = context._ddContext
224-
this._ddSpan.addLink({ context: ddSpanContext, attributes })
214+
addOtelLink(this._ddSpan, link, attrs)
225215
return this
226216
}
227217

@@ -244,16 +234,8 @@ class Span {
244234
return this.addLink(zeroContext, attributes)
245235
}
246236

247-
setStatus ({ code, message }) {
248-
if (!this.ended && !this._hasStatus && code) {
249-
this._hasStatus = true
250-
if (code === 2) {
251-
this._ddSpan.addTags({
252-
[ERROR_MESSAGE]: message,
253-
[IGNORE_OTEL_ERROR]: false,
254-
})
255-
}
256-
}
237+
setStatus (status) {
238+
setStatus(this._ddSpan, this, status)
257239
return this
258240
}
259241

@@ -291,18 +273,8 @@ class Span {
291273
}
292274

293275
recordException (exception, timeInput) {
294-
this._ddSpan.addTags({
295-
[ERROR_TYPE]: exception.name,
296-
[ERROR_MESSAGE]: exception.message,
297-
[ERROR_STACK]: exception.stack,
298-
[IGNORE_OTEL_ERROR]: this._ddSpan.context()._tags[IGNORE_OTEL_ERROR] ?? true,
299-
})
300-
const attributes = {}
301-
if (exception.message) attributes['exception.message'] = exception.message
302-
if (exception.type) attributes['exception.type'] = exception.type
303-
if (exception.escaped) attributes['exception.escaped'] = exception.escaped
304-
if (exception.stack) attributes['exception.stacktrace'] = exception.stack
305-
this.addEvent(exception.name, attributes, timeInput)
276+
// Route through `this.addEvent` so its time-input conversion applies.
277+
recordException(this._ddSpan, this, exception, timeInput)
306278
}
307279

308280
get duration () {

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

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,12 @@ const tracer = require('../../')
77

88
const id = require('../id')
99
const log = require('../log')
10-
const DatadogSpanContext = require('../opentracing/span_context')
1110
const TextMapPropagator = require('../opentracing/propagation/text_map')
1211
const TraceState = require('../opentracing/propagation/tracestate')
1312
const SpanContext = require('./span_context')
1413
const Span = require('./span')
1514
const Sampler = require('./sampler')
16-
17-
function normalizeLinkContext (context) {
18-
if (!context) return
19-
20-
// OTel API bridge SpanContext wrapper
21-
if (context._ddContext) return context._ddContext
22-
23-
// Datadog span context
24-
if (typeof context.toTraceId === 'function' && typeof context.toSpanId === 'function') {
25-
return context
26-
}
27-
28-
// Standard OTel SpanContext (traceId/spanId)
29-
if (typeof context.traceId !== 'string' || typeof context.spanId !== 'string') {
30-
// Invalid
31-
return
32-
}
33-
34-
let sampling
35-
if (typeof context.traceFlags === 'number') {
36-
sampling = { priority: context.traceFlags & 1 }
37-
}
38-
39-
let tracestate
40-
if (context.traceState?.serialize) {
41-
tracestate = TraceState.fromString(context.traceState.serialize())
42-
}
43-
44-
return new DatadogSpanContext({
45-
traceId: id(context.traceId, 16),
46-
spanId: id(context.spanId, 16),
47-
sampling,
48-
tracestate,
49-
})
50-
}
15+
const { normalizeLinkContext } = require('./span-helpers')
5116

5217
class Tracer {
5318
constructor (library, config, tracerProvider) {

0 commit comments

Comments
 (0)