Skip to content

Commit

Permalink
Add initial support for Code Origin for Spans (Fastify only)
Browse files Browse the repository at this point in the history
To enable, set `DD_CODE_ORIGIN_FOR_SPANS_ENABLED=true`.

By default span origin is only set for spans whos callstack contains
user land stack frames.

To support span origin on entry spans (e.g. the span automatically
created when an incoming HTTP request is being instrumented), one would
need to add this to the router instrumentation for each web framework.

This commit includes support for span origin on Fastify entry spans
only.
  • Loading branch information
watson committed Oct 7, 2024
1 parent bba5f3d commit 2ff305f
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 32 deletions.
22 changes: 21 additions & 1 deletion packages/datadog-instrumentations/src/fastify.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

const shimmer = require('../../datadog-shimmer')
const { addHook, channel, AsyncResource } = require('./helpers/instrument')
const {
getCodeOriginForSpansTags: getCodeOriginForSpansTagsFromCallsite,
getTopUserLandCallsite
} = require('../../dd-trace/src/plugins/util/stacktrace')

const errorChannel = channel('apm:fastify:middleware:error')
const handleChannel = channel('apm:fastify:request:handle')

const kCodeOriginForSpansTagsSym = Symbol('kCodeOriginForSpansTagsSym')

const parsingResources = new WeakMap()

function wrapFastify (fastify, hasParsingEvents) {
Expand All @@ -27,6 +33,8 @@ function wrapFastify (fastify, hasParsingEvents) {
app.addHook('preHandler', preValidation)
}

app.addHook('onRoute', onRoute)

app.addHook = wrapAddHook(app.addHook)

return app
Expand Down Expand Up @@ -86,8 +94,9 @@ function onRequest (request, reply, done) {

const req = getReq(request)
const res = getRes(reply)
const tags = getCodeOriginForSpansTags(request)

handleChannel.publish({ req, res })
handleChannel.publish({ req, res, tags })

return done()
}
Expand Down Expand Up @@ -142,6 +151,10 @@ function getRes (reply) {
return reply && (reply.raw || reply.res || reply)
}

function getCodeOriginForSpansTags (request) {
return request?.routeOptions?.config?.[kCodeOriginForSpansTagsSym]
}

function publishError (error, req) {
if (error) {
errorChannel.publish({ error, req })
Expand All @@ -150,6 +163,13 @@ function publishError (error, req) {
return error
}

function onRoute (routeOptions) {
const tags = getCodeOriginForSpansTagsFromCallsite(getTopUserLandCallsite())
if (!tags) return
if (!routeOptions.config) routeOptions.config = {}
routeOptions.config[kCodeOriginForSpansTagsSym] = tags
}

addHook({ name: 'fastify', versions: ['>=3'] }, fastify => {
const wrapped = shimmer.wrapFunction(fastify, fastify => wrapFastify(fastify, true))

Expand Down
2 changes: 1 addition & 1 deletion packages/datadog-instrumentations/src/mocha/common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { addHook, channel } = require('../helpers/instrument')
const shimmer = require('../../../datadog-shimmer')
const { getCallSites } = require('../../../dd-trace/src/plugins/util/test')
const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace')
const { testToStartLine } = require('./utils')

const parameterizedTestCh = channel('ci:mocha:test:parameterize')
Expand Down
3 changes: 2 additions & 1 deletion packages/datadog-plugin-fastify/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ class FastifyPlugin extends RouterPlugin {
constructor (...args) {
super(...args)

this.addSub('apm:fastify:request:handle', ({ req }) => {
this.addSub('apm:fastify:request:handle', ({ req, tags }) => {
this.setFramework(req, 'fastify', this.config)
this.setSpanTags(req, tags)
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/datadog-plugin-web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class WebPlugin extends Plugin {
setFramework (req, name, config) {
web.setFramework(req, name, config)
}

setSpanTags (req, tags) {
web.setSpanTags(req, tags)
}
}

module.exports = WebPlugin
4 changes: 4 additions & 0 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ class Config {
this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs
this._setValue(defaults, 'clientIpEnabled', false)
this._setValue(defaults, 'clientIpHeader', null)
this._setValue(defaults, 'codeOriginForSpansEnabled', false)
this._setValue(defaults, 'dbmPropagationMode', 'disabled')
this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1')
this._setValue(defaults, 'dogstatsd.port', '8125')
Expand Down Expand Up @@ -571,6 +572,7 @@ class Config {
DD_APPSEC_RASP_ENABLED,
DD_APPSEC_TRACE_RATE_LIMIT,
DD_APPSEC_WAF_TIMEOUT,
DD_CODE_ORIGIN_FOR_SPANS_ENABLED,
DD_DATA_STREAMS_ENABLED,
DD_DBM_PROPAGATION_MODE,
DD_DOGSTATSD_HOSTNAME,
Expand Down Expand Up @@ -701,6 +703,7 @@ class Config {
this._envUnprocessed['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT
this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
this._setBoolean(env, 'codeOriginForSpansEnabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED)
this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE)
this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME)
this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT)
Expand Down Expand Up @@ -867,6 +870,7 @@ class Config {
this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout
this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
this._setString(opts, 'clientIpHeader', options.clientIpHeader)
this._setBoolean(opts, 'codeOriginForSpansEnabled', options.codeOriginForSpansEnabled)
this._setString(opts, 'dbmPropagationMode', options.dbmPropagationMode)
if (options.dogstatsd) {
this._setString(opts, 'dogstatsd.hostname', options.dogstatsd.hostname)
Expand Down
8 changes: 8 additions & 0 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 { storage } = require('../../../datadog-core')
const telemetryMetrics = require('../telemetry/metrics')
const { channel } = require('dc-polyfill')
const spanleak = require('../spanleak')
const { getTopUserLandCallsite, getCodeOriginForSpansTags } = require('../plugins/util/stacktrace')

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

Expand Down Expand Up @@ -84,6 +85,8 @@ class DatadogSpan {

this._spanContext._trace.started.push(this)

if (fields.codeOriginForSpansEnabled) this._setCodeOriginForSpans()

this._startTime = fields.startTime || this._getTime()

this._links = []
Expand Down Expand Up @@ -315,6 +318,11 @@ class DatadogSpan {
return spanContext
}

_setCodeOriginForSpans (callsite = getTopUserLandCallsite(this.constructor)) {
if (!callsite) return
this.addTags(getCodeOriginForSpansTags(callsite))
}

_getTime () {
const { startTime, ticks } = this._spanContext._trace

Expand Down
2 changes: 2 additions & 0 deletions packages/dd-trace/src/opentracing/tracer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DatadogTracer {
this._url = this._exporter._url
this._enableGetRumData = config.experimental.enableGetRumData
this._traceId128BitGenerationEnabled = config.traceId128BitGenerationEnabled
this._codeOriginForSpansEnabled = config.codeOriginForSpansEnabled
this._propagators = {
[formats.TEXT_MAP]: new TextMapPropagator(config),
[formats.HTTP_HEADERS]: new HttpPropagator(config),
Expand Down Expand Up @@ -63,6 +64,7 @@ class DatadogTracer {
startTime: options.startTime,
hostname: this._hostname,
traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled,
codeOriginForSpansEnabled: this._codeOriginForSpansEnabled,
integrationName: options.integrationName,
links: options.links
}, this._debug)
Expand Down
101 changes: 101 additions & 0 deletions packages/dd-trace/src/plugins/util/stacktrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict'

const { relative, sep, isAbsolute } = require('path')

const cwd = process.cwd()

module.exports = {
getCallSites,
getUserLandCallsites,
getTopUserLandCallsite,
getCodeOriginForSpansTags
}

// From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L1
function getCallSites (constructorOpt) {
const oldLimit = Error.stackTraceLimit
Error.stackTraceLimit = Infinity

const dummy = {}

const v8Handler = Error.prepareStackTrace
Error.prepareStackTrace = function (_, v8StackTrace) {
return v8StackTrace
}
Error.captureStackTrace(dummy, constructorOpt)

const v8StackTrace = dummy.stack
Error.prepareStackTrace = v8Handler
Error.stackTraceLimit = oldLimit

return v8StackTrace
}

function getUserLandCallsites (constructorOpt = getUserLandCallsites, returnTopUserLandFrameOnly = false) {
const callsites = getCallSites(constructorOpt)
for (let i = 0; i < callsites.length; i++) {
const callsite = callsites[i]

if (callsite.isNative()) {
continue
}

const filename = callsite.getFileName()

// If the callsite is native, there will be no associated filename. However, there might be other instances where
// this can happen, so to be sure, we add this additional check
if (filename === null) {
continue
}

// ESM module paths start with the "file://" protocol (because ESM supports https imports)
// TODO: Node.js also supports `data:` and `node:` imports, should we do something specific for `data:`?
const containsFileProtocol = filename.startsWith('file:')

// TODO: I'm not sure how stable this check is. Alternatively, we could consider reversing it if we can get
// a comprehensive list of all non-file-based values, eg:
//
// filename === '<anonymous>' || filename.startsWith('node:')
if (containsFileProtocol === false && isAbsolute(filename) === false) {
continue
}

// TODO: Technically, the algorithm below could be simplified to not use the relative path, but be simply:
//
// if (filename.includes(sep + 'node_modules' + sep)) continue
//
// However, the tests in `packages/dd-trace/test/plugins/util/stacktrace.spec.js` will fail on my machine
// because I have the source code in a parent folder called `node_modules`. So the code below thinks that
// it's not in user-land
const relativePath = relative(cwd, containsFileProtocol ? filename.substring(7) : filename)
if (relativePath.startsWith('node_modules' + sep) || relativePath.includes(sep + 'node_modules' + sep)) {
continue
}

return returnTopUserLandFrameOnly
? callsite
: (i === 0 ? callsites : callsites.slice(i))
}
}

function getTopUserLandCallsite (constructorOpt) {
return getUserLandCallsites(constructorOpt, true)
}

// TODO: This should be somewhere else specifically related to Code Origin for Spans
function getCodeOriginForSpansTags (callsite) {
if (!callsite) return
const file = callsite.getFileName()
const line = String(callsite.getLineNumber())
const method = callsite.getFunctionName()
return method
? {
'_dd.entry_location.file': file,
'_dd.entry_location.line': line,
'_dd.entry_location.method': method
}
: {
'_dd.entry_location.file': file,
'_dd.entry_location.line': line
}
}
21 changes: 0 additions & 21 deletions packages/dd-trace/src/plugins/util/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ module.exports = {
mergeCoverage,
fromCoverageMapToCoverage,
getTestLineStart,
getCallSites,
removeInvalidMetadata,
parseAnnotations,
EFD_STRING,
Expand Down Expand Up @@ -557,26 +556,6 @@ function getTestLineStart (err, testSuitePath) {
}
}

// From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L1
function getCallSites () {
const oldLimit = Error.stackTraceLimit
Error.stackTraceLimit = Infinity

const dummy = {}

const v8Handler = Error.prepareStackTrace
Error.prepareStackTrace = function (_, v8StackTrace) {
return v8StackTrace
}
Error.captureStackTrace(dummy)

const v8StackTrace = dummy.stack
Error.prepareStackTrace = v8Handler
Error.stackTraceLimit = oldLimit

return v8StackTrace
}

/**
* Gets an object of test tags from an Playwright annotations array.
* @param {Object[]} annotations - Annotations from a Playwright test.
Expand Down
9 changes: 9 additions & 0 deletions packages/dd-trace/src/plugins/util/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ const web = {
web.setConfig(req, config)
},

setSpanTags (req, tags) {
const context = this.patch(req)
const span = context.span

if (!span) return

span.addTags(tags)
},

setConfig (req, config) {
const context = contexts.get(req)
const span = context.span
Expand Down
Loading

0 comments on commit 2ff305f

Please sign in to comment.