dd-trace
Version:
Datadog APM tracing client for JavaScript
275 lines (214 loc) • 7.88 kB
JavaScript
const { addHook, channel } = require('../helpers/instrument')
const shimmer = require('../../../datadog-shimmer')
const types = require('./types')
const patched = new WeakSet()
const instances = new WeakMap()
const startChannel = channel('apm:grpc:client:request:start')
const asyncStartChannel = channel('apm:grpc:client:request:asyncStart')
const errorChannel = channel('apm:grpc:client:request:error')
const finishChannel = channel('apm:grpc:client:request:finish')
const emitChannel = channel('apm:grpc:client:request:emit')
function createWrapMakeRequest (type, hasPeer = false) {
const metadataIndex = type === types.client_stream || type === types.bidi ? 3 : 4
return function wrapMakeRequest (makeRequest) {
return function (path) {
if (!startChannel.hasSubscribers) return makeRequest.apply(this, arguments)
const { metadata, args } = resolveMetadata(this, arguments, metadataIndex)
return callMethod(this, makeRequest, args, path, metadata, type, hasPeer)
}
}
}
function createWrapLoadPackageDefinition (hasPeer = false) {
return function wrapLoadPackageDefinition (loadPackageDefinition) {
return function (packageDef) {
const result = loadPackageDefinition.apply(this, arguments)
if (!result) return result
wrapPackageDefinition(result, hasPeer)
return result
}
}
}
function createWrapMakeClientConstructor (hasPeer = false) {
return function wrapMakeClientConstructor (makeClientConstructor) {
return function (methods) {
const ServiceClient = makeClientConstructor.apply(this, arguments)
wrapClientConstructor(ServiceClient, methods, hasPeer)
return ServiceClient
}
}
}
function wrapPackageDefinition (def, hasPeer = false) {
for (const name in def) {
if (def[name].format) continue
if (def[name].service && def[name].prototype) {
wrapClientConstructor(def[name], def[name].service, hasPeer)
} else {
wrapPackageDefinition(def[name], hasPeer)
}
}
}
function wrapClientConstructor (ServiceClient, methods, hasPeer = false) {
const proto = ServiceClient.prototype
if (typeof methods !== 'object' || 'format' in methods) return
for (const [name, method] of Object.entries(methods)) {
if (!method) continue
const { originalName, path } = method
const type = getType(method)
proto[name] = wrapMethod(proto[name], path, type, hasPeer)
if (originalName) {
proto[originalName] = wrapMethod(proto[originalName], path, type, hasPeer)
}
}
}
function wrapMethod (method, path, type, hasPeer) {
if (typeof method !== 'function' || patched.has(method)) {
return method
}
const metadataIndex = type === types.client_stream || type === types.bidi ? 0 : 1
const wrapped = shimmer.wrapFunction(method, method => function () {
if (!startChannel.hasSubscribers) return method.apply(this, arguments)
const { metadata, args } = resolveMetadata(this, arguments, metadataIndex)
return callMethod(this, method, args, path, metadata, type, hasPeer)
})
patched.add(wrapped)
return wrapped
}
function wrapCallback (ctx, callback = () => {}) {
return shimmer.wrapFunction(callback, callback => function (err) {
if (err) {
ctx.error = err
errorChannel.publish(ctx)
}
return asyncStartChannel.runStores(ctx, () => {
return callback.apply(this, arguments)
// No async end channel needed
})
})
}
const onStatusWithPeer = function (ctx, arg1, thisArg) {
ctx.result = arg1
ctx.peer = thisArg.getPeer()
finishChannel.publish(ctx)
}
const onStatusWithoutPeer = function (ctx, arg1) {
ctx.result = arg1
finishChannel.publish(ctx)
}
function createWrapEmit (ctx, hasPeer = false) {
const onStatus = hasPeer ? onStatusWithPeer : onStatusWithoutPeer
return function wrapEmit (emit) {
return function (event, arg1) {
switch (event) {
case 'error':
ctx.error = arg1
errorChannel.publish(ctx)
break
case 'status':
onStatus(ctx, arg1, this)
break
}
return emitChannel.runStores(ctx, () => {
return emit.apply(this, arguments)
})
}
}
}
function callMethod (client, method, args, path, metadata, type, hasPeer = false) {
const ctx = { metadata, path, type }
return startChannel.runStores(ctx, () => {
try {
let callArgs = args
if (type === types.unary || type === types.client_stream) {
if (!Array.isArray(callArgs)) callArgs = [...callArgs]
const length = callArgs.length
const callback = callArgs[length - 1]
if (typeof callback === 'function') {
callArgs[length - 1] = wrapCallback(ctx, callback)
} else {
callArgs[length] = wrapCallback(ctx)
}
}
const call = method.apply(client, callArgs)
if (call && typeof call.emit === 'function') {
shimmer.wrap(call, 'emit', createWrapEmit(ctx, hasPeer))
}
return call
} catch (e) {
ctx.error = e
errorChannel.publish(ctx)
throw e
}
// No end channel needed
})
}
/**
* Returns the `Metadata` for a gRPC client invocation, splicing or replacing
* one at `index` when the user did not pass their own.
*
* @param {object} client
* @param {ArrayLike<unknown>} args
* @param {number} index
* @returns {{ metadata: object | undefined, args: ArrayLike<unknown> }}
*/
function resolveMetadata (client, args, index) {
const grpc = client && getGrpc(client)
if (!grpc) return { metadata: undefined, args }
const slot = args[index]
if (slot instanceof grpc.Metadata || slot?.constructor?.name === 'Metadata') {
return { metadata: slot, args }
}
const metadata = new grpc.Metadata()
if (slot == null) {
const out = [...args]
out[index] = metadata
return { metadata, args: out }
}
const out = new Array(args.length + 1)
for (let i = 0; i < index; i++) out[i] = args[i]
out[index] = metadata
for (let i = index; i < args.length; i++) out[i + 1] = args[i]
return { metadata, args: out }
}
function getType (definition) {
if (definition.requestStream) {
if (definition.responseStream) {
return types.bidi
}
return types.client_stream
}
if (definition.responseStream) {
return types.server_stream
}
return types.unary
}
function getGrpc (client) {
let proto = client
do {
const instance = instances.get(proto)
if (instance) return instance
} while ((proto = Object.getPrototypeOf(proto)))
}
function patch (hasPeer = false) {
return function patch (grpc) {
const proto = grpc.Client.prototype
instances.set(proto, grpc)
shimmer.wrap(proto, 'makeBidiStreamRequest', createWrapMakeRequest(types.bidi, hasPeer))
shimmer.wrap(proto, 'makeClientStreamRequest', createWrapMakeRequest(types.clientStream, hasPeer))
shimmer.wrap(proto, 'makeServerStreamRequest', createWrapMakeRequest(types.serverStream, hasPeer))
shimmer.wrap(proto, 'makeUnaryRequest', createWrapMakeRequest(types.unary, hasPeer))
return grpc
}
}
addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3 <1.1.4'] }, patch(false))
addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3 <1.1.4'], file: 'build/src/make-client.js' }, client => {
shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor(false))
shimmer.wrap(client, 'loadPackageDefinition', createWrapLoadPackageDefinition(false))
return client
})
addHook({ name: '@grpc/grpc-js', versions: ['>=1.1.4'] }, patch(true))
addHook({ name: '@grpc/grpc-js', versions: ['>=1.1.4'], file: 'build/src/make-client.js' }, client => {
shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor(true))
shimmer.wrap(client, 'loadPackageDefinition', createWrapLoadPackageDefinition(true))
return client
})