UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

427 lines (350 loc) 12.3 kB
'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