Skip to content

Commit 7bd337b

Browse files
watsonbengl
authored andcommitted
Add support for Fastify entry spans for Code Origin for Spans (#4449)
This commit does two things: - It lays the groundwork for an upcoming feature called "Code Origin for Spans". - To showcase this feature, it adds limited support for just Fastify entry-spans. To enable, set `DD_CODE_ORIGIN_FOR_SPANS_ENABLED=true`.
1 parent f3dc7d3 commit 7bd337b

File tree

13 files changed

+509
-37
lines changed

13 files changed

+509
-37
lines changed

packages/datadog-code-origin/index.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict'
2+
3+
const { getUserLandFrames } = require('../dd-trace/src/plugins/util/stacktrace')
4+
5+
const limit = Number(process.env._DD_CODE_ORIGIN_MAX_USER_FRAMES) || 8
6+
7+
module.exports = {
8+
entryTag,
9+
exitTag
10+
}
11+
12+
function entryTag (topOfStackFunc) {
13+
return tag('entry', topOfStackFunc)
14+
}
15+
16+
function exitTag (topOfStackFunc) {
17+
return tag('exit', topOfStackFunc)
18+
}
19+
20+
function tag (type, topOfStackFunc) {
21+
const frames = getUserLandFrames(topOfStackFunc, limit)
22+
const tags = {
23+
'_dd.code_origin.type': type
24+
}
25+
for (let i = 0; i < frames.length; i++) {
26+
const frame = frames[i]
27+
tags[`_dd.code_origin.frames.${i}.file`] = frame.file
28+
tags[`_dd.code_origin.frames.${i}.line`] = String(frame.line)
29+
tags[`_dd.code_origin.frames.${i}.column`] = String(frame.column)
30+
if (frame.method) {
31+
tags[`_dd.code_origin.frames.${i}.method`] = frame.method
32+
}
33+
if (frame.type) {
34+
tags[`_dd.code_origin.frames.${i}.type`] = frame.type
35+
}
36+
}
37+
return tags
38+
}

packages/datadog-instrumentations/src/fastify.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { addHook, channel, AsyncResource } = require('./helpers/instrument')
55

66
const errorChannel = channel('apm:fastify:middleware:error')
77
const handleChannel = channel('apm:fastify:request:handle')
8+
const routeAddedChannel = channel('apm:fastify:route:added')
89

910
const parsingResources = new WeakMap()
1011

@@ -16,6 +17,7 @@ function wrapFastify (fastify, hasParsingEvents) {
1617

1718
if (!app || typeof app.addHook !== 'function') return app
1819

20+
app.addHook('onRoute', onRoute)
1921
app.addHook('onRequest', onRequest)
2022
app.addHook('preHandler', preHandler)
2123

@@ -86,8 +88,9 @@ function onRequest (request, reply, done) {
8688

8789
const req = getReq(request)
8890
const res = getRes(reply)
91+
const routeConfig = getRouteConfig(request)
8992

90-
handleChannel.publish({ req, res })
93+
handleChannel.publish({ req, res, routeConfig })
9194

9295
return done()
9396
}
@@ -142,6 +145,10 @@ function getRes (reply) {
142145
return reply && (reply.raw || reply.res || reply)
143146
}
144147

148+
function getRouteConfig (request) {
149+
return request?.routeOptions?.config
150+
}
151+
145152
function publishError (error, req) {
146153
if (error) {
147154
errorChannel.publish({ error, req })
@@ -150,6 +157,10 @@ function publishError (error, req) {
150157
return error
151158
}
152159

160+
function onRoute (routeOptions) {
161+
routeAddedChannel.publish({ routeOptions, onRoute })
162+
}
163+
153164
addHook({ name: 'fastify', versions: ['>=3'] }, fastify => {
154165
const wrapped = shimmer.wrapFunction(fastify, fastify => wrapFastify(fastify, true))
155166

packages/datadog-instrumentations/src/mocha/common.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { addHook, channel } = require('../helpers/instrument')
22
const shimmer = require('../../../datadog-shimmer')
3-
const { getCallSites } = require('../../../dd-trace/src/plugins/util/test')
3+
const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace')
44
const { testToStartLine } = require('./utils')
55

66
const parameterizedTestCh = channel('ci:mocha:test:parameterize')
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict'
2+
3+
const { entryTag } = require('../../datadog-code-origin')
4+
const Plugin = require('../../dd-trace/src/plugins/plugin')
5+
const web = require('../../dd-trace/src/plugins/util/web')
6+
7+
const kCodeOriginForSpansTagsSym = Symbol('datadog.codeOriginForSpansTags')
8+
9+
class FastifyCodeOriginForSpansPlugin extends Plugin {
10+
static get id () {
11+
return 'fastify'
12+
}
13+
14+
constructor (...args) {
15+
super(...args)
16+
17+
this.addSub('apm:fastify:request:handle', ({ req, routeConfig }) => {
18+
const tags = routeConfig?.[kCodeOriginForSpansTagsSym]
19+
if (!tags) return
20+
const context = web.getContext(req)
21+
context.span?.addTags(tags)
22+
})
23+
24+
this.addSub('apm:fastify:route:added', ({ routeOptions, onRoute }) => {
25+
if (!routeOptions.config) routeOptions.config = {}
26+
routeOptions.config[kCodeOriginForSpansTagsSym] = entryTag(onRoute)
27+
})
28+
}
29+
}
30+
31+
module.exports = FastifyCodeOriginForSpansPlugin

packages/datadog-plugin-fastify/src/index.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
'use strict'
22

3-
const RouterPlugin = require('../../datadog-plugin-router/src')
3+
const FastifyTracingPlugin = require('./tracing')
4+
const FastifyCodeOriginForSpansPlugin = require('./code_origin')
5+
const CompositePlugin = require('../../dd-trace/src/plugins/composite')
46

5-
class FastifyPlugin extends RouterPlugin {
6-
static get id () {
7-
return 'fastify'
8-
}
9-
10-
constructor (...args) {
11-
super(...args)
12-
13-
this.addSub('apm:fastify:request:handle', ({ req }) => {
14-
this.setFramework(req, 'fastify', this.config)
15-
})
7+
class FastifyPlugin extends CompositePlugin {
8+
static get id () { return 'fastify' }
9+
static get plugins () {
10+
return {
11+
tracing: FastifyTracingPlugin,
12+
codeOriginForSpans: FastifyCodeOriginForSpansPlugin
13+
}
1614
}
1715
}
1816

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
const RouterPlugin = require('../../datadog-plugin-router/src')
4+
5+
class FastifyTracingPlugin extends RouterPlugin {
6+
static get id () {
7+
return 'fastify'
8+
}
9+
10+
constructor (...args) {
11+
super(...args)
12+
13+
this.addSub('apm:fastify:request:handle', ({ req }) => {
14+
this.setFramework(req, 'fastify', this.config)
15+
})
16+
}
17+
}
18+
19+
module.exports = FastifyTracingPlugin
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
'use strict'
2+
3+
const axios = require('axios')
4+
const semver = require('semver')
5+
const agent = require('../../dd-trace/test/plugins/agent')
6+
const { NODE_MAJOR } = require('../../../version')
7+
8+
const host = 'localhost'
9+
10+
describe('Plugin', () => {
11+
let fastify
12+
let app
13+
14+
describe('fastify', () => {
15+
withVersions('fastify', 'fastify', (version, _, specificVersion) => {
16+
if (NODE_MAJOR <= 18 && semver.satisfies(specificVersion, '>=5')) return
17+
18+
afterEach(() => {
19+
app.close()
20+
})
21+
22+
withExports('fastify', version, ['default', 'fastify'], '>=3', getExport => {
23+
describe('with tracer config codeOriginForSpans.enabled: true', () => {
24+
if (semver.satisfies(specificVersion, '<4')) return // TODO: Why doesn't it work on older versions?
25+
26+
before(() => {
27+
return agent.load(
28+
['fastify', 'find-my-way', 'http'],
29+
[{}, {}, { client: false }],
30+
{ codeOriginForSpans: { enabled: true } }
31+
)
32+
})
33+
34+
after(() => {
35+
return agent.close({ ritmReset: false })
36+
})
37+
38+
beforeEach(() => {
39+
fastify = getExport()
40+
app = fastify()
41+
42+
if (semver.intersects(version, '>=3')) {
43+
return app.register(require('../../../versions/middie').get())
44+
}
45+
})
46+
47+
it('should add code_origin tag on entry spans when feature is enabled', done => {
48+
let routeRegisterLine
49+
50+
// Wrap in a named function to have at least one frame with a function name
51+
function wrapperFunction () {
52+
routeRegisterLine = getNextLineNumber()
53+
app.get('/user', function userHandler (request, reply) {
54+
reply.send()
55+
})
56+
}
57+
58+
const callWrapperLine = getNextLineNumber()
59+
wrapperFunction()
60+
61+
app.listen(() => {
62+
const port = app.server.address().port
63+
64+
agent
65+
.use(traces => {
66+
const spans = traces[0]
67+
const tags = spans[0].meta
68+
69+
expect(tags).to.have.property('_dd.code_origin.type', 'entry')
70+
71+
expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename)
72+
expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine)
73+
expect(tags).to.have.property('_dd.code_origin.frames.0.column').to.match(/^\d+$/)
74+
expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'wrapperFunction')
75+
expect(tags).to.not.have.property('_dd.code_origin.frames.0.type')
76+
77+
expect(tags).to.have.property('_dd.code_origin.frames.1.file', __filename)
78+
expect(tags).to.have.property('_dd.code_origin.frames.1.line', callWrapperLine)
79+
expect(tags).to.have.property('_dd.code_origin.frames.1.column').to.match(/^\d+$/)
80+
expect(tags).to.not.have.property('_dd.code_origin.frames.1.method')
81+
expect(tags).to.have.property('_dd.code_origin.frames.1.type', 'Context')
82+
83+
expect(tags).to.not.have.property('_dd.code_origin.frames.2.file')
84+
})
85+
.then(done)
86+
.catch(done)
87+
88+
axios
89+
.get(`http://localhost:${port}/user`)
90+
.catch(done)
91+
})
92+
})
93+
94+
it('should point to where actual route handler is configured, not the prefix', done => {
95+
let routeRegisterLine
96+
97+
app.register(function v1Handler (app, opts, done) {
98+
routeRegisterLine = getNextLineNumber()
99+
app.get('/user', function userHandler (request, reply) {
100+
reply.send()
101+
})
102+
done()
103+
}, { prefix: '/v1' })
104+
105+
app.listen(() => {
106+
const port = app.server.address().port
107+
108+
agent
109+
.use(traces => {
110+
const spans = traces[0]
111+
const tags = spans[0].meta
112+
113+
expect(tags).to.have.property('_dd.code_origin.type', 'entry')
114+
115+
expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename)
116+
expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine)
117+
expect(tags).to.have.property('_dd.code_origin.frames.0.column').to.match(/^\d+$/)
118+
expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'v1Handler')
119+
expect(tags).to.not.have.property('_dd.code_origin.frames.0.type')
120+
121+
expect(tags).to.not.have.property('_dd.code_origin.frames.1.file')
122+
})
123+
.then(done)
124+
.catch(done)
125+
126+
axios
127+
.get(`http://localhost:${port}/v1/user`)
128+
.catch(done)
129+
})
130+
})
131+
132+
it('should point to route handler even if passed through a middleware', function testCase (done) {
133+
app.use(function middleware (req, res, next) {
134+
next()
135+
})
136+
137+
const routeRegisterLine = getNextLineNumber()
138+
app.get('/user', function userHandler (request, reply) {
139+
reply.send()
140+
})
141+
142+
app.listen({ host, port: 0 }, () => {
143+
const port = app.server.address().port
144+
145+
agent
146+
.use(traces => {
147+
const spans = traces[0]
148+
const tags = spans[0].meta
149+
150+
expect(tags).to.have.property('_dd.code_origin.type', 'entry')
151+
152+
expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename)
153+
expect(tags).to.have.property('_dd.code_origin.frames.0.line', routeRegisterLine)
154+
expect(tags).to.have.property('_dd.code_origin.frames.0.column').to.match(/^\d+$/)
155+
expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'testCase')
156+
expect(tags).to.have.property('_dd.code_origin.frames.0.type', 'Context')
157+
158+
expect(tags).to.not.have.property('_dd.code_origin.frames.1.file')
159+
})
160+
.then(done)
161+
.catch(done)
162+
163+
axios
164+
.get(`http://localhost:${port}/user`)
165+
.catch(done)
166+
})
167+
})
168+
169+
// TODO: In Fastify, the route is resolved before the middleware is called, so we actually can get the line
170+
// number of where the route handler is defined. However, this might not be the right choice and it might be
171+
// better to point to the middleware.
172+
it.skip('should point to middleware if middleware responds early', function testCase (done) {
173+
const middlewareRegisterLine = getNextLineNumber()
174+
app.use(function middleware (req, res, next) {
175+
res.end()
176+
})
177+
178+
app.get('/user', function userHandler (request, reply) {
179+
reply.send()
180+
})
181+
182+
app.listen({ host, port: 0 }, () => {
183+
const port = app.server.address().port
184+
185+
agent
186+
.use(traces => {
187+
const spans = traces[0]
188+
const tags = spans[0].meta
189+
190+
expect(tags).to.have.property('_dd.code_origin.type', 'entry')
191+
192+
expect(tags).to.have.property('_dd.code_origin.frames.0.file', __filename)
193+
expect(tags).to.have.property('_dd.code_origin.frames.0.line', middlewareRegisterLine)
194+
expect(tags).to.have.property('_dd.code_origin.frames.0.column').to.match(/^\d+$/)
195+
expect(tags).to.have.property('_dd.code_origin.frames.0.method', 'testCase')
196+
expect(tags).to.have.property('_dd.code_origin.frames.0.type', 'Context')
197+
198+
expect(tags).to.not.have.property('_dd.code_origin.frames.1.file')
199+
})
200+
.then(done)
201+
.catch(done)
202+
203+
axios
204+
.get(`http://localhost:${port}/user`)
205+
.catch(done)
206+
})
207+
})
208+
})
209+
})
210+
})
211+
})
212+
})
213+
214+
function getNextLineNumber () {
215+
return String(Number(new Error().stack.split('\n')[2].match(/:(\d+):/)[1]) + 1)
216+
}

0 commit comments

Comments
 (0)