dd-trace
Version:
Datadog APM tracing client for JavaScript
427 lines (350 loc) • 12.3 kB
JavaScript
'use strict'
// TODO (new internal tracer): use DC events for lifecycle metrics and test them
const { performance } = require('perf_hooks')
const now = performance.now.bind(performance)
const util = require('util')
const { channel } = require('dc-polyfill')
const id = require('../id')
const tagger = require('../tagger')
const runtimeMetrics = require('../runtime_metrics')
const log = require('../log')
const { storage } = require('../../../datadog-core')
const telemetryMetrics = require('../telemetry/metrics')
const SpanContext = require('./span_context')
const dateNow = Date.now
const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
const unfinishedRegistry = createRegistry('unfinished')
const finishedRegistry = createRegistry('finished')
let OTEL_ENABLED = false
const ALLOWED = new Set(['string', 'number', 'boolean'])
const integrationCounters = {
spans_created: {},
spans_finished: {},
}
const startCh = channel('dd-trace:span:start')
const finishCh = channel('dd-trace:span:finish')
const tagsUpdateCh = channel('dd-trace:span:tags:update')
// Module-scope so we don't allocate a fresh recursive closure on every
// `addLink` / `addEvent`.
/**
* @param {Record<string, string>} out
* @param {string} key
* @param {unknown} value
*/
function addArrayOrScalarAttribute (out, key, value) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
addArrayOrScalarAttribute(out, `${key}.${i}`, value[i])
}
} else if (ALLOWED.has(typeof value)) {
out[key] = typeof value === 'string' ? value : String(value)
} else {
log.warn('Dropping span link attribute. It is not of an allowed type')
}
}
function getIntegrationCounter (event, integration) {
const counters = integrationCounters[event]
if (integration in counters) {
return counters[integration]
}
const counter = tracerMetrics.count(event, [
`integration_name:${integration.toLowerCase()}`,
`otel_enabled:${OTEL_ENABLED}`,
])
integrationCounters[event][integration] = counter
return counter
}
class DatadogSpan {
#parentTracer
constructor (tracer, processor, prioritySampler, fields, debug) {
OTEL_ENABLED = tracer._config.DD_TRACE_OTEL_ENABLED
const operationName = fields.operationName
const parent = fields.parent || null
// Stay on `Object.assign({}, src)` for backportability: V8 12+ (Node 22 /
// 24) inlines `{ ...src }` and beats `Object.assign` here, but on V8 10.2
// / 11.3 (Node 18 / 20) the spread takes a generic runtime path and slows
// `spans-finish-*` by ~140%. Revisit once those LTS lines drop.
// eslint-disable-next-line prefer-object-spread
const tags = Object.assign({}, fields.tags)
const hostname = fields.hostname
this.#parentTracer = tracer
this._debug = debug
this._processor = processor
this._prioritySampler = prioritySampler
this._store = storage('legacy').getHandle()
this._duration = undefined
this._events = []
// For internal use only. You probably want `context()._name`.
// This name property is not updated when the span name changes.
// This is necessary for span count metrics.
this._name = operationName
this._integrationName = fields.integrationName || 'opentracing'
getIntegrationCounter('spans_created', this._integrationName).inc()
this._spanContext = this._createContext(parent, fields)
this._spanContext._name = operationName
this._spanContext._tags = tags
this._spanContext._hostname = hostname
this._spanContext._trace.started.push(this)
this._startTime = fields.startTime || this._getTime()
this._links = fields.links?.map(link => ({
context: link.context._ddContext ?? link.context,
attributes: this._sanitizeAttributes(link.attributes),
})) ?? []
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
runtimeMetrics.increment('runtime.node.spans.unfinished')
runtimeMetrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
runtimeMetrics.increment('runtime.node.spans.open') // unfinished for real
runtimeMetrics.increment('runtime.node.spans.open.by.name', `span_name:${operationName}`)
unfinishedRegistry.register(this, operationName, this)
}
if (tracer._config.DD_TRACE_SPAN_LEAK_DEBUG > 0) {
require('../spanleak').addSpan(this)
}
if (startCh.hasSubscribers) {
startCh.publish({ span: this, fields })
}
}
[util.inspect.custom] () {
return {
...this,
parentTracer: `[${this.#parentTracer.constructor.name}]`,
_prioritySampler: `[${this._prioritySampler.constructor.name}]`,
_processor: `[${this._processor.constructor.name}]`,
}
}
toString () {
const spanContext = this.context()
const resourceName = spanContext._tags['resource.name'] || ''
const resource = resourceName.length > 100
? `${resourceName.slice(0, 97)}...`
: resourceName
const json = JSON.stringify({
traceId: spanContext._traceId,
spanId: spanContext._spanId,
parentId: spanContext._parentId,
service: spanContext._tags['service.name'],
name: spanContext._name,
resource,
})
return `Span${json}`
}
/**
* @returns {import('./span_context')}
*/
context () {
return this._spanContext
}
tracer () {
return this.#parentTracer
}
setOperationName (name) {
this._spanContext._name = name
return this
}
setBaggageItem (key, value) {
this._spanContext._baggageItems[key] = value
return this
}
getBaggageItem (key) {
return this._spanContext._baggageItems[key]
}
getAllBaggageItems () {
return JSON.stringify(this._spanContext._baggageItems)
}
removeBaggageItem (key) {
delete this._spanContext._baggageItems[key]
}
removeAllBaggageItems () {
this._spanContext._baggageItems = {}
}
setTag (key, value) {
this._addTags({ [key]: value })
return this
}
addTags (keyValueMap) {
this._addTags(keyValueMap)
return this
}
log () {
return this
}
logEvent () {}
addLink (link, attrs) {
// TODO: Remove this once we remove addLink(context, attrs) in v6.0.0
if (link instanceof SpanContext) {
link = { context: link, attributes: attrs ?? {} }
}
const { context, attributes } = link
this._links.push({
context: context._ddContext ?? context,
attributes: this._sanitizeAttributes(attributes),
})
}
addLinks (links) {
for (const link of links) {
this.addLink(link)
}
return this
}
addSpanPointer (ptrKind, ptrDir, ptrHash) {
const zeroContext = new SpanContext({
traceId: id('0'),
spanId: id('0'),
})
const attributes = {
'ptr.kind': ptrKind,
'ptr.dir': ptrDir,
'ptr.hash': ptrHash,
'link.kind': 'span-pointer',
}
this.addLink({ context: zeroContext, attributes })
}
addEvent (name, attributesOrStartTime, startTime) {
const event = { name }
if (attributesOrStartTime) {
if (typeof attributesOrStartTime === 'object') {
event.attributes = this._sanitizeEventAttributes(attributesOrStartTime)
} else {
startTime = attributesOrStartTime
}
}
event.startTime = startTime || this._getTime()
this._events.push(event)
}
finish (finishTime) {
if (this._duration !== undefined) {
return
}
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_STATE_TRACKING && !this._spanContext._tags['service.name']) {
log.error('Finishing invalid span: %s', this)
}
getIntegrationCounter('spans_finished', this._integrationName).inc()
this._spanContext._tags['_dd.integration'] = this._integrationName
if (this.#parentTracer._config.DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
runtimeMetrics.decrement('runtime.node.spans.unfinished')
runtimeMetrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`)
runtimeMetrics.increment('runtime.node.spans.finished')
runtimeMetrics.increment('runtime.node.spans.finished.by.name', `span_name:${this._name}`)
runtimeMetrics.decrement('runtime.node.spans.open') // unfinished for real
runtimeMetrics.decrement('runtime.node.spans.open.by.name', `span_name:${this._name}`)
unfinishedRegistry.unregister(this)
finishedRegistry.register(this, this._name)
}
// Dominant call site is `span.finish()` with no argument; skip the
// `Number.parseFloat` round-trip for the undefined case.
finishTime = finishTime === undefined
? this._getTime()
: (Number.parseFloat(finishTime) || this._getTime())
this._duration = finishTime - this._startTime
this._spanContext._trace.finished.push(this)
this._spanContext._isFinished = true
finishCh.publish(this)
this._processor.process(this)
}
/**
* @param {Record<string, unknown>} [attributes]
*/
_sanitizeAttributes (attributes = {}) {
/** @type {Record<string, string>} */
const out = {}
for (const key of Object.keys(attributes)) {
addArrayOrScalarAttribute(out, key, attributes[key])
}
return out
}
/**
* @param {Record<string, unknown>} [attributes]
*/
_sanitizeEventAttributes (attributes = {}) {
const sanitizedAttributes = {}
for (const key of Object.keys(attributes)) {
const value = attributes[key]
if (Array.isArray(value)) {
const newArray = []
for (const subvalue of value) {
if (ALLOWED.has(typeof subvalue)) {
newArray.push(subvalue)
} else {
log.warn('Dropping span event attribute. It is not of an allowed type')
}
}
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 sanitizedAttributes
}
_createContext (parent, fields) {
let spanContext
let startTime
let baggage = {}
const propagationBehavior = this.#parentTracer._config.DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
if (parent && parent._isRemote && propagationBehavior !== 'continue') {
baggage = parent._baggageItems
parent = null
}
if (fields.context) {
spanContext = fields.context
if (!spanContext._trace.startTime) {
startTime = dateNow()
}
} else if (parent) {
spanContext = new SpanContext({
traceId: parent._traceId,
spanId: id(),
parentId: parent._spanId,
sampling: parent._sampling,
baggageItems: { ...parent._baggageItems },
trace: parent._trace,
tracestate: parent._tracestate,
})
if (!spanContext._trace.startTime) {
startTime = dateNow()
}
} else {
const spanId = id()
startTime = dateNow()
spanContext = new SpanContext({
traceId: spanId,
spanId,
})
spanContext._trace.startTime = startTime
if (fields.traceId128BitGenerationEnabled) {
spanContext._trace.tags['_dd.p.tid'] = Math.floor(startTime / 1000).toString(16)
.padStart(8, '0')
.padEnd(16, '0')
}
if (propagationBehavior === 'restart') {
spanContext._baggageItems = baggage
}
}
spanContext._trace.ticks = spanContext._trace.ticks || now()
if (startTime) {
spanContext._trace.startTime = startTime
}
// SpanContext was NOT propagated from a remote parent
spanContext._isRemote = false
return spanContext
}
_getTime () {
const { startTime, ticks } = this._spanContext._trace
return startTime + now() - ticks
}
_addTags (keyValuePairs) {
tagger.add(this._spanContext._tags, keyValuePairs)
this._prioritySampler.sample(this, false)
if (tagsUpdateCh.hasSubscribers) {
tagsUpdateCh.publish(this)
}
}
}
function createRegistry (type) {
return new global.FinalizationRegistry(name => {
runtimeMetrics.decrement(`runtime.node.spans.${type}`)
runtimeMetrics.decrement(`runtime.node.spans.${type}.by.name`, [`span_name:${name}`])
})
}
module.exports = DatadogSpan