Skip to content

Commit 772fc3b

Browse files
authored
feat(debugger): add Remote Enablement support (#7137)
Implements Remote Enablement (RE) for Dynamic Instrumentation/Live Debugger and Code Origin via APM_TRACING_MULTICONFIG. Key Remote Config changes: - Add RCClientLibConfigManager class for priority-based merging of multiple APM_TRACING configs (Service+Env > Service > Env > Cluster > Org) - Add APM_TRACING_MULTICONFIG, APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION, APM_TRACING_ENABLE_CODE_ORIGIN, and APM_TRACING_ENABLE_LIVE_DEBUGGING capability flags - Centralize client library configuration remote config logic in config/remote_config.js Key Debugger changes: - Add lifecycle methods (isStarted(), stop(), configure()) to debugger/index.js for dynamic control Key Code Origin changes: - Add runtime enable/disable support for Code Origin via Remote Config - Update Express and Fastify code origin plugins to pre-compute entry tags at route registration time when RC is enabled (allowing runtime enabling to work immediately) - Add integration tests for Code Origin Remote Config toggling
1 parent cb2cd12 commit 772fc3b

File tree

15 files changed

+1562
-117
lines changed

15 files changed

+1562
-117
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const path = require('node:path')
5+
const Axios = require('axios')
6+
const { FakeAgent, sandboxCwd, useSandbox, spawnProc } = require('./helpers')
7+
8+
const ACKNOWLEDGED = 2
9+
10+
describe('Code Origin Remote Config', function () {
11+
this.timeout(20000)
12+
13+
let cwd, agent, proc, axios
14+
15+
useSandbox(
16+
['express', 'fastify'],
17+
false,
18+
[path.join(__dirname, 'code-origin')]
19+
)
20+
21+
before(() => {
22+
cwd = sandboxCwd()
23+
})
24+
25+
afterEach(async () => {
26+
proc?.kill()
27+
await agent?.stop()
28+
})
29+
30+
const frameworks = [
31+
{ name: 'Express', spanName: 'express.request', appFile: 'express-app.js' },
32+
{ name: 'Fastify', spanName: 'fastify.request', appFile: 'fastify-app.js' }
33+
]
34+
35+
const setupApp = (framework, envVars) => async () => {
36+
const appFile = path.join(cwd, 'code-origin', framework.appFile)
37+
agent = await new FakeAgent().start()
38+
proc = await spawnProc(appFile, {
39+
cwd,
40+
env: {
41+
DD_TRACE_AGENT_PORT: agent.port,
42+
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: 0.1,
43+
...envVars
44+
}
45+
})
46+
axios = Axios.create({
47+
baseURL: proc.url,
48+
headers: { 'Content-Type': 'application/json' }
49+
})
50+
}
51+
52+
const addRemoteConfigAndWaitForAck = (libConfig) => {
53+
return /** @type {Promise<void>} */ (new Promise((resolve) => {
54+
// Random config id - Just needs to be unique between calls to this function
55+
const configId = Math.random().toString(36).slice(2)
56+
const handler = (id, version, state) => {
57+
if (id === configId && state === ACKNOWLEDGED) {
58+
agent.removeListener('remote-config-ack-update', handler)
59+
resolve()
60+
}
61+
}
62+
agent.on('remote-config-ack-update', handler)
63+
agent.addRemoteConfig({
64+
product: 'APM_TRACING',
65+
id: configId,
66+
config: {
67+
service_target: { service: 'node', env: '*' },
68+
lib_config: libConfig
69+
}
70+
})
71+
}))
72+
}
73+
74+
const assertCodeOriginPresent = (framework, url = '/hello') => {
75+
return Promise.all([
76+
agent.assertMessageReceived(({ payload }) => {
77+
const spans = payload.flatMap(p => p)
78+
const requestSpan = spans.find(span => span.name === framework.spanName)
79+
assert.ok(requestSpan, `${framework.spanName} span should exist`)
80+
assert.strictEqual(requestSpan.meta['_dd.code_origin.type'], 'entry')
81+
assert.ok(requestSpan.meta['_dd.code_origin.frames.0.file'])
82+
assert.ok(requestSpan.meta['_dd.code_origin.frames.0.line'])
83+
}, 3000),
84+
axios.get(url)
85+
])
86+
}
87+
88+
const assertCodeOriginAbsent = (framework, url = '/hello') => {
89+
return Promise.all([
90+
agent.assertMessageReceived(({ payload }) => {
91+
const spans = payload.flatMap(p => p)
92+
const requestSpan = spans.find(span => span.name === framework.spanName)
93+
assert.ok(requestSpan, `${framework.spanName} span should exist`)
94+
assert.strictEqual(requestSpan.meta['_dd.code_origin.type'], undefined)
95+
assert.strictEqual(requestSpan.meta['_dd.code_origin.frames.0.file'], undefined)
96+
}, 3000),
97+
axios.get(url)
98+
])
99+
}
100+
101+
frameworks.forEach(framework => {
102+
describe(framework.name, () => {
103+
describe('both CO and RC enabled at boot (runtime disable)', () => {
104+
beforeEach(setupApp(framework, {
105+
DD_CODE_ORIGIN_FOR_SPANS_ENABLED: 'true',
106+
DD_REMOTE_CONFIG_ENABLED: 'true'
107+
}))
108+
109+
it('should disable code origin tags at runtime via remote config', async () => {
110+
// Step 1: Make a request with code origin enabled (default)
111+
await assertCodeOriginPresent(framework)
112+
113+
// Verify config shows enabled
114+
const configBefore = await axios.get('/config')
115+
assert.strictEqual(configBefore.data.codeOriginEnabled, true)
116+
assert.strictEqual(configBefore.data.remoteConfigEnabled, true)
117+
118+
// Step 2: Disable code origin via remote config
119+
await addRemoteConfigAndWaitForAck({ code_origin_enabled: false })
120+
121+
// Verify config shows disabled
122+
const configAfter = await axios.get('/config')
123+
assert.strictEqual(configAfter.data.codeOriginEnabled, false)
124+
assert.strictEqual(configAfter.data.remoteConfigEnabled, true)
125+
126+
// Step 3: Make another request and verify NO code origin tags
127+
// The tags are pre-computed and cached, but not applied since _enabled is false
128+
await assertCodeOriginAbsent(framework)
129+
})
130+
})
131+
132+
describe('CO enabled at boot, RC disabled', () => {
133+
beforeEach(setupApp(framework, {
134+
DD_CODE_ORIGIN_FOR_SPANS_ENABLED: 'true',
135+
DD_REMOTE_CONFIG_ENABLED: 'false'
136+
}))
137+
138+
it('should pre-compute and add code origin tags', async () => {
139+
// With CO enabled, tags should be computed and added normally
140+
await assertCodeOriginPresent(framework)
141+
})
142+
})
143+
144+
describe('RC enabled at boot, CO disabled', () => {
145+
beforeEach(setupApp(framework, {
146+
DD_CODE_ORIGIN_FOR_SPANS_ENABLED: 'false',
147+
DD_REMOTE_CONFIG_ENABLED: 'true'
148+
}))
149+
150+
it('should pre-compute but not add code origin tags, then add them after runtime enable', async () => {
151+
// Step 1: With RC enabled but CO disabled, tags are pre-computed (for potential runtime enabling)
152+
// but should NOT be applied to spans since CO is disabled
153+
await assertCodeOriginAbsent(framework)
154+
155+
// Verify config shows CO disabled
156+
const configBefore = await axios.get('/config')
157+
assert.strictEqual(configBefore.data.codeOriginEnabled, false)
158+
assert.strictEqual(configBefore.data.remoteConfigEnabled, true)
159+
160+
// Step 2: Enable code origin at runtime via remote config
161+
await addRemoteConfigAndWaitForAck({ code_origin_enabled: true })
162+
163+
// Verify config shows CO enabled
164+
const configAfter = await axios.get('/config')
165+
assert.strictEqual(configAfter.data.codeOriginEnabled, true)
166+
167+
// Step 3: Make another request and verify code origin tags ARE now present
168+
// This works because tags were pre-computed at boot when RC was enabled
169+
await assertCodeOriginPresent(framework)
170+
})
171+
})
172+
})
173+
})
174+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict'
2+
3+
// @ts-expect-error This code is running in a sandbox where dd-trace is available
4+
const tracer = require('dd-trace')
5+
6+
tracer.init({ flushInterval: 1 })
7+
8+
const express = require('express')
9+
const app = express()
10+
11+
app.get('/hello', (req, res) => {
12+
res.json({ message: 'Hello World' })
13+
})
14+
15+
app.get('/config', (req, res) => {
16+
const config = tracer._tracer._config
17+
res.json({
18+
codeOriginEnabled: config.codeOriginForSpans.enabled,
19+
remoteConfigEnabled: config.remoteConfig.enabled
20+
})
21+
})
22+
23+
const server = app.listen(process.env.APP_PORT || 0, () => {
24+
process.send?.({ port: (/** @type {import('net').AddressInfo} */ (server.address())).port })
25+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
3+
// @ts-expect-error This code is running in a sandbox where dd-trace is available
4+
const tracer = require('dd-trace')
5+
6+
tracer.init({ flushInterval: 1 })
7+
8+
// @ts-expect-error This code is running in a sandbox where fastify is available
9+
const fastify = require('fastify')
10+
const app = fastify()
11+
12+
app.get('/hello', (req, res) => {
13+
res.send({ message: 'Hello World' })
14+
})
15+
16+
app.get('/config', (req, res) => {
17+
const config = tracer._tracer._config
18+
res.send({
19+
codeOriginEnabled: config.codeOriginForSpans.enabled,
20+
remoteConfigEnabled: config.remoteConfig.enabled
21+
})
22+
})
23+
24+
app.listen({ port: process.env.APP_PORT || 0 }, (error) => {
25+
if (error) {
26+
throw error
27+
}
28+
process.send?.({ port: app.server.address().port })
29+
})

packages/datadog-plugin-express/src/code_origin.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const dc = require('dc-polyfill')
4+
35
const { entryTags } = require('../../datadog-code-origin')
46
const Plugin = require('../../dd-trace/src/plugins/plugin')
57
const web = require('../../dd-trace/src/plugins/util/web')
@@ -12,29 +14,33 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
1214

1315
const layerTags = new WeakMap()
1416

15-
this.addSub('apm:express:middleware:enter', ({ req, layer }) => {
17+
// Middleware/request handling: apply pre-computed tags to spans
18+
const handleMiddlewareEnter = ({ req, layer }) => {
1619
const tags = layerTags.get(layer)
1720
if (!tags) return
1821
web.getContext(req)?.span?.addTags(tags)
19-
})
20-
21-
this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => {
22-
if (!layer) return
23-
if (layerTags.has(layer)) return
24-
layerTags.set(layer, entryTags(topOfStackFunc))
25-
})
22+
}
2623

27-
this.addSub('apm:router:middleware:enter', ({ req, layer }) => {
28-
const tags = layerTags.get(layer)
29-
if (!tags) return
30-
web.getContext(req)?.span?.addTags(tags)
31-
})
24+
this.addSub('apm:express:middleware:enter', handleMiddlewareEnter)
25+
this.addSub('apm:router:middleware:enter', handleMiddlewareEnter)
3226

33-
this.addSub('apm:router:route:added', ({ topOfStackFunc, layer }) => {
27+
// Route added handling: compute and cache tags
28+
const handleRouteAdded = ({ topOfStackFunc, layer }) => {
3429
if (!layer) return
3530
if (layerTags.has(layer)) return
3631
layerTags.set(layer, entryTags(topOfStackFunc))
37-
})
32+
}
33+
34+
if (this._tracerConfig.remoteConfig?.enabled) {
35+
// When RC is enabled, use manual subscriptions (always pre-compute)
36+
// This allows tags to be computed even when CO is disabled, so runtime enabling works
37+
dc.channel('apm:express:route:added').subscribe(handleRouteAdded)
38+
dc.channel('apm:router:route:added').subscribe(handleRouteAdded)
39+
} else {
40+
// When RC is disabled, use addSub (only computes when CO is enabled)
41+
this.addSub('apm:express:route:added', handleRouteAdded)
42+
this.addSub('apm:router:route:added', handleRouteAdded)
43+
}
3844
}
3945
}
4046

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const dc = require('dc-polyfill')
4+
35
const { entryTags } = require('../../datadog-code-origin')
46
const Plugin = require('../../dd-trace/src/plugins/plugin')
57
const web = require('../../dd-trace/src/plugins/util/web')
@@ -18,11 +20,22 @@ class FastifyCodeOriginForSpansPlugin extends Plugin {
1820
web.getContext(req)?.span?.addTags(tags)
1921
})
2022

21-
this.addSub('apm:fastify:route:added', ({ routeOptions, onRoute }) => {
22-
if (!routeOptions.config) routeOptions.config = {}
23-
routeOptions.config[kCodeOriginForSpansTagsSym] = entryTags(onRoute)
24-
})
23+
if (this._tracerConfig.remoteConfig?.enabled) {
24+
// When RC is enabled, use manual subscription (always pre-computes)
25+
// This allows tags to be computed even when CO is disabled, so runtime enabling works
26+
dc.channel('apm:fastify:route:added').subscribe(handleRouteAdded)
27+
} else {
28+
// When RC is disabled, use addSub (only computes when CO is enabled)
29+
this.addSub('apm:fastify:route:added', handleRouteAdded)
30+
}
2531
}
2632
}
2733

2834
module.exports = FastifyCodeOriginForSpansPlugin
35+
36+
// Route added handling: compute and cache tags
37+
function handleRouteAdded ({ routeOptions, onRoute }) {
38+
if (!routeOptions.config) routeOptions.config = {}
39+
if (routeOptions.config[kCodeOriginForSpansTagsSym]) return
40+
routeOptions.config[kCodeOriginForSpansTagsSym] = entryTags(onRoute)
41+
}

0 commit comments

Comments
 (0)