dd-trace
Version:
Datadog APM tracing client for JavaScript
237 lines (179 loc) • 7.45 kB
JavaScript
// TODO: Move traceIterator to Orchestrion.
const { parse, query, traverse } = require('./compiler')
const tracingChannelPredicate = (node) => (
node.specifiers?.[0]?.local?.name === 'tr_ch_apm_tracingChannel' ||
node.declarations?.[0]?.id?.properties?.[0]?.value?.name === 'tr_ch_apm_tracingChannel'
)
const transforms = module.exports = {
tracingChannelImport ({ dcModule, sourceType }, node) {
if (node.body.some(tracingChannelPredicate)) return
const index = node.body.findIndex(child => child.directive === 'use strict')
const code = sourceType === 'module'
? `import { tracingChannel as tr_ch_apm_tracingChannel } from "${dcModule}"`
: `const {tracingChannel: tr_ch_apm_tracingChannel} = require("${dcModule}")`
node.body.splice(index + 1, 0, parse(code, { sourceType }).body[0])
},
tracingChannelDeclaration (state, node) {
const { channelName, module: { name } } = state
const channelVariable = 'tr_ch_apm$' + channelName.replaceAll(':', '_')
if (node.body.some(child => child.declarations?.[0]?.id?.name === channelVariable)) return
transforms.tracingChannelImport(state, node)
const index = node.body.findIndex(tracingChannelPredicate)
const code = `
const ${channelVariable} = tr_ch_apm_tracingChannel("orchestrion:${name}:${channelName}")
`
node.body.splice(index + 1, 0, parse(code).body[0])
},
traceAsyncIterator: traceAny,
traceIterator: traceAny,
}
function traceAny (state, node, _parent, ancestry) {
const program = ancestry[ancestry.length - 1]
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
traceInstanceMethod(state, node, program)
} else {
traceFunction(state, node, program)
}
}
function traceFunction (state, node, program) {
transforms.tracingChannelDeclaration(state, program)
node.body = wrap(state, {
type: 'FunctionExpression',
params: node.params,
body: node.body,
async: node.async,
expression: false,
generator: node.generator,
}, program)
// The original function no longer contains any calls to `await` or `yield` as
// the function body is copied to the internal wrapped function, so we set
// these to false to avoid altering the return value of the wrapper. The old
// values are instead copied to the new AST node above.
node.generator = false
node.async = false
wrapSuper(state, node)
}
function traceInstanceMethod (state, node, program) {
const { functionQuery, operator } = state
const { methodName } = functionQuery
const classBody = node.body
// If the method exists on the class, we return as it will be patched later
// while traversing child nodes later on.
if (classBody.body.some(({ key }) => key.name === methodName)) return
// Method doesn't exist on the class so we assume an instance method and
// wrap it in the constructor instead.
let ctor = classBody.body.find(({ kind }) => kind === 'constructor')
transforms.tracingChannelDeclaration(state, program)
if (!ctor) {
ctor = parse(
node.superClass
? 'class A { constructor (...args) { super(...args) } }'
: 'class A { constructor () {} }'
).body[0].body.body[0] // Extract constructor from dummy class body.
classBody.body.unshift(ctor)
}
const ctorBody = parse(`
const __apm$${methodName} = this["${methodName}"]
this["${methodName}"] = function () {}
`).body
// Extract only right-hand side function of line 2.
const fn = ctorBody[1].expression.right
fn.async = operator === 'tracePromise'
fn.body = wrap(state, { type: 'Identifier', name: `__apm$${methodName}` }, program)
wrapSuper(state, fn)
ctor.value.body.body.push(...ctorBody)
}
function wrap (state, node, program) {
const { operator } = state
if (operator === 'traceAsyncIterator') return wrapIterator(state, node, program)
if (operator === 'traceIterator') return wrapIterator(state, node, program)
}
function wrapSuper (_state, node) {
const members = new Set()
traverse(
node.body,
'[object.type=Super]',
(node, parent) => {
const { name } = node.property
let child
if (parent.callee) {
// This is needed because for generator functions we have to move the
// original function to a nested wrapped function, but we can't use an
// arrow function because arrow function cannot be generator functions,
// and `super` cannot be called from a nested function, so we have to
// rewrite any `super` call to not use the keyword.
const { expression } = parse(`__apm$super['${name}'].call(this)`).body[0]
parent.callee = child = expression.callee
parent.arguments.unshift(...expression.arguments)
} else {
parent.expression = child = parse(`__apm$super['${name}']`).body[0]
}
child.computed = parent.callee.computed
child.optional = parent.callee.optional
members.add(name)
}
)
for (const name of members) {
const member = parse(`
class Wrapper {
wrapper () {
__apm$super['${name}'] = super['${name}']
}
}
`).body[0].body.body[0].value.body.body[0]
node.body.body.unshift(member)
}
if (members.size > 0) {
node.body.body.unshift(parse('const __apm$super = {}').body[0])
}
}
function wrapIterator (state, node, program) {
const { channelName, operator } = state
const baseChannel = channelName.replaceAll(':', '_')
const channelVariable = 'tr_ch_apm$' + baseChannel
const nextChannel = baseChannel + '_next'
const traceMethod = operator === 'traceAsyncIterator' ? 'tracePromise' : 'traceSync'
const traceNext = `tr_ch_apm$${nextChannel}.${traceMethod}`
transforms.tracingChannelDeclaration({ ...state, channelName: nextChannel }, program)
const wrapper = parse(`
function wrapper () {
const __apm$traced = () => {
const __apm$wrapped = () => {};
return __apm$wrapped.apply(this, arguments);
};
if (!${channelVariable}.start.hasSubscribers) return __apm$traced();
{
const wrap = iter => {
const { next: iterNext, return: iterReturn, throw: iterThrow } = iter;
iter.next = (...args) => ${traceNext}(iterNext, ctx, iter, ...args);
iter.return = (...args) => ${traceNext}(iterReturn, ctx, iter, ...args);
iter.throw = (...args) => ${traceNext}(iterThrow, ctx, iter, ...args);
return iter;
};
const ctx = {
arguments,
self: this,
moduleVersion: "1.0.0"
};
const iter = ${channelVariable}.traceSync(__apm$traced, ctx);
if (typeof iter.then !== 'function') return wrap(iter);
return iter.then(result => {
ctx.result = result;
${channelVariable}.asyncStart.publish(ctx);
${channelVariable}.asyncEnd.publish(ctx);
return wrap(result);
}, err => {
ctx.error = err;
${channelVariable}.error.publish(ctx);
${channelVariable}.asyncStart.publish(ctx);
${channelVariable}.asyncEnd.publish(ctx);
return Promise.reject(err);
});
};
}
`).body[0].body // Extract only block statement of function body.
// Replace the right-hand side assignment of `const __apm$wrapped = () => {}`.
query(wrapper, '[id.name=__apm$wrapped]')[0].init = node
return wrapper
}