Skip to content

Commit 2db2ea5

Browse files
authored
Khanayan123/implement span events (#4386)
* implement span events and record exception api
1 parent a7413ed commit 2db2ea5

File tree

10 files changed

+315
-12
lines changed

10 files changed

+315
-12
lines changed

packages/dd-trace/src/format.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function format (span) {
3434
const formatted = formatSpan(span)
3535

3636
extractSpanLinks(formatted, span)
37+
extractSpanEvents(formatted, span)
3738
extractRootTags(formatted, span)
3839
extractChunkTags(formatted, span)
3940
extractTags(formatted, span)
@@ -88,6 +89,22 @@ function extractSpanLinks (trace, span) {
8889
if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
8990
}
9091

92+
function extractSpanEvents (trace, span) {
93+
const events = []
94+
if (span._events) {
95+
for (const event of span._events) {
96+
const formattedEvent = {
97+
name: event.name,
98+
time_unix_nano: Math.round(event.startTime * 1e6),
99+
attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
100+
}
101+
102+
events.push(formattedEvent)
103+
}
104+
}
105+
if (events.length > 0) { trace.meta.events = JSON.stringify(events) }
106+
}
107+
91108
function extractTags (trace, span) {
92109
const context = span.context()
93110
const origin = context._trace.origin
@@ -134,15 +151,17 @@ function extractTags (trace, span) {
134151
case ERROR_STACK:
135152
// HACK: remove when implemented in the backend
136153
if (context._name !== 'fs.operation') {
137-
trace.error = 1
154+
// HACK: to ensure otel.recordException does not influence trace.error
155+
if (tags.setTraceError) {
156+
trace.error = 1
157+
}
138158
} else {
139159
break
140160
}
141161
default: // eslint-disable-line no-fallthrough
142162
addTag(trace.meta, trace.metrics, tag, tags[tag])
143163
}
144164
}
145-
146165
setSingleSpanIngestionTags(trace, context._spanSampling)
147166

148167
addTag(trace.meta, trace.metrics, 'language', 'javascript')

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ function hrTimeToMilliseconds (time) {
2020
return time[0] * 1e3 + time[1] / 1e6
2121
}
2222

23+
function isTimeInput (startTime) {
24+
if (typeof startTime === 'number') {
25+
return true
26+
}
27+
if (startTime instanceof Date) {
28+
return true
29+
}
30+
if (Array.isArray(startTime) && startTime.length === 2 &&
31+
typeof startTime[0] === 'number' && typeof startTime[1] === 'number') {
32+
return true
33+
}
34+
return false
35+
}
36+
2337
const spanKindNames = {
2438
[api.SpanKind.INTERNAL]: kinds.INTERNAL,
2539
[api.SpanKind.SERVER]: kinds.SERVER,
@@ -196,11 +210,6 @@ class Span {
196210
return this
197211
}
198212

199-
addEvent (name, attributesOrStartTime, startTime) {
200-
api.diag.warn('Events not supported')
201-
return this
202-
}
203-
204213
addLink (context, attributes) {
205214
// extract dd context
206215
const ddSpanContext = context._ddContext
@@ -244,12 +253,29 @@ class Span {
244253
return this.ended === false
245254
}
246255

247-
recordException (exception) {
256+
addEvent (name, attributesOrStartTime, startTime) {
257+
startTime = attributesOrStartTime && isTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime
258+
const hrStartTime = timeInputToHrTime(startTime || (performance.now() + timeOrigin))
259+
startTime = hrTimeToMilliseconds(hrStartTime)
260+
261+
this._ddSpan.addEvent(name, attributesOrStartTime, startTime)
262+
return this
263+
}
264+
265+
recordException (exception, timeInput) {
266+
// HACK: identifier is added so that trace.error remains unchanged after a call to otel.recordException
248267
this._ddSpan.addTags({
249268
[ERROR_TYPE]: exception.name,
250269
[ERROR_MESSAGE]: exception.message,
251-
[ERROR_STACK]: exception.stack
270+
[ERROR_STACK]: exception.stack,
271+
doNotSetTraceError: true
252272
})
273+
const attributes = {}
274+
if (exception.message) attributes['exception.message'] = exception.message
275+
if (exception.type) attributes['exception.type'] = exception.type
276+
if (exception.escaped) attributes['exception.escaped'] = exception.escaped
277+
if (exception.stack) attributes['exception.stacktrace'] = exception.stack
278+
this.addEvent(exception.name, attributes, timeInput)
253279
}
254280

255281
get duration () {

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ class DatadogSpan {
6767
this._store = storage.getStore()
6868
this._duration = undefined
6969

70+
this._events = []
71+
7072
// For internal use only. You probably want `context()._name`.
7173
// This name property is not updated when the span name changes.
7274
// This is necessary for span count metrics.
@@ -163,6 +165,19 @@ class DatadogSpan {
163165
})
164166
}
165167

168+
addEvent (name, attributesOrStartTime, startTime) {
169+
const event = { name }
170+
if (attributesOrStartTime) {
171+
if (typeof attributesOrStartTime === 'object') {
172+
event.attributes = this._sanitizeEventAttributes(attributesOrStartTime)
173+
} else {
174+
startTime = attributesOrStartTime
175+
}
176+
}
177+
event.startTime = startTime || this._getTime()
178+
this._events.push(event)
179+
}
180+
166181
finish (finishTime) {
167182
if (this._duration !== undefined) {
168183
return
@@ -221,7 +236,30 @@ class DatadogSpan {
221236
const [key, value] = entry
222237
addArrayOrScalarAttributes(key, value)
223238
})
239+
return sanitizedAttributes
240+
}
241+
242+
_sanitizeEventAttributes (attributes = {}) {
243+
const sanitizedAttributes = {}
224244

245+
for (const key in attributes) {
246+
const value = attributes[key]
247+
if (Array.isArray(value)) {
248+
const newArray = []
249+
for (const subkey in value) {
250+
if (ALLOWED.includes(typeof value[subkey])) {
251+
newArray.push(value[subkey])
252+
} else {
253+
log.warn('Dropping span event attribute. It is not of an allowed type')
254+
}
255+
}
256+
sanitizedAttributes[key] = newArray
257+
} else if (ALLOWED.includes(typeof value)) {
258+
sanitizedAttributes[key] = value
259+
} else {
260+
log.warn('Dropping span event attribute. It is not of an allowed type')
261+
}
262+
}
225263
return sanitizedAttributes
226264
}
227265

packages/dd-trace/src/tagger.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict'
22

3+
const constants = require('./constants')
34
const log = require('./log')
5+
const ERROR_MESSAGE = constants.ERROR_MESSAGE
6+
const ERROR_STACK = constants.ERROR_STACK
7+
const ERROR_TYPE = constants.ERROR_TYPE
48

59
const otelTagMap = {
610
'deployment.environment': 'env',
@@ -14,7 +18,6 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
1418
if (Array.isArray(keyValuePairs)) {
1519
return keyValuePairs.forEach(tags => add(carrier, tags))
1620
}
17-
1821
try {
1922
if (typeof keyValuePairs === 'string') {
2023
const segments = keyValuePairs.split(',')
@@ -32,6 +35,12 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
3235
carrier[key.trim()] = value.trim()
3336
}
3437
} else {
38+
// HACK: to ensure otel.recordException does not influence trace.error
39+
if (ERROR_MESSAGE in keyValuePairs || ERROR_STACK in keyValuePairs || ERROR_TYPE in keyValuePairs) {
40+
if (!('doNotSetTraceError' in keyValuePairs)) {
41+
carrier.setTraceError = true
42+
}
43+
}
3544
Object.assign(carrier, keyValuePairs)
3645
}
3746
} catch (e) {

packages/dd-trace/test/encode/0.4.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,21 @@ describe('encode', () => {
185185
})
186186
})
187187

188+
it('should encode span events', () => {
189+
const encodedLink = '[{"name":"Something went so wrong","time_unix_nano":1000000},' +
190+
'{"name":"I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx","time_unix_nano":1633023102000000,' +
191+
'"attributes":{"emotion":"happy","rating":9.8,"other":[1,9.5,1],"idol":false}}]'
192+
193+
data[0].meta.events = encodedLink
194+
195+
encoder.encode(data)
196+
197+
const buffer = encoder.makePayload()
198+
const decoded = msgpack.decode(buffer, { codec })
199+
const trace = decoded[0]
200+
expect(trace[0].meta.events).to.deep.equal(encodedLink)
201+
})
202+
188203
it('should encode spanLinks', () => {
189204
const traceIdHigh = id('10')
190205
const traceId = id('1234abcd1234abcd')

packages/dd-trace/test/encode/0.5.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ describe('encode 0.5', () => {
6565
expect(stringMap[trace[0][11]]).to.equal('') // unset
6666
})
6767

68+
it('should encode span events', () => {
69+
const encodedLink = '[{"name":"Something went so wrong","time_unix_nano":1000000},' +
70+
'{"name":"I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx","time_unix_nano":1633023102000000,' +
71+
'"attributes":{"emotion":"happy","rating":9.8,"other":[1,9.5,1],"idol":false}}]'
72+
73+
data[0].meta.events = encodedLink
74+
75+
encoder.encode(data)
76+
77+
const buffer = encoder.makePayload()
78+
const decoded = msgpack.decode(buffer, { codec })
79+
const stringMap = decoded[0]
80+
const trace = decoded[1][0]
81+
expect(stringMap).to.include('events')
82+
expect(stringMap).to.include(encodedLink)
83+
expect(trace[0][9]).to.include({
84+
[stringMap.indexOf('bar')]: stringMap.indexOf('baz'),
85+
[stringMap.indexOf('events')]: stringMap.indexOf(encodedLink)
86+
})
87+
})
88+
6889
it('should encode span links', () => {
6990
const traceIdHigh = id('10')
7091
const traceId = id('1234abcd1234abcd')

packages/dd-trace/test/format.spec.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ describe('format', () => {
8787
})
8888

8989
describe('format', () => {
90+
it('should format span events', () => {
91+
span._events = [
92+
{ name: 'Something went so wrong', startTime: 1 },
93+
{
94+
name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx',
95+
attributes: { emotion: 'happy', rating: 9.8, other: [1, 9.5, 1], idol: false },
96+
startTime: 1633023102
97+
}
98+
]
99+
100+
trace = format(span)
101+
const spanEvents = JSON.parse(trace.meta.events)
102+
expect(spanEvents).to.deep.equal([{
103+
name: 'Something went so wrong',
104+
time_unix_nano: 1000000
105+
}, {
106+
name: 'I can sing!!! acbdefggnmdfsdv k 2e2ev;!|=xxx',
107+
time_unix_nano: 1633023102000000,
108+
attributes: { emotion: 'happy', rating: 9.8, other: [1, 9.5, 1], idol: false }
109+
}])
110+
})
90111
it('should convert a span to the correct trace format', () => {
91112
trace = format(span)
92113

@@ -403,14 +424,31 @@ describe('format', () => {
403424
})
404425
})
405426

406-
it('should set the error flag when there is an error-related tag', () => {
427+
it('should not set the error flag when there is an error-related tag without a set trace tag', () => {
428+
spanContext._tags[ERROR_TYPE] = 'Error'
429+
spanContext._tags[ERROR_MESSAGE] = 'boom'
430+
spanContext._tags[ERROR_STACK] = ''
431+
432+
trace = format(span)
433+
434+
expect(trace.error).to.equal(0)
435+
})
436+
437+
it('should set the error flag when there is an error-related tag with should setTrace', () => {
407438
spanContext._tags[ERROR_TYPE] = 'Error'
408439
spanContext._tags[ERROR_MESSAGE] = 'boom'
409440
spanContext._tags[ERROR_STACK] = ''
441+
spanContext._tags.setTraceError = 1
410442

411443
trace = format(span)
412444

413445
expect(trace.error).to.equal(1)
446+
447+
spanContext._tags[ERROR_TYPE] = 'foo'
448+
spanContext._tags[ERROR_MESSAGE] = 'foo'
449+
spanContext._tags[ERROR_STACK] = 'foo'
450+
451+
expect(trace.error).to.equal(1)
414452
})
415453

416454
it('should not set the error flag for internal spans with error tags', () => {

0 commit comments

Comments
 (0)