dd-trace
Version:
Datadog APM tracing client for JavaScript
134 lines (111 loc) • 4.3 kB
JavaScript
const { readFile } = require('fs')
const { types } = require('util')
const { join } = require('path')
const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads')
const getDebuggerConfig = require('./config')
const log = require('../log')
let worker = null
let configChannel = null
let ackId = 0
// eslint-disable-next-line eslint-rules/eslint-process-env
const { NODE_OPTIONS, ...env } = process.env
module.exports = {
start,
configure
}
function start (config, rc) {
if (worker !== null) return
log.debug('[debugger] Starting Dynamic Instrumentation client...')
const rcAckCallbacks = new Map()
const probeChannel = new MessageChannel()
const logChannel = new MessageChannel()
configChannel = new MessageChannel()
process[Symbol.for('datadog:node:util:types')] = types
readProbeFile(config.dynamicInstrumentation.probeFile, (probes) => {
const action = 'apply'
for (const probe of probes) {
probeChannel.port2.postMessage({ action, probe })
}
})
rc.setProductHandler('LIVE_DEBUGGING', (action, probe, id, ack) => {
rcAckCallbacks.set(++ackId, ack)
probeChannel.port2.postMessage({ action, probe, ackId })
})
probeChannel.port2.on('message', ({ ackId, error }) => {
const ack = rcAckCallbacks.get(ackId)
if (ack === undefined) {
// This should never happen, but just in case something changes in the future, we should guard against it
log.error('[debugger] Received an unknown ackId: %s', ackId)
if (error) log.error('[debugger] Error starting Dynamic Instrumentation client', error)
return
}
ack(error)
rcAckCallbacks.delete(ackId)
})
probeChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on probe port', err))
logChannel.port2.on('message', ({ level, args }) => {
log[level](...args)
})
logChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on log port', err))
worker = new Worker(
join(__dirname, 'devtools_client', 'index.js'),
{
execArgv: [], // Avoid worker thread inheriting the `-r` command line argument
env, // Avoid worker thread inheriting the `NODE_OPTIONS` environment variable (in case it contains `-r`)
workerData: {
config: getDebuggerConfig(config),
parentThreadId,
probePort: probeChannel.port1,
logPort: logChannel.port1,
configPort: configChannel.port1
},
transferList: [probeChannel.port1, logChannel.port1, configChannel.port1]
}
)
worker.on('online', () => {
log.debug('[debugger] Dynamic Instrumentation worker thread started successfully (thread id: %d)', worker.threadId)
})
worker.on('error', (err) => log.error('[debugger] worker thread error', err))
worker.on('messageerror', (err) => log.error('[debugger] received "messageerror" from worker', err))
worker.on('exit', (code) => {
const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`)
log.error('[debugger] worker thread exited unexpectedly', error)
// Be nice, clean up now that the worker thread encounted an issue and we can't continue
rc.removeProductHandler('LIVE_DEBUGGING')
worker.removeAllListeners()
configChannel = null
for (const ackId of rcAckCallbacks.keys()) {
rcAckCallbacks.get(ackId)(error)
rcAckCallbacks.delete(ackId)
}
})
worker.unref()
probeChannel.port1.unref()
probeChannel.port2.unref()
logChannel.port1.unref()
logChannel.port2.unref()
configChannel.port1.unref()
configChannel.port2.unref()
}
function configure (config) {
if (configChannel === null) return
configChannel.port2.postMessage(getDebuggerConfig(config))
}
function readProbeFile (path, cb) {
if (!path) return
log.debug('[debugger] Reading probe file: %s', path)
readFile(path, 'utf8', (err, data) => {
if (err) {
log.error('[debugger] Failed to read probe file: %s', path, err)
return
}
try {
const parsedData = JSON.parse(data)
log.debug('[debugger] Successfully parsed probe file: %s', path)
cb(parsedData)
} catch (err) {
log.error('[debugger] Probe file (%s) is not valid JSON', path, err)
}
})
}