Skip to content

Commit 55608eb

Browse files
authored
Improve tagger, format, and encoding (#5354)
* Improve tagger and format These are hot code paths and the otel parts should not be part of the tagger. This removes the special handling for errors in the tagger while also improving the performance and not adding tags that have no purpose. * Minor encoding perf improvement Calling Object.entries is actually doing more work than Object.keys while the result is the same.
1 parent ef3d607 commit 55608eb

11 files changed

Lines changed: 181 additions & 126 deletions

File tree

integration-tests/opentelemetry.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('opentelemetry', () => {
7979
await sandbox.remove()
8080
})
8181

82-
it('should not capture telemetry DD and OTEL vars dont conflict', () => {
82+
it("should not capture telemetry DD and OTEL vars don't conflict", () => {
8383
proc = fork(join(cwd, 'opentelemetry/basic.js'), {
8484
cwd,
8585
env: {

packages/dd-trace/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
ERROR_TYPE: 'error.type',
2727
ERROR_MESSAGE: 'error.message',
2828
ERROR_STACK: 'error.stack',
29+
IGNORE_OTEL_ERROR: Symbol('ignore.otel.error'),
2930
COMPONENT: 'component',
3031
CLIENT_PORT_KEY: 'network.destination.port',
3132
PEER_SERVICE_KEY: 'peer.service',

packages/dd-trace/src/encode/0.4.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ function formatSpanEvents (span) {
357357
delete spanEvent.attributes[key] // delete from attributes if undefined
358358
}
359359
}
360-
if (Object.entries(spanEvent.attributes).length === 0) {
360+
if (Object.keys(spanEvent.attributes).length === 0) {
361361
delete spanEvent.attributes
362362
}
363363
}
@@ -370,48 +370,55 @@ function convertSpanEventAttributeValues (key, value, depth = 0) {
370370
type: 0,
371371
string_value: value
372372
}
373-
} else if (typeof value === 'boolean') {
373+
}
374+
375+
if (typeof value === 'boolean') {
374376
return {
375377
type: 1,
376378
bool_value: value
377379
}
378-
} else if (Number.isInteger(value)) {
379-
return {
380-
type: 2,
381-
int_value: value
380+
}
381+
382+
if (typeof value === 'number') {
383+
if (Number.isInteger(value)) {
384+
return {
385+
type: 2,
386+
int_value: value
387+
}
382388
}
383-
} else if (typeof value === 'number') {
384389
return {
385390
type: 3,
386391
double_value: value
387392
}
388-
} else if (Array.isArray(value)) {
393+
}
394+
395+
if (Array.isArray(value)) {
389396
if (depth === 0) {
390-
const convertedArray = value
391-
.map((val) => convertSpanEventAttributeValues(key, val, 1))
392-
.filter((convertedVal) => convertedVal !== undefined)
397+
const convertedArray = []
398+
for (const val of value) {
399+
const convertedVal = convertSpanEventAttributeValues(key, val, 1)
400+
if (convertedVal !== undefined) {
401+
convertedArray.push(convertedVal)
402+
}
403+
}
393404

394405
// Only include array_value if there are valid elements
395406
if (convertedArray.length > 0) {
396407
return {
397408
type: 4,
398409
array_value: { values: convertedArray }
399410
}
400-
} else {
401-
// If all elements were unsupported, return undefined
402-
return undefined
403411
}
412+
// If all elements were unsupported, return undefined
404413
} else {
405414
memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' +
406415
`Skipping encoding key: ${key}: with value: ${typeof value}.`
407416
)
408-
return undefined
409417
}
410418
} else {
411419
memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
412420
`${key}: with value: ${typeof value}. Skipping encoding of pair.`
413421
)
414-
return undefined
415422
}
416423
}
417424

packages/dd-trace/src/format.js

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const PROCESS_ID = constants.PROCESS_ID
2222
const ERROR_MESSAGE = constants.ERROR_MESSAGE
2323
const ERROR_STACK = constants.ERROR_STACK
2424
const ERROR_TYPE = constants.ERROR_TYPE
25+
const { IGNORE_OTEL_ERROR } = constants
2526

27+
// TODO(BridgeAR)[31.03.2025]: Should these land in the constants file?
2628
const map = {
2729
'operation.name': 'name',
2830
'service.name': 'service',
@@ -69,48 +71,46 @@ function setSingleSpanIngestionTags (span, options) {
6971
}
7072

7173
function extractSpanLinks (formattedSpan, span) {
72-
const links = []
73-
if (span._links) {
74-
for (const link of span._links) {
75-
const { context, attributes } = link
76-
const formattedLink = {}
77-
78-
formattedLink.trace_id = context.toTraceId(true)
79-
formattedLink.span_id = context.toSpanId(true)
80-
81-
if (attributes && Object.keys(attributes).length > 0) {
82-
formattedLink.attributes = attributes
83-
}
84-
if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
85-
if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
74+
if (!span._links?.length) {
75+
return
76+
}
77+
const links = span._links.map(link => {
78+
const { context, attributes } = link
79+
const formattedLink = {
80+
trace_id: context.toTraceId(true),
81+
span_id: context.toSpanId(true)
82+
}
8683

87-
links.push(formattedLink)
84+
if (attributes && Object.keys(attributes).length > 0) {
85+
formattedLink.attributes = attributes
8886
}
89-
}
90-
if (links.length > 0) { formattedSpan.meta['_dd.span_links'] = JSON.stringify(links) }
87+
if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
88+
if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
89+
90+
return formattedLink
91+
})
92+
formattedSpan.meta['_dd.span_links'] = JSON.stringify(links)
9193
}
9294

9395
function extractSpanEvents (formattedSpan, span) {
94-
const events = []
95-
if (span._events) {
96-
for (const event of span._events) {
97-
const formattedEvent = {
98-
name: event.name,
99-
time_unix_nano: Math.round(event.startTime * 1e6),
100-
attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
101-
}
102-
103-
events.push(formattedEvent)
104-
}
105-
}
106-
if (events.length > 0) {
107-
formattedSpan.span_events = events
96+
if (!span._events?.length) {
97+
return
10898
}
99+
const events = span._events.map(event => {
100+
return {
101+
name: event.name,
102+
time_unix_nano: Math.round(event.startTime * 1e6),
103+
attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
104+
}
105+
})
106+
formattedSpan.span_events = events
109107
}
110108

111109
function extractTags (formattedSpan, span) {
112110
const context = span.context()
113111
const origin = context._trace.origin
112+
// TODO(BridgeAR)[31.03.2025]: Look into changing the way we store tags. Using
113+
// a map is likely faster short term.
114114
const tags = context._tags
115115
const hostname = context._hostname
116116
const priority = context._sampling.priority
@@ -126,43 +126,48 @@ function extractTags (formattedSpan, span) {
126126
registerExtraService(tags['service.name'])
127127
}
128128

129-
for (const tag in tags) {
129+
for (const [tag, value] of Object.entries(tags)) {
130+
// TODO(BridgeAR)[31.03.2025]: Check how many tags are defined in average.
131+
// In case there are more than 2 tags in average, check for all special
132+
// cases up front and loop over the tags afterwards, skipping the already
133+
// visited property names by checking a map with these keys.
130134
switch (tag) {
131135
case 'service.name':
132136
case 'span.type':
133137
case 'resource.name':
134-
addTag(formattedSpan, {}, map[tag], tags[tag])
138+
addTag(formattedSpan, {}, map[tag], value)
135139
break
136140
// HACK: remove when Datadog supports numeric status code
137141
case 'http.status_code':
138-
addTag(formattedSpan.meta, {}, tag, tags[tag] && String(tags[tag]))
142+
addTag(formattedSpan.meta, {}, tag, value && String(value))
139143
break
140144
case 'analytics.event':
141-
addTag({}, formattedSpan.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
145+
addTag({}, formattedSpan.metrics, ANALYTICS, value === undefined || value ? 1 : 0)
142146
break
143147
case HOSTNAME_KEY:
144148
case MEASURED:
145-
addTag({}, formattedSpan.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
149+
addTag({}, formattedSpan.metrics, tag, value === undefined || value ? 1 : 0)
146150
break
151+
// TODO(BridgeAR)[31.03.2025]: How come we use two different ways to pass
152+
// through errors? Can we just unify the behavior to always use one way?
147153
case 'error':
148154
if (context._name !== 'fs.operation') {
149-
extractError(formattedSpan, tags[tag])
155+
extractError(formattedSpan, value)
150156
}
151157
break
152158
case ERROR_TYPE:
153159
case ERROR_MESSAGE:
154160
case ERROR_STACK:
155161
// HACK: remove when implemented in the backend
156-
if (context._name !== 'fs.operation') {
157-
// HACK: to ensure otel.recordException does not influence formattedSpan.error
158-
if (tags.setTraceError) {
159-
formattedSpan.error = 1
160-
}
161-
} else {
162+
if (context._name === 'fs.operation') {
162163
break
163164
}
165+
// otel.recordException should not influence trace.error
166+
if (!tags[IGNORE_OTEL_ERROR]) {
167+
formattedSpan.error = 1
168+
}
164169
default: // eslint-disable-line no-fallthrough
165-
addTag(formattedSpan.meta, formattedSpan.metrics, tag, tags[tag])
170+
addTag(formattedSpan.meta, formattedSpan.metrics, tag, value)
166171
}
167172
}
168173
setSingleSpanIngestionTags(formattedSpan, context._spanSampling)
@@ -193,8 +198,8 @@ function extractChunkTags (formattedSpan, span) {
193198

194199
if (!isLocalRoot) return
195200

196-
for (const key in context._trace.tags) {
197-
addTag(formattedSpan.meta, formattedSpan.metrics, key, context._trace.tags[key])
201+
for (const [key, value] of Object.entries(context._trace.tags)) {
202+
addTag(formattedSpan.meta, formattedSpan.metrics, key, value)
198203
}
199204
}
200205

@@ -205,6 +210,8 @@ function extractError (formattedSpan, error) {
205210

206211
if (isError(error)) {
207212
// AggregateError only has a code and no message.
213+
// TODO(BridgeAR)[31.03.2025]: An AggregateError can have a message. Should
214+
// the code just generally be added, if available?
208215
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_MESSAGE, error.message || error.code)
209216
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_TYPE, error.name)
210217
addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_STACK, error.stack)
@@ -223,30 +230,21 @@ function addTag (meta, metrics, key, value, nested) {
223230
case 'boolean':
224231
metrics[key] = value ? 1 : 0
225232
break
226-
case 'undefined':
227-
break
228-
case 'object':
229-
if (value === null) break
233+
default:
234+
if (value == null) break
230235

231236
// Special case for Node.js Buffer and URL
237+
// TODO(BridgeAR)[31.03.2025]: Figure out if all typed arrays should be treated as buffers.
232238
if (isNodeBuffer(value) || isUrl(value)) {
233239
metrics[key] = value.toString()
234240
} else if (!Array.isArray(value) && !nested) {
235-
for (const prop in value) {
236-
if (!hasOwn(value, prop)) continue
237-
238-
addTag(meta, metrics, `${key}.${prop}`, value[prop], true)
241+
for (const [prop, val] of Object.entries(value)) {
242+
addTag(meta, metrics, `${key}.${prop}`, val, true)
239243
}
240244
}
241-
242-
break
243245
}
244246
}
245247

246-
function hasOwn (object, prop) {
247-
return Object.prototype.hasOwnProperty.call(object, prop)
248-
}
249-
250248
function isNodeBuffer (obj) {
251249
return obj.constructor && obj.constructor.name === 'Buffer' &&
252250
typeof obj.readInt8 === 'function' &&

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { timeInputToHrTime } = require('@opentelemetry/core')
99

1010
const tracer = require('../../')
1111
const DatadogSpan = require('../opentracing/span')
12-
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../constants')
12+
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('../constants')
1313
const { SERVICE_NAME, RESOURCE_NAME } = require('../../../../ext/tags')
1414
const kinds = require('../../../../ext/kinds')
1515

@@ -237,7 +237,8 @@ class Span {
237237
this._hasStatus = true
238238
if (code === 2) {
239239
this._ddSpan.addTags({
240-
[ERROR_MESSAGE]: message
240+
[ERROR_MESSAGE]: message,
241+
[IGNORE_OTEL_ERROR]: false
241242
})
242243
}
243244
}
@@ -278,12 +279,11 @@ class Span {
278279
}
279280

280281
recordException (exception, timeInput) {
281-
// HACK: identifier is added so that trace.error remains unchanged after a call to otel.recordException
282282
this._ddSpan.addTags({
283283
[ERROR_TYPE]: exception.name,
284284
[ERROR_MESSAGE]: exception.message,
285285
[ERROR_STACK]: exception.stack,
286-
doNotSetTraceError: true
286+
[IGNORE_OTEL_ERROR]: this._ddSpan.context()._tags[IGNORE_OTEL_ERROR] ?? true
287287
})
288288
const attributes = {}
289289
if (exception.message) attributes['exception.message'] = exception.message

0 commit comments

Comments
 (0)