dd-trace
Version:
Datadog APM tracing client for JavaScript
306 lines (255 loc) • 8.35 kB
JavaScript
'use strict'
const api = require('@opentelemetry/api')
const { performance } = require('perf_hooks')
const { timeOrigin } = performance
const { timeInputToHrTime } = require('@opentelemetry/core')
const tracer = require('../../')
const DatadogSpan = require('../opentracing/span')
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('../constants')
const { SERVICE_NAME, RESOURCE_NAME } = require('../../../../ext/tags')
const kinds = require('../../../../ext/kinds')
const SpanContext = require('./span_context')
const id = require('../id')
// The one built into OTel rounds so we lose sub-millisecond precision.
function hrTimeToMilliseconds (time) {
return time[0] * 1e3 + time[1] / 1e6
}
function isTimeInput (startTime) {
if (typeof startTime === 'number') {
return true
}
if (startTime instanceof Date) {
return true
}
if (Array.isArray(startTime) && startTime.length === 2 &&
typeof startTime[0] === 'number' && typeof startTime[1] === 'number') {
return true
}
return false
}
const spanKindNames = {
[api.SpanKind.INTERNAL]: kinds.INTERNAL,
[api.SpanKind.SERVER]: kinds.SERVER,
[api.SpanKind.CLIENT]: kinds.CLIENT,
[api.SpanKind.PRODUCER]: kinds.PRODUCER,
[api.SpanKind.CONSUMER]: kinds.CONSUMER
}
/**
* Several of these attributes are not yet supported by the Node.js OTel API.
* We check for old equivalents where we can, but not all had equivalents.
*/
function spanNameMapper (spanName, kind, attributes) {
if (spanName) return spanName
const opName = attributes['operation.name']
if (opName) return opName
const { INTERNAL, SERVER, CLIENT } = api.SpanKind
// HTTP server and client requests
// TODO: Drop http.method when http.request.method is supported.
for (const key of ['http.method', 'http.request.method']) {
if (key in attributes) {
if (kind === SERVER) {
return 'http.server.request'
}
if (kind === CLIENT) {
return 'http.client.request'
}
}
}
// Databases
const dbSystem = attributes['db.system']
if (dbSystem && kind === CLIENT) {
return `${dbSystem}.query`
}
// Messaging
const msgSys = attributes['messaging.system']
const msgOp = attributes['messaging.operation']
if (msgSys && msgOp && kind !== INTERNAL) {
return `${msgSys}.${msgOp}`
}
// RPC (and AWS)
const rpcSystem = attributes['rpc.system']
if (rpcSystem) {
if (kind === CLIENT) {
return rpcSystem === 'aws-api'
? `aws.${attributes['rpc.service'] || 'client'}.request`
: `${rpcSystem}.client.request`
}
if (kind === SERVER) {
return `${rpcSystem}.server.request`
}
}
// FaaS
const faasProvider = attributes['faas.invoked_provider']
const faasName = attributes['faas.invoked_name']
const faasTrigger = attributes['faas.trigger']
if (kind === CLIENT && faasProvider && faasName) {
return `${faasProvider}.${faasName}.invoke`
}
if (kind === SERVER && faasTrigger) {
return `${faasTrigger}.invoke`
}
// GraphQL
// NOTE: Not part of Semantic Convention spec yet, but is used in the GraphQL
// integration.
const isGraphQL = 'graphql.operation.type' in attributes
if (isGraphQL) return 'graphql.server.request'
// Network
// TODO: Doesn't exist yet. No equivalent.
const protocol = attributes['network.protocol.name']
const protocolPrefix = protocol ? `${protocol}.` : ''
if (kind === SERVER) return `${protocolPrefix}server.request`
if (kind === CLIENT) return `${protocolPrefix}client.request`
// If all else fails, default to stringified span.kind.
return spanKindNames[kind]
}
class Span {
constructor (
parentTracer,
context,
spanName,
spanContext,
kind,
links = [],
timeInput,
attributes
) {
const { _tracer } = tracer
const hrStartTime = timeInputToHrTime(timeInput || (performance.now() + timeOrigin))
const startTime = hrTimeToMilliseconds(hrStartTime)
this._ddSpan = new DatadogSpan(_tracer, _tracer._processor, _tracer._prioritySampler, {
operationName: spanNameMapper(spanName, kind, attributes),
context: spanContext._ddContext,
startTime,
hostname: _tracer._hostname,
integrationName: parentTracer?._isOtelLibrary ? 'otel.library' : 'otel',
tags: {
[SERVICE_NAME]: _tracer._service,
[RESOURCE_NAME]: spanName
},
links
}, _tracer._debug)
if (attributes) {
this.setAttributes(attributes)
}
this._parentTracer = parentTracer
this._context = context
this._hasStatus = false
// NOTE: Need to grab the value before setting it on the span because the
// math for computing opentracing timestamps is apparently lossy...
this.startTime = hrStartTime
this.kind = kind
this._spanProcessor.onStart(this, context)
}
get parentSpanId () {
const { _parentId } = this._ddSpan.context()
return _parentId && _parentId.toString(16)
}
// Expected by OTel
get resource () {
return this._parentTracer.resource
}
get instrumentationLibrary () {
return this._parentTracer.instrumentationLibrary
}
get _spanProcessor () {
return this._parentTracer.getActiveSpanProcessor()
}
get name () {
return this._ddSpan.context()._name
}
spanContext () {
return new SpanContext(this._ddSpan.context())
}
setAttribute (key, value) {
if (key === 'http.response.status_code') {
this._ddSpan.setTag('http.status_code', value.toString())
}
this._ddSpan.setTag(key, value)
return this
}
setAttributes (attributes) {
if ('http.response.status_code' in attributes) {
attributes['http.status_code'] = attributes['http.response.status_code'].toString()
}
this._ddSpan.addTags(attributes)
return this
}
addLink (context, attributes) {
// extract dd context
const ddSpanContext = context._ddContext
this._ddSpan.addLink(ddSpanContext, attributes)
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'
}
return this.addLink(zeroContext, attributes)
}
setStatus ({ code, message }) {
if (!this.ended && !this._hasStatus && code) {
this._hasStatus = true
if (code === 2) {
this._ddSpan.addTags({
[ERROR_MESSAGE]: message,
[IGNORE_OTEL_ERROR]: false
})
}
}
return this
}
updateName (name) {
if (!this.ended) {
this._ddSpan.setOperationName(name)
}
return this
}
end (timeInput) {
if (this.ended) {
api.diag.error('You can only call end() on a span once.')
return
}
const hrEndTime = timeInputToHrTime(timeInput || (performance.now() + timeOrigin))
const endTime = hrTimeToMilliseconds(hrEndTime)
this._ddSpan.finish(endTime)
this._spanProcessor.onEnd(this)
}
isRecording () {
return this.ended === false
}
addEvent (name, attributesOrStartTime, startTime) {
startTime = attributesOrStartTime && isTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime
const hrStartTime = timeInputToHrTime(startTime || (performance.now() + timeOrigin))
startTime = hrTimeToMilliseconds(hrStartTime)
this._ddSpan.addEvent(name, attributesOrStartTime, startTime)
return this
}
recordException (exception, timeInput) {
this._ddSpan.addTags({
[ERROR_TYPE]: exception.name,
[ERROR_MESSAGE]: exception.message,
[ERROR_STACK]: exception.stack,
[IGNORE_OTEL_ERROR]: this._ddSpan.context()._tags[IGNORE_OTEL_ERROR] ?? true
})
const attributes = {}
if (exception.message) attributes['exception.message'] = exception.message
if (exception.type) attributes['exception.type'] = exception.type
if (exception.escaped) attributes['exception.escaped'] = exception.escaped
if (exception.stack) attributes['exception.stacktrace'] = exception.stack
this.addEvent(exception.name, attributes, timeInput)
}
get duration () {
return this._ddSpan._duration
}
get ended () {
return this.duration !== undefined
}
}
module.exports = Span