Skip to content

Commit a42029e

Browse files
committed
[DI] Add support for loading probes from JSON file (#5941)
Add support for loading Dynamic Instrumentation / Live Debugger probes via a JSON file instead of Remote Configuration (RC). To use this feature, specify a path to the JSON file using either the environment variable `DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE` or the programmatic configuration `dynamicInstrumentation.probeFile`. This can be used in combination with RC. The JSON should be an array of probe objects in the same format as received via the RC `config` object, for example: [{ id: '100c9a5c-45ad-49dc-818b-c570d31e11d1', version: 0, type: 'LOG_PROBE', where: { sourceFile: 'index.js', lines: ['25'] }, template: 'Hello World', segments: [{ str: 'Hello World' }], captureSnapshot: true, capture: { maxReferenceDepth: 3 }, sampling: { snapshotsPerSecond: 100 } }]
1 parent 27b52ca commit a42029e

File tree

7 files changed

+96
-38
lines changed

7 files changed

+96
-38
lines changed

integration-tests/debugger/basic.spec.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict'
22

3+
const { writeFileSync } = require('fs')
34
const os = require('os')
5+
const { join } = require('path')
46

57
const { assert } = require('chai')
68
const { pollInterval, setup } = require('./utils')
@@ -188,7 +190,7 @@ describe('Dynamic Instrumentation', function () {
188190

189191
it(
190192
'should send expected error diagnostics messages if probe doesn\'t conform to expected schema',
191-
unsupporedOrInvalidProbesTest('bad config!!!', { status: 'ERROR' })
193+
unsupporedOrInvalidProbesTest({ invalid: 'config' }, { status: 'ERROR' })
192194
)
193195

194196
it(
@@ -493,10 +495,7 @@ describe('Dynamic Instrumentation', function () {
493495
})
494496

495497
describe('input messages', function () {
496-
it(
497-
'should capture and send expected payload when a log line probe is triggered',
498-
testBasicInputWithDD.bind(null, t)
499-
)
498+
it('should capture and send expected payload when a log line probe is triggered', testBasicInput.bind(null, t))
500499

501500
it('should respond with updated message if probe message is updated', function (done) {
502501
const expectedMessages = ['Hello World!', 'Hello Updated World!']
@@ -743,17 +742,26 @@ describe('Dynamic Instrumentation', function () {
743742
})
744743
})
745744

745+
describe('probe file', function () {
746+
const probeFile = join(os.tmpdir(), 'probes.json')
747+
const t = setup({
748+
env: { DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE: probeFile },
749+
dependencies: ['fastify']
750+
})
751+
const probe = t.generateProbeConfig()
752+
writeFileSync(probeFile, JSON.stringify([probe]))
753+
754+
it('should install probes from a probe file', testBasicInputWithoutRC.bind(null, t, probe))
755+
})
756+
746757
describe('DD_TRACING_ENABLED=true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=true', function () {
747758
const t = setup({
748759
env: { DD_TRACING_ENABLED: true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: true },
749760
dependencies: ['fastify']
750761
})
751762

752763
describe('input messages', function () {
753-
it(
754-
'should capture and send expected payload when a log line probe is triggered',
755-
testBasicInputWithDD.bind(null, t)
756-
)
764+
it('should capture and send expected payload when a log line probe is triggered', testBasicInput.bind(null, t))
757765
})
758766
})
759767

@@ -764,10 +772,7 @@ describe('Dynamic Instrumentation', function () {
764772
})
765773

766774
describe('input messages', function () {
767-
it(
768-
'should capture and send expected payload when a log line probe is triggered',
769-
testBasicInputWithDD.bind(null, t)
770-
)
775+
it('should capture and send expected payload when a log line probe is triggered', testBasicInput.bind(null, t))
771776
})
772777
})
773778

@@ -786,10 +791,19 @@ describe('Dynamic Instrumentation', function () {
786791
})
787792
})
788793

789-
function testBasicInputWithDD (t, done) {
790-
let traceId, spanId, dd
791-
794+
function testBasicInput (t, done) {
792795
t.triggerBreakpoint()
796+
setupAssertionListeners(t, done)
797+
t.agent.addRemoteConfig(t.rcConfig)
798+
}
799+
800+
function testBasicInputWithoutRC (t, probe, done) {
801+
t.triggerBreakpoint(false)
802+
setupAssertionListeners(t, done, probe)
803+
}
804+
805+
function setupAssertionListeners (t, done, probe) {
806+
let traceId, spanId, dd
793807

794808
t.agent.on('message', ({ payload }) => {
795809
const span = payload.find((arr) => arr[0].name === 'fastify.request')?.[0]
@@ -802,7 +816,7 @@ function testBasicInputWithDD (t, done) {
802816
})
803817

804818
t.agent.on('debugger-input', ({ payload }) => {
805-
assertBasicInputPayload(t, payload)
819+
assertBasicInputPayload(t, payload, probe)
806820

807821
payload = payload[0]
808822
assert.isObject(payload.dd)
@@ -816,8 +830,6 @@ function testBasicInputWithDD (t, done) {
816830
assertDD()
817831
})
818832

819-
t.agent.addRemoteConfig(t.rcConfig)
820-
821833
function assertDD () {
822834
if (!traceId || !spanId || !dd) return
823835
assert.strictEqual(dd.trace_id, traceId)
@@ -838,7 +850,7 @@ function testBasicInputWithoutDD (t, done) {
838850
t.agent.addRemoteConfig(t.rcConfig)
839851
}
840852

841-
function assertBasicInputPayload (t, payload) {
853+
function assertBasicInputPayload (t, payload, probe = t.rcConfig.config) {
842854
assert.isArray(payload)
843855
assert.lengthOf(payload, 1)
844856
payload = payload[0]
@@ -857,7 +869,7 @@ function assertBasicInputPayload (t, payload) {
857869
debugger: {
858870
snapshot: {
859871
probe: {
860-
id: t.rcConfig.config.id,
872+
id: probe.id,
861873
version: 0,
862874
location: { file: t.breakpoint.deployedFile, lines: [String(t.breakpoint.line)] }
863875
},

packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class TestVisDynamicInstrumentation {
6262

6363
log.debug('Starting Test Visibility - Dynamic Instrumentation client...')
6464

65-
const rcChannel = new MessageChannel() // mock channel
65+
const probeChannel = new MessageChannel() // mock channel
6666
const configChannel = new MessageChannel() // mock channel
6767

6868
this.worker = new Worker(
@@ -84,14 +84,14 @@ class TestVisDynamicInstrumentation {
8484
workerData: {
8585
config: this._config.serialize(),
8686
parentThreadId,
87-
rcPort: rcChannel.port1,
87+
probePort: probeChannel.port1,
8888
configPort: configChannel.port1,
8989
breakpointSetChannel: this.breakpointSetChannel.port1,
9090
breakpointHitChannel: this.breakpointHitChannel.port1,
9191
breakpointRemoveChannel: this.breakpointRemoveChannel.port1
9292
},
9393
transferList: [
94-
rcChannel.port1,
94+
probeChannel.port1,
9595
configChannel.port1,
9696
this.breakpointSetChannel.port1,
9797
this.breakpointHitChannel.port1,

packages/dd-trace/src/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ class Config {
516516
defaults['dogstatsd.port'] = '8125'
517517
defaults.dsmEnabled = false
518518
defaults['dynamicInstrumentation.enabled'] = false
519+
defaults['dynamicInstrumentation.probeFile'] = undefined
519520
defaults['dynamicInstrumentation.redactedIdentifiers'] = []
520521
defaults['dynamicInstrumentation.redactionExcludedIdentifiers'] = []
521522
defaults['dynamicInstrumentation.uploadIntervalSeconds'] = 1
@@ -707,6 +708,7 @@ class Config {
707708
DD_DOGSTATSD_HOST,
708709
DD_DOGSTATSD_PORT,
709710
DD_DYNAMIC_INSTRUMENTATION_ENABLED,
711+
DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE,
710712
DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS,
711713
DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS,
712714
DD_DYNAMIC_INSTRUMENTATION_UPLOAD_INTERVAL_SECONDS,
@@ -883,6 +885,7 @@ class Config {
883885
this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT)
884886
this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED)
885887
this._setBoolean(env, 'dynamicInstrumentation.enabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED)
888+
this._setString(env, 'dynamicInstrumentation.probeFile', DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE)
886889
this._setArray(env, 'dynamicInstrumentation.redactedIdentifiers', DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS)
887890
this._setArray(
888891
env,
@@ -1108,6 +1111,7 @@ class Config {
11081111
}
11091112
this._setBoolean(opts, 'dsmEnabled', options.dsmEnabled)
11101113
this._setBoolean(opts, 'dynamicInstrumentation.enabled', options.dynamicInstrumentation?.enabled)
1114+
this._setString(opts, 'dynamicInstrumentation.probeFile', options.dynamicInstrumentation?.probeFile)
11111115
this._setArray(
11121116
opts,
11131117
'dynamicInstrumentation.redactedIdentifiers',

packages/dd-trace/src/debugger/devtools_client/remote_config.js

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

3-
const { workerData: { rcPort } } = require('node:worker_threads')
3+
const { workerData: { probePort } } = require('node:worker_threads')
44
const { addBreakpoint, removeBreakpoint, modifyBreakpoint } = require('./breakpoints')
55
const { ackReceived, ackInstalled, ackError } = require('./status')
66
const log = require('../../log')
@@ -32,16 +32,19 @@ const log = require('../../log')
3232
// sampling: { snapshotsPerSecond: 5000 },
3333
// evaluateAt: 'EXIT' // only used for method probes
3434
// }
35-
rcPort.on('message', async ({ action, conf: probe, ackId }) => {
35+
probePort.on('message', async ({ action, probe, ackId }) => {
3636
try {
3737
await processMsg(action, probe)
38-
rcPort.postMessage({ ackId })
38+
probePort.postMessage({ ackId })
3939
} catch (err) {
40-
rcPort.postMessage({ ackId, error: err })
40+
probePort.postMessage({ ackId, error: err })
4141
ackError(err, probe)
4242
}
4343
})
44-
rcPort.on('messageerror', (err) => log.error('[debugger:devtools_client] received "messageerror" on RC port', err))
44+
probePort.on(
45+
'messageerror',
46+
(err) => log.error('[debugger:devtools_client] received "messageerror" on probe port', err)
47+
)
4548

4649
async function processMsg (action, probe) {
4750
log.debug(

packages/dd-trace/src/debugger/index.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const { readFile } = require('fs')
34
const { types } = require('util')
45
const { join } = require('path')
56
const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads')
@@ -23,17 +24,24 @@ function start (config, rc) {
2324
log.debug('[debugger] Starting Dynamic Instrumentation client...')
2425

2526
const rcAckCallbacks = new Map()
26-
const rcChannel = new MessageChannel()
27+
const probeChannel = new MessageChannel()
2728
configChannel = new MessageChannel()
2829

2930
process[Symbol.for('datadog:node:util:types')] = types
3031

31-
rc.setProductHandler('LIVE_DEBUGGING', (action, conf, id, ack) => {
32+
readProbeFile(config.dynamicInstrumentation.probeFile, (probes) => {
33+
const action = 'apply'
34+
for (const probe of probes) {
35+
probeChannel.port2.postMessage({ action, probe })
36+
}
37+
})
38+
39+
rc.setProductHandler('LIVE_DEBUGGING', (action, probe, id, ack) => {
3240
rcAckCallbacks.set(++ackId, ack)
33-
rcChannel.port2.postMessage({ action, conf, ackId })
41+
probeChannel.port2.postMessage({ action, probe, ackId })
3442
})
3543

36-
rcChannel.port2.on('message', ({ ackId, error }) => {
44+
probeChannel.port2.on('message', ({ ackId, error }) => {
3745
const ack = rcAckCallbacks.get(ackId)
3846
if (ack === undefined) {
3947
// This should never happen, but just in case something changes in the future, we should guard against it
@@ -44,7 +52,7 @@ function start (config, rc) {
4452
ack(error)
4553
rcAckCallbacks.delete(ackId)
4654
})
47-
rcChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on RC port', err))
55+
probeChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on probe port', err))
4856

4957
worker = new Worker(
5058
join(__dirname, 'devtools_client', 'index.js'),
@@ -54,10 +62,10 @@ function start (config, rc) {
5462
workerData: {
5563
config: config.serialize(),
5664
parentThreadId,
57-
rcPort: rcChannel.port1,
65+
probePort: probeChannel.port1,
5866
configPort: configChannel.port1
5967
},
60-
transferList: [rcChannel.port1, configChannel.port1]
68+
transferList: [probeChannel.port1, configChannel.port1]
6169
}
6270
)
6371

@@ -84,8 +92,8 @@ function start (config, rc) {
8492
})
8593

8694
worker.unref()
87-
rcChannel.port1.unref()
88-
rcChannel.port2.unref()
95+
probeChannel.port1.unref()
96+
probeChannel.port2.unref()
8997
configChannel.port1.unref()
9098
configChannel.port2.unref()
9199
}
@@ -94,3 +102,22 @@ function configure (config) {
94102
if (configChannel === null) return
95103
configChannel.port2.postMessage(config.serialize())
96104
}
105+
106+
function readProbeFile (path, cb) {
107+
if (!path) return
108+
109+
log.debug('[debugger] Reading probe file: %s', path)
110+
readFile(path, 'utf8', (err, data) => {
111+
if (err) {
112+
log.error('[debugger] Failed to read probe file: %s', path, err)
113+
return
114+
}
115+
try {
116+
const parsedData = JSON.parse(data)
117+
log.debug('[debugger] Successfully parsed probe file: %s', path)
118+
cb(parsedData)
119+
} catch (err) {
120+
log.error('[debugger] Probe file (%s) is not valid JSON', path, err)
121+
}
122+
})
123+
}

packages/dd-trace/src/supported-configurations.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"DD_DOGSTATSD_HOST": ["A"],
5757
"DD_DOGSTATSD_PORT": ["A"],
5858
"DD_DYNAMIC_INSTRUMENTATION_ENABLED": ["A"],
59+
"DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE": ["A"],
5960
"DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS": ["A"],
6061
"DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS": ["A"],
6162
"DD_DYNAMIC_INSTRUMENTATION_UPLOAD_INTERVAL_SECONDS": ["A"],

0 commit comments

Comments
 (0)