dd-trace
Version:
Datadog APM tracing client for JavaScript
397 lines (325 loc) • 12 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 dateNow = Date.now
const SpanContext = require('./span_context')
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 { channel } = require('dc-polyfill')
const util = require('util')
const { getEnvironmentVariable } = require('../config-helper')
const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
const DD_TRACE_EXPERIMENTAL_STATE_TRACKING = getEnvironmentVariable('DD_TRACE_EXPERIMENTAL_STATE_TRACKING')
const DD_TRACE_EXPERIMENTAL_SPAN_COUNTS = getEnvironmentVariable('DD_TRACE_EXPERIMENTAL_SPAN_COUNTS')
const unfinishedRegistry = createRegistry('unfinished')
const finishedRegistry = createRegistry('finished')
const OTEL_ENABLED = !!getEnvironmentVariable('DD_TRACE_OTEL_ENABLED')
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')
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 {
constructor (tracer, processor, prioritySampler, fields, debug) {
const operationName = fields.operationName
const parent = fields.parent || null
// TODO(BridgeAR): Investigate why this is causing a performance regression
// 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 && fields.links.forEach(link => this.addLink(link.context, link.attributes))
if (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)
}
// Nullish operator is used here because both `tracer` and `tracer._config`
// can be null and there are tests passing invalid values to the `Span`
// constructor which still succeed today. Part of the problem is that `Span`
// stores only the tracer and not the config, so anything that needs the
// config has to read it from the tracer stored on the span, including
// even `Span` itself in this case.
//
// TODO: Refactor Tracer/Span + tests to avoid having to do nullish checks.
if (tracer?._config?.spanLeakDebug > 0) {
require('../spanleak').addSpan(this, operationName)
}
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 {DatadogSpanContext}
*/
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 (context, attributes) {
this._links.push({
context: context._ddContext ?? context,
attributes: this._sanitizeAttributes(attributes)
})
}
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(zeroContext, attributes)
}
addEvent (name, attributesOrStartTime, startTime) {
const event = { name }
if (attributesOrStartTime) {
if (typeof attributesOrStartTime === 'object') { // eslint-disable-line eslint-rules/eslint-safe-typeof-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 (DD_TRACE_EXPERIMENTAL_STATE_TRACKING === 'true' && !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 (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)
}
finishTime = 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)
}
_sanitizeAttributes (attributes = {}) {
const sanitizedAttributes = {}
const addArrayOrScalarAttributes = (key, maybeArray) => {
if (Array.isArray(maybeArray)) {
for (const subkey in maybeArray) {
addArrayOrScalarAttributes(`${key}.${subkey}`, maybeArray[subkey])
}
} else {
const maybeScalar = maybeArray
if (ALLOWED.has(typeof maybeScalar)) {
// Wrap the value as a string if it's not already a string
sanitizedAttributes[key] = typeof maybeScalar === 'string' ? maybeScalar : String(maybeScalar)
} else {
log.warn('Dropping span link attribute. It is not of an allowed type')
}
}
}
Object.entries(attributes).forEach(entry => {
const [key, value] = entry
addArrayOrScalarAttributes(key, value)
})
return sanitizedAttributes
}
_sanitizeEventAttributes (attributes = {}) {
const sanitizedAttributes = {}
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')
}
}
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 = {}
if (parent && parent._isRemote && this._parentTracer?._config?.tracePropagationBehaviorExtract !== '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 (this._parentTracer?._config?.tracePropagationBehaviorExtract === '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)
}
}
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