lib/instrumentation/modules/@smithy/smithy-client.js (139 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
const semver = require('semver');
const shimmer = require('../../shimmer');
const elasticAPMMiddlewares = Symbol('elasticAPMMiddlewares');
const {
DYNAMODB_NAME,
DYNAMODB_TYPE,
DYNAMODB_SUBTYPE,
dynamoDBMiddlewareFactory,
dynamoDBShouldIgnoreCommand,
} = require('../@aws-sdk/client-dynamodb');
const {
S3_NAME,
S3_TYPE,
S3_SUBTYPE,
s3MiddlewareFactory,
s3ShouldIgnoreCommand,
} = require('../@aws-sdk/client-s3');
const {
SNS_NAME,
SNS_TYPE,
SNS_SUBTYPE,
snsMiddlewareFactory,
snsShouldIgnoreCommand,
} = require('../@aws-sdk/client-sns');
const {
SQS_NAME,
SQS_TYPE,
SQS_SUBTYPE,
sqsMiddlewareFactory,
sqsShouldIgnoreCommand,
} = require('../@aws-sdk/client-sqs');
/**
* We do alias them to a local type
* @typedef {import('@aws-sdk/types').InitializeMiddleware} InitializeMiddleware
* @typedef {import('@aws-sdk/types').FinalizeRequestMiddleware } FinalizeRequestMiddleware
* @typedef {import('@aws-sdk/types').InitializeHandlerOptions} InitializeHandlerOptions
* @typedef {import('@aws-sdk/types').FinalizeRequestHandlerOptions } FinalizeRequestHandlerOptions
*
* Then create our types
* @typedef {InitializeMiddleware | FinalizeRequestMiddleware} AWSMiddleware
* @typedef {InitializeHandlerOptions | FinalizeRequestHandlerOptions} AWSMiddlewareOptions
* @typedef {object} AWSMiddlewareEntry
* @property {AWSMiddleware} middleware
* @property {AWSMiddlewareOptions} options
*/
const COMMAND_NAME_RE = /^(\w+)Command$/;
/**
* TODO: this method may be shared with other instrumentations
* For a HeadObject API call, `context.commandName === 'HeadObjectCommand'`.
*
* @param {String} commandName
* @returns {String}
*/
function opNameFromCommandName(commandName) {
const match = COMMAND_NAME_RE.exec(commandName);
if (match) {
return match[1];
} else {
return '<unknown command>';
}
}
const clientsConfig = {
DynamoDBClient: {
NAME: DYNAMODB_NAME,
TYPE: DYNAMODB_TYPE,
SUBTYPE: DYNAMODB_SUBTYPE,
factory: dynamoDBMiddlewareFactory,
shouldIgnoreCommand: dynamoDBShouldIgnoreCommand,
},
S3Client: {
NAME: S3_NAME,
TYPE: S3_TYPE,
SUBTYPE: S3_SUBTYPE,
factory: s3MiddlewareFactory,
shouldIgnoreCommand: s3ShouldIgnoreCommand,
},
SNSClient: {
NAME: SNS_NAME,
TYPE: SNS_TYPE,
SUBTYPE: SNS_SUBTYPE,
factory: snsMiddlewareFactory,
shouldIgnoreCommand: snsShouldIgnoreCommand,
},
SQSClient: {
NAME: SQS_NAME,
TYPE: SQS_TYPE,
SUBTYPE: SQS_SUBTYPE,
factory: sqsMiddlewareFactory,
shouldIgnoreCommand: sqsShouldIgnoreCommand,
},
};
module.exports = function (mod, agent, { name, version, enabled }) {
if (!enabled) return mod;
// As of `@aws-sdk/*@3.363.0` the underlying smithy-client is under the
// `@smithy/` npm org.
if (
name === '@smithy/smithy-client' &&
!semver.satisfies(version, '>=1 <5')
) {
agent.logger.debug(
'cannot instrument @aws-sdk/client-*: @smithy/smithy-client version %s not supported',
version,
);
return mod;
} else if (
name === '@aws-sdk/smithy-client' &&
!semver.satisfies(version, '>=3 <4')
) {
agent.logger.debug(
'cannot instrument @aws-sdk/client-*: @aws-sdk/smithy-client version %s not supported',
version,
);
return mod;
}
shimmer.wrap(mod.Client.prototype, 'send', function (orig) {
return function _wrappedSmithyClientSend() {
const clientName = this.constructor && this.constructor.name;
const clientConfig = clientsConfig[clientName];
if (!clientConfig) {
return orig.apply(this, arguments);
}
if (!this[elasticAPMMiddlewares]) {
const factory = clientConfig && clientConfig.factory;
const middlewares =
typeof factory === 'function' ? factory(this, agent) : [];
// We do the instrumentation by leveraging the middleware mechanism provided by the
// middlewareStack property of the Client instance. We add the instrumentation middlewares
// once at the client level so they persist for the whole life of the client instance
// https://github.com/aws/aws-sdk-js-v3/tree/main/packages/middleware-stack
this[elasticAPMMiddlewares] = middlewares;
for (const item of this[elasticAPMMiddlewares]) {
this.middlewareStack.add(item.middleware, item.options);
}
}
const command = arguments[0];
if (clientConfig.shouldIgnoreCommand(command, agent._conf)) {
return orig.apply(this, arguments);
}
const opName = opNameFromCommandName(command.constructor.name);
const name = clientConfig.NAME + ' ' + opName;
const ins = agent._instrumentation;
const span = ins.createSpan(
name,
clientConfig.TYPE,
clientConfig.SUBTYPE,
opName,
{ exitSpan: true },
);
if (!span) {
return orig.apply(this, arguments);
}
// Run context notes: The `orig` should run in the context of the S3 span,
// because that is the point. The user's callback `cb` should run outside of
// the S3 span.
const parentRunContext = ins.currRunContext();
const spanRunContext = parentRunContext.enterSpan(span);
// Although the client consumer may use the Promise API `S3Client.send(command).then(...)`
// the clients may make use of the callback parameter on the super class method (SmithyClient)
// therefore we need to have this check
const cb = arguments[arguments.length - 1];
if (typeof cb === 'function') {
arguments[arguments.length - 1] = ins.bindFunctionToRunContext(
parentRunContext,
cb,
);
}
return ins.withRunContext(spanRunContext, orig, this, ...arguments);
};
});
return mod;
};