Skip to content

feat(tracer): add recordException API #6010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,24 @@ declare namespace tracer {
* @returns {void}
*/
addLink (context: SpanContext, attributes?: Object): void;

/**
* Represents an exception object that can be recorded on a span.
*/
Exception?: {
message: string;
name?: string;
stack?: string;
};

/**
* Records an exception as a span event.
*
* @param {Error | Span['Exception']} exception the exception to record.
* @param {otel.Attributes} [attributes] additional attributes to add to the span event.
*/
recordException(exception: Error | Span['Exception'], attributes?: otel.Attributes): this;

}

/**
Expand Down
80 changes: 63 additions & 17 deletions packages/dd-trace/src/opentracing/span.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const telemetryMetrics = require('../telemetry/metrics')
const { channel } = require('dc-polyfill')
const util = require('util')
const { getEnvironmentVariable } = require('../config-helper')
const { isError } = require('../util')

const tracerMetrics = telemetryMetrics.manager.namespace('tracers')

Expand All @@ -26,6 +27,9 @@ const finishedRegistry = createRegistry('finished')
const OTEL_ENABLED = !!getEnvironmentVariable('DD_TRACE_OTEL_ENABLED')
const ALLOWED = new Set(['string', 'number', 'boolean'])

const MIN_INT_64BITS = -(2n ** 63n)
const MAX_INT_64BITS = (2n ** 63n) - 1n

const integrationCounters = {
spans_created: {},
spans_finished: {}
Expand Down Expand Up @@ -230,6 +234,18 @@ class DatadogSpan {
this._events.push(event)
}

recordException (exception, attributes = {}) {
if (!isError(exception)) return

const eventAttributes = {
'exception.type': exception.name,
'exception.message': exception.message,
'exception.stacktrace': exception.stack
}

this.addEvent('exception', { ...eventAttributes, ...attributes })
}

finish (finishTime) {
if (this._duration !== undefined) {
return
Expand Down Expand Up @@ -290,25 +306,55 @@ class DatadogSpan {
return sanitizedAttributes
}

_sanitizeEventAttributes (attributes = {}) {
const sanitizedAttributes = {}
_validateEventAttributes (key, value) {
if (ALLOWED.has(typeof value)) {
return this._validateScalar(key, value)
}

if (Array.isArray(value)) {
if (value.length === 0) return true

for (const key in attributes) {
const value = attributes[key]
if (Array.isArray(value)) {
const newArray = []
for (const subkey in value) {
if (ALLOWED.has(typeof value[subkey])) {
newArray.push(value[subkey])
} else {
log.warn('Dropping span event attribute. It is not of an allowed type')
}
const expectedType = typeof value[0]
if (!ALLOWED.has(expectedType)) {
log.warn(`Dropping span event attribute. List values ${key} must be string, number, or boolean: ${value}`)
return false
}

for (let i = 0; i < value.length; i++) {
const currentType = typeof value[i]
if (currentType !== expectedType || !this._validateScalar(key, value[i])) {
log.warn(`Dropping span event attribute. Attribute ${key} array values are not homogenous or valid: ${value}`)
return false
}
sanitizedAttributes[key] = newArray
} else if (ALLOWED.has(typeof value)) {
sanitizedAttributes[key] = value
} else {
log.warn('Dropping span event attribute. It is not of an allowed type')
}
return true
}

log.warn(`Dropping span event attribute. Attribute ${key} must be (array of) string, number, or boolean: ${value}`)
return false
}

_validateScalar (key, value) {
if (typeof value !== 'number') {
return true
}

if (Number.isInteger(value) && (value < MIN_INT_64BITS || value > MAX_INT_64BITS)) {
log.warn(`Dropping span event attribute. Attribute ${key} must be within the range of a
signed 64-bit integer: ${value}`)
return false
} else if (!Number.isFinite(value)) {
log.warn(`Dropping span event attribute. Attribute ${key} must be a finite number: ${value}`)
return false
}
return true
}

_sanitizeEventAttributes (attributes = {}) {
const sanitizedAttributes = {}
for (const [k, v] of Object.entries(attributes)) {
if (this._validateEventAttributes(k, v)) {
sanitizedAttributes[k] = v
}
}
return sanitizedAttributes
Expand Down
2 changes: 1 addition & 1 deletion packages/dd-trace/test/opentelemetry/span.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ describe('OTel Span', () => {
const span2 = makeSpan('span2')
const datenow = Date.now()
span1.addEvent('Web page unresponsive',
{ 'error.code': '403', 'unknown values': [1, ['h', 'a', [false]]] }, datenow)
{ 'error.code': '403', 'unknown values': [1] }, datenow)
span2.addEvent('Web page loaded')
span2.addEvent('Button changed color', { colors: [112, 215, 70], 'response.time': 134.3, success: true })
const events1 = span1._ddSpan._events
Expand Down
117 changes: 115 additions & 2 deletions packages/dd-trace/test/opentracing/span.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('Span', () => {
let handle
let id
let tagger
let log

beforeEach(() => {
sinon.stub(Date, 'now').returns(1500000000000)
Expand Down Expand Up @@ -48,6 +49,11 @@ describe('Span', () => {
add: sinon.spy()
}

log = {
warn: sinon.stub(),
error: sinon.stub()
}

Span = proxyquire('../src/opentracing/span', {
perf_hooks: {
performance: {
Expand All @@ -56,7 +62,8 @@ describe('Span', () => {
},
'../id': id,
'../tagger': tagger,
'../metrics': metrics
'../metrics': metrics,
'../log': log
})
})

Expand Down Expand Up @@ -336,7 +343,7 @@ describe('Span', () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })

span.addEvent('Web page unresponsive',
{ 'error.code': '403', 'unknown values': [1, ['h', 'a', [false]]] }, 1714536311886)
{ 'error.code': '403', 'unknown values': [1] }, 1714536311886)
span.addEvent('Web page loaded')
span.addEvent('Button changed color', { colors: [112, 215, 70], 'response.time': 134.3, success: true })

Expand Down Expand Up @@ -368,6 +375,112 @@ describe('Span', () => {
})
})

describe('recordException', () => {
it('should record exception', () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })

try {
throw new TypeError('foo')
} catch (error) {
span.recordException(error)
}

try {
throw new Error('bar')
} catch (error) {
span.recordException(error)
}

const events = span._events
expect(events).to.have.lengthOf(2)

expect(events[0].name).to.equal('exception')
expect(events[0].attributes).to.have.property('exception.type', 'TypeError')
expect(events[0].attributes).to.have.property('exception.message', 'foo')

expect(events[1].name).to.equal('exception')
expect(events[1].attributes).to.have.property('exception.type', 'Error')
expect(events[1].attributes).to.have.property('exception.message', 'bar')
})

it('should record exception when error is not an error object', async () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })

span.recordException({ message: 'something went wrong' })

const events = span._events
expect(events).to.have.lengthOf(1)

expect(events[0].name).to.equal('exception')
expect(Object.keys(events[0].attributes)).to.have.lengthOf(1)
expect(events[0].attributes).to.have.property('exception.message', 'something went wrong')
})

it('should record exception with custom attributes', () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })

try {
throw new TypeError('foo')
} catch (error) {
span.recordException(error, { foo: 'bar' })
}

const events = span._events
expect(events).to.have.lengthOf(1)

expect(events[0].name).to.equal('exception')
expect(events[0].attributes).to.have.property('exception.type', 'TypeError')
expect(events[0].attributes).to.have.property('exception.message', 'foo')
expect(events[0].attributes).to.have.property('foo', 'bar')
})

it('should record exception with invalid attributes', () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })

try {
throw new TypeError('foo')
} catch (error) {
span.recordException(error,
{
foo: 'bar',
invalid1: [1, false],
invalid2: [[1]],
invalid3: { foo: 'bar' },
invalid4: NaN,
invalid5: 9223372036854775808n,
})
}

const events = span._events
expect(events).to.have.lengthOf(1)

expect(events[0].name).to.equal('exception')
expect(Object.keys(events[0].attributes)).to.have.lengthOf(4)
expect(events[0].attributes).to.have.property('exception.type', 'TypeError')
expect(events[0].attributes).to.have.property('exception.message', 'foo')
expect(events[0].attributes).to.have.property('foo', 'bar')

// Check that warning logs were called for invalid attributes
expect(log.warn).to.have.been.calledWith(
'Dropping span event attribute. Attribute invalid1 array values are not homogenous or valid: 1,false'
)
expect(log.warn).to.have.been.calledWith(
'Dropping span event attribute. List values invalid2 must be string, number, or boolean: 1'
)
expect(log.warn).to.have.been.calledWith(
'Dropping span event attribute. Attribute invalid3 must be (array of) string, number, or boolean: ' +
'[object Object]'
)
expect(log.warn).to.have.been.calledWith(
'Dropping span event attribute. Attribute invalid4 must be a finite number: NaN'
)
expect(log.warn).to.have.been.calledWith(
'Dropping span event attribute. Attribute invalid5 must be (array of) string, number, or boolean: ' +
'9223372036854775808'
)
})
})

describe('getBaggageItem', () => {
it('should get a baggage item', () => {
span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' })
Expand Down