dd-trace
Version:
Datadog APM tracing client for JavaScript
265 lines (225 loc) • 7.98 kB
JavaScript
const shimmer = require('../../datadog-shimmer')
const { channel, addHook } = require('./helpers/instrument')
const patchedClientConfigProtocols = new WeakSet()
function wrapRequest (send) {
return function wrappedRequest (cb) {
if (!this.service) return send.apply(this, arguments)
const serviceIdentifier = this.service.serviceIdentifier
const channelSuffix = getChannelSuffix(serviceIdentifier)
const startCh = channel(`apm:aws:request:start:${channelSuffix}`)
if (!startCh.hasSubscribers) return send.apply(this, arguments)
const ctx = {
serviceIdentifier,
operation: this.operation,
awsRegion: this.service.config && this.service.config.region,
awsService: this.service.api && this.service.api.className,
request: this,
cbExists: typeof cb === 'function'
}
this.on('complete', response => {
ctx.response = response
channel(`apm:aws:request:complete:${channelSuffix}`).publish(ctx)
})
if (ctx.cbExists) {
arguments[0] = wrapCb(cb, channelSuffix, ctx)
}
return startCh.runStores(ctx, send, this, ...arguments)
}
}
function wrapDeserialize (deserialize, channelSuffix, responseIndex = 0) {
const headersCh = channel(`apm:aws:response:deserialize:${channelSuffix}`)
return function () {
const response = arguments[responseIndex]
if (headersCh.hasSubscribers) {
headersCh.publish({ headers: response.headers })
}
return deserialize.apply(this, arguments)
}
}
function wrapSmithySend (send) {
return function (command, ...args) {
const cb = args.at(-1)
const serviceIdentifier = this.config.serviceId.toLowerCase()
const channelSuffix = getChannelSuffix(serviceIdentifier)
const commandName = command.constructor.name
const clientName = this.constructor.name.replace(/Client$/, '')
const operation = `${commandName[0].toLowerCase()}${commandName.slice(1).replace(/Command$/, '')}`
const request = {
operation,
params: command.input
}
const startCh = channel(`apm:aws:request:start:${channelSuffix}`)
const regionCh = channel(`apm:aws:request:region:${channelSuffix}`)
const responseStartChannel = channel(`apm:aws:response:start:${channelSuffix}`)
const responseFinishChannel = channel(`apm:aws:response:finish:${channelSuffix}`)
if (typeof command.deserialize === 'function') {
shimmer.wrap(command, 'deserialize', deserialize => wrapDeserialize(deserialize, channelSuffix))
} else if (this.config?.protocol?.deserializeResponse && !patchedClientConfigProtocols.has(this.config.protocol)) {
shimmer.wrap(
this.config.protocol,
'deserializeResponse',
deserializeResponse => wrapDeserialize(deserializeResponse, channelSuffix, 2)
)
patchedClientConfigProtocols.add(this.config.protocol)
}
const ctx = {
serviceIdentifier,
operation,
awsService: clientName,
request
}
return startCh.runStores(ctx, () => {
// When the region is not set this never resolves so we can't await.
this.config.region().then(region => {
ctx.region = region
regionCh.publish(ctx)
})
if (typeof cb === 'function') {
args[args.length - 1] = shimmer.wrapFunction(cb, cb => function (err, result) {
addResponse(ctx, err, result)
handleCompletion(result, ctx, channelSuffix)
const responseCtx = { request, response: ctx.response }
responseStartChannel.runStores(responseCtx, () => {
cb.apply(this, arguments)
responseFinishChannel.publish(responseCtx)
})
})
} else { // always a promise
return send.call(this, command, ...args)
.then(
result => {
addResponse(ctx, null, result)
handleCompletion(result, ctx, channelSuffix)
return result
},
error => {
addResponse(ctx, error)
handleCompletion(null, ctx, channelSuffix)
throw error
}
)
}
return send.call(this, command, ...args)
})
}
}
function handleCompletion (result, ctx, channelSuffix) {
const completeChannel = channel(`apm:aws:request:complete:${channelSuffix}`)
const streamedChunkChannel = channel(`apm:aws:response:streamed-chunk:${channelSuffix}`)
const iterator = result?.body?.[Symbol.asyncIterator]
if (!iterator) {
completeChannel.publish(ctx)
return
}
shimmer.wrap(result.body, Symbol.asyncIterator, function (asyncIterator) {
return function () {
const iterator = asyncIterator.apply(this, arguments)
shimmer.wrap(iterator, 'next', function (next) {
return function () {
return next.apply(this, arguments)
.then(result => {
const { done, value: chunk } = result
streamedChunkChannel.publish({ ctx, chunk, done })
if (done) {
completeChannel.publish(ctx)
}
return result
})
.catch(err => {
addResponse(ctx, err)
completeChannel.publish(ctx)
throw err
})
}
})
return iterator
}
})
}
function wrapCb (cb, serviceName, ctx) {
// eslint-disable-next-line n/handle-callback-err
return shimmer.wrapFunction(cb, cb => function wrappedCb (err, response) {
ctx = { request: ctx.request, response }
return channel(`apm:aws:response:start:${serviceName}`).runStores(ctx, () => {
const finishChannel = channel(`apm:aws:response:finish:${serviceName}`)
try {
let result = cb.apply(this, arguments)
if (result && result.then) {
result = result.then(x => {
finishChannel.publish(ctx)
return x
}, e => {
ctx.error = e
finishChannel.publish(ctx)
throw e
})
} else {
finishChannel.publish(ctx)
}
return result
} catch (e) {
ctx.error = e
finishChannel.publish(ctx)
throw e
}
})
})
}
function addResponse (ctx, error, result) {
const request = ctx.request
const response = { request, error, ...result }
if (result && result.$metadata) {
response.requestId = result.$metadata.requestId
}
ctx.response = response
}
function getChannelSuffix (name) {
// some resource identifiers have spaces between ex: bedrock runtime
name = String(name).replaceAll(' ', '')
return [
'cloudwatchlogs',
'dynamodb',
'eventbridge',
'kinesis',
'lambda',
'redshift',
's3',
'sfn',
'sns',
'sqs',
'states',
'stepfunctions',
'bedrockruntime'
].includes(name)
? name
: 'default'
}
addHook({ name: '@smithy/smithy-client', versions: ['>=1.0.3'] }, smithy => {
shimmer.wrap(smithy.Client.prototype, 'send', wrapSmithySend)
return smithy
})
addHook({ name: '@aws-sdk/smithy-client', versions: ['>=3'] }, smithy => {
shimmer.wrap(smithy.Client.prototype, 'send', wrapSmithySend)
return smithy
})
addHook({ name: 'aws-sdk', versions: ['>=2.3.0'] }, AWS => {
shimmer.wrap(AWS.config, 'setPromisesDependency', setPromisesDependency => {
return function wrappedSetPromisesDependency (dep) {
const result = setPromisesDependency.apply(this, arguments)
shimmer.wrap(AWS.Request.prototype, 'promise', wrapRequest)
return result
}
})
return AWS
})
addHook({ name: 'aws-sdk', file: 'lib/core.js', versions: ['>=2.3.0'] }, AWS => {
shimmer.wrap(AWS.Request.prototype, 'promise', wrapRequest)
return AWS
})
// <2.1.35 has breaking changes for instrumentation
// https://github.com/aws/aws-sdk-js/pull/629
addHook({ name: 'aws-sdk', file: 'lib/core.js', versions: ['>=2.1.35'] }, AWS => {
shimmer.wrap(AWS.Request.prototype, 'send', wrapRequest)
return AWS
})