@aws/aws-distro-opentelemetry-node-autoinstrumentation
Version:
This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.
405 lines (403 loc) • 23.9 kB
JavaScript
;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.SKIP_CREDENTIAL_CAPTURE_KEY = exports.customExtractor = exports.applyInstrumentationPatches = exports.headerGetter = exports.AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = exports.traceContextEnvironmentKey = void 0;
const api_1 = require("@opentelemetry/api");
const propagator_aws_xray_1 = require("@opentelemetry/propagator-aws-xray");
const aws_attribute_keys_1 = require("../aws-attribute-keys");
const bedrock_1 = require("./aws/services/bedrock");
const secretsmanager_1 = require("./aws/services/secretsmanager");
const step_functions_1 = require("./aws/services/step-functions");
const core_1 = require("@opentelemetry/core");
exports.traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
exports.AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id';
exports.headerGetter = {
keys(carrier) {
return Object.keys(carrier);
},
get(carrier, key) {
return carrier[key];
},
};
function applyInstrumentationPatches(instrumentations) {
/*
Apply patches to upstream instrumentation libraries.
This method is invoked to apply changes to upstream instrumentation libraries, typically when changes to upstream
are required on a timeline that cannot wait for upstream release. Generally speaking, patches should be short-term
local solutions that are comparable to long-term upstream solutions.
Where possible, automated testing should be run to catch upstream changes resulting in broken patches
*/
instrumentations.forEach((instrumentation, index) => {
var _a;
if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-sdk') {
api_1.diag.debug('Patching aws sdk instrumentation');
patchAwsSdkInstrumentation(instrumentation);
// Access private property servicesExtensions of AwsInstrumentation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const services = (_a = instrumentations[index].servicesExtensions) === null || _a === void 0 ? void 0 : _a.services;
if (services) {
services.set('SecretsManager', new secretsmanager_1.SecretsManagerServiceExtension());
services.set('SFN', new step_functions_1.StepFunctionsServiceExtension());
services.set('Bedrock', new bedrock_1.BedrockServiceExtension());
services.set('BedrockAgent', new bedrock_1.BedrockAgentServiceExtension());
services.set('BedrockAgentRuntime', new bedrock_1.BedrockAgentRuntimeServiceExtension());
services.set('BedrockRuntime', new bedrock_1.BedrockRuntimeServiceExtension());
patchSqsServiceExtension(services.get('SQS'));
patchSnsServiceExtension(services.get('SNS'));
patchLambdaServiceExtension(services.get('Lambda'));
patchKinesisServiceExtension(services.get('Kinesis'));
patchDynamoDbServiceExtension(services.get('DynamoDB'));
}
}
else if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-lambda') {
api_1.diag.debug('Patching aws lambda instrumentation');
patchAwsLambdaInstrumentation(instrumentation);
}
});
}
exports.applyInstrumentationPatches = applyInstrumentationPatches;
/*
* This function `customExtractor` is used to extract SpanContext for AWS Lambda functions.
* It extracts the X-Ray trace ID from the Lambda context (xRayTraceId) or environment variable (_X_AMZN_TRACE_ID) into the event headers
* It then uses OpenTelemetry global propagator to extract trace context from the event headers
* If a valid span context is extracted, it returns that context; otherwise, it returns the root context.
*/
const lambdaContextXrayTraceIdKey = 'xRayTraceId';
const customExtractor = (event, _handlerContext) => {
var _a;
const xrayTraceIdFromLambdaContext = _handlerContext
? _handlerContext[lambdaContextXrayTraceIdKey]
: undefined;
const xrayTraceIdFromLambdaEnv = process.env[exports.traceContextEnvironmentKey];
const xrayTraceIdFromLambda = xrayTraceIdFromLambdaContext || xrayTraceIdFromLambdaEnv;
const httpHeaders = event.headers || {};
if (xrayTraceIdFromLambda) {
// Delete any X-Ray Trace ID via case-insensitive checks since we will overwrite it here.
Object.keys(httpHeaders).forEach(key => {
if (key.toLowerCase() === propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER.toLowerCase()) {
delete httpHeaders[key];
}
});
httpHeaders[propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER] = xrayTraceIdFromLambda;
}
const extractedContext = api_1.propagation.extract(api_1.context.active(), httpHeaders, exports.headerGetter);
if ((_a = api_1.trace.getSpan(extractedContext)) === null || _a === void 0 ? void 0 : _a.spanContext()) {
return extractedContext;
}
return api_1.ROOT_CONTEXT;
};
exports.customExtractor = customExtractor;
/*
* This patch extends the existing upstream extension for SQS. Extensions allow for custom logic for adding
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
* `aws.sqs.queue.url` and `aws.sqs.queue.name` attributes, to be used to generate RemoteTarget and achieve parity
* with the Java/Python instrumentation.
*
* Callout that today, the upstream logic adds `messaging.url` and `messaging.destination` but we feel that
* `aws.sqs` is more in line with existing AWS Semantic Convention attributes like `AWS_S3_BUCKET`, etc.
*
* @param sqsServiceExtension SQS Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
*/
function patchSqsServiceExtension(sqsServiceExtension) {
// It is not expected that `sqsServiceExtension` is undefined
if (sqsServiceExtension) {
const requestPreSpanHook = sqsServiceExtension.requestPreSpanHook;
// Save original `requestPreSpanHook` under a similar name, to be invoked by the patched hook
sqsServiceExtension._requestPreSpanHook = requestPreSpanHook;
// The patched hook will populate the 'aws.sqs.queue.url' and 'aws.sqs.queue.name' attributes according to spec
// from the 'messaging.url' attribute
const patchedRequestPreSpanHook = (request, _config) => {
var _a, _b;
const requestMetadata = sqsServiceExtension._requestPreSpanHook(request, _config);
// It is not expected that `requestMetadata.spanAttributes` can possibly be undefined, but still be careful anyways
if (requestMetadata.spanAttributes) {
if ((_a = request.commandInput) === null || _a === void 0 ? void 0 : _a.QueueUrl) {
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_URL] = request.commandInput.QueueUrl;
}
if ((_b = request.commandInput) === null || _b === void 0 ? void 0 : _b.QueueName) {
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_SQS_QUEUE_NAME] = request.commandInput.QueueName;
}
}
return requestMetadata;
};
sqsServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
}
}
/*
* This patch extends the existing upstream extension for SNS. Extensions allow for custom logic for adding
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
* `aws.sns.topic.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
*
*
* @param snsServiceExtension SNS Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
*/
function patchSnsServiceExtension(snsServiceExtension) {
if (snsServiceExtension) {
const requestPreSpanHook = snsServiceExtension.requestPreSpanHook;
snsServiceExtension._requestPreSpanHook = requestPreSpanHook;
const patchedRequestPreSpanHook = (request, _config) => {
var _a;
const requestMetadata = snsServiceExtension._requestPreSpanHook(request, _config);
if (requestMetadata.spanAttributes) {
const topicArn = (_a = request.commandInput) === null || _a === void 0 ? void 0 : _a.TopicArn;
if (topicArn) {
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_SNS_TOPIC_ARN] = topicArn;
}
}
return requestMetadata;
};
snsServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
}
}
/*
* This patch extends the existing upstream extension for Kinesis. Extensions allow for custom logic for adding
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
* `aws.kinesis.stream.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
*
*
* @param kinesisServiceExtension Kinesis Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
*/
function patchKinesisServiceExtension(kinesisServiceExtension) {
if (kinesisServiceExtension) {
const requestPreSpanHook = kinesisServiceExtension.requestPreSpanHook;
kinesisServiceExtension._requestPreSpanHook = requestPreSpanHook;
const patchedRequestPreSpanHook = (request, _config) => {
var _a;
const requestMetadata = kinesisServiceExtension._requestPreSpanHook(request, _config);
if (requestMetadata.spanAttributes) {
const streamArn = (_a = request.commandInput) === null || _a === void 0 ? void 0 : _a.StreamARN;
if (streamArn) {
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_KINESIS_STREAM_ARN] = streamArn;
}
}
return requestMetadata;
};
kinesisServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
}
}
/*
* This patch extends the existing upstream extension for DynamoDB. Extensions allow for custom logic for adding
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
* `aws.dynamodb.table.arn` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
*
*
* @param dynamoDbServiceExtension DynamoDB Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
*/
function patchDynamoDbServiceExtension(dynamoDbServiceExtension) {
if (dynamoDbServiceExtension) {
if (typeof dynamoDbServiceExtension.responseHook === 'function') {
const responseHook = dynamoDbServiceExtension.responseHook;
const patchedResponseHook = (response, span, tracer, config) => {
var _a, _b;
responseHook.call(dynamoDbServiceExtension, response, span, tracer, config);
const tableArn = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.Table) === null || _b === void 0 ? void 0 : _b.TableArn;
if (tableArn) {
span.setAttribute(aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_DYNAMODB_TABLE_ARN, tableArn);
}
};
dynamoDbServiceExtension.responseHook = patchedResponseHook;
}
}
}
/*
* This patch extends the existing upstream extension for Lambda. Extensions allow for custom logic for adding
* service-specific information to spans, such as attributes. Specifically, we are adding logic to add
* `aws.lambda.resource_mapping.id` attribute, to be used to generate RemoteTarget and achieve parity with the Java/Python instrumentation.
*
*
* @param lambdaServiceExtension Lambda Service Extension obtained the service extension list from the AWS SDK OTel Instrumentation
*/
function patchLambdaServiceExtension(lambdaServiceExtension) {
if (lambdaServiceExtension) {
const requestPreSpanHook = lambdaServiceExtension.requestPreSpanHook;
lambdaServiceExtension._requestPreSpanHook = requestPreSpanHook;
const patchedRequestPreSpanHook = (request, _config) => {
var _a, _b;
const requestMetadata = lambdaServiceExtension._requestPreSpanHook(request, _config);
if (requestMetadata.spanAttributes) {
const resourceMappingId = (_a = request.commandInput) === null || _a === void 0 ? void 0 : _a.UUID;
if (resourceMappingId) {
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID] = resourceMappingId;
}
const requestFunctionNameFormat = (_b = request.commandInput) === null || _b === void 0 ? void 0 : _b.FunctionName;
let functionName = requestFunctionNameFormat;
if (requestFunctionNameFormat) {
if (requestFunctionNameFormat.startsWith('arn:aws:lambda')) {
const split = requestFunctionNameFormat.split(':');
functionName = split[split.length - 1];
}
requestMetadata.spanAttributes[aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME] = functionName;
}
}
return requestMetadata;
};
lambdaServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
if (typeof lambdaServiceExtension.responseHook === 'function') {
const originalResponseHook = lambdaServiceExtension.responseHook;
lambdaServiceExtension.responseHook = (response, span, tracer, config) => {
originalResponseHook.call(lambdaServiceExtension, response, span, tracer, config);
if (response.data && response.data.Configuration) {
const functionArn = response.data.Configuration.FunctionArn;
if (functionArn) {
span.setAttribute(aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, functionArn);
}
}
};
}
}
}
// Patch AWS Lambda Instrumentation
// 1. Override the upstream private _endSpan method to remove the unnecessary metric force-flush error message
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L358-L398
// 2. Support setting logger provider and force flushing logs
function patchAwsLambdaInstrumentation(instrumentation) {
if (instrumentation) {
const _setLoggerProvider = instrumentation['setLoggerProvider'];
instrumentation['_setLoggerProvider'] = _setLoggerProvider;
instrumentation['_logForceFlusher'] = undefined;
instrumentation['setLoggerProvider'] = function (loggerProvider) {
this['_setLoggerProvider'](loggerProvider);
this['_logForceFlusher'] = this['_logForceFlush'](loggerProvider);
};
instrumentation['_logForceFlush'] = function (loggerProvider) {
if (!loggerProvider)
return undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let currentProvider = loggerProvider;
if (typeof currentProvider.getDelegate === 'function') {
currentProvider = currentProvider.getDelegate();
}
if (typeof currentProvider.forceFlush === 'function') {
return currentProvider.forceFlush.bind(currentProvider);
}
return undefined;
};
instrumentation['_endSpan'] = function (span, err, callback) {
if (err) {
span.recordException(err);
}
let errMessage;
if (typeof err === 'string') {
errMessage = err;
}
else if (err) {
errMessage = err.message;
}
if (errMessage) {
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: errMessage,
});
}
span.end();
const flushers = [];
if (this['_traceForceFlusher']) {
flushers.push(this['_traceForceFlusher']());
}
else {
api_1.diag.error('Spans may not be exported for the lambda function because we are not force flushing before callback.');
}
if (this['_metricForceFlusher']) {
flushers.push(this['_metricForceFlusher']());
}
else {
api_1.diag.debug('Metrics may not be exported for the lambda function because we are not force flushing before callback.');
}
if (this['_logForceFlusher']) {
flushers.push(this['_logForceFlusher']());
}
else {
api_1.diag.debug('Logs may not be exported for the lambda function because we are not force flushing before callback.');
}
Promise.all(flushers).then(callback, callback);
};
}
}
// Override the upstream private _getV3SmithyClientSendPatch method to add middlewares to inject X-Ray Trace Context into HTTP Headers and to extract account access key id and region for cross-account support
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/instrumentation-aws-sdk-v0.48.0/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts#L373-L384
const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.client.config');
// Symbol to prevent infinite recursion during credential capture
// When we extract credentials, the AWS SDK may need to make additional AWS API calls
// (e.g., sts:AssumeRoleWithWebIdentity) which go through the same instrumented 'send' method.
// Without this flag, each credential request would trigger another credential extraction attempt,
// creating an infinite loop of nested AWS SDK calls.
exports.SKIP_CREDENTIAL_CAPTURE_KEY = Symbol('skip-credential-capture');
function patchAwsSdkInstrumentation(instrumentation) {
if (instrumentation) {
instrumentation['_getV3SmithyClientSendPatch'] = function (original) {
return function send(command, ...args) {
var _a, _b;
// Only add middleware once per client instance to reduce overhead
// AWS SDK clients may call 'send' multiple times, but we only need to patch once
// Even with override=true, adding middleware still causes overhead as it replaces existing stack entries
if (!this.__adotMiddlewarePatched) {
(_a = this.middlewareStack) === null || _a === void 0 ? void 0 : _a.add((next, context) => async (middlewareArgs) => {
api_1.propagation.inject(api_1.context.active(), middlewareArgs.request.headers, api_1.defaultTextMapSetter);
// Need to set capitalized version of the trace id to ensure that the Recursion Detection Middleware
// of aws-sdk-js-v3 will detect the propagated X-Ray Context
// See: https://github.com/aws/aws-sdk-js-v3/blob/v3.768.0/packages/middleware-recursion-detection/src/index.ts#L13
const xrayTraceId = middlewareArgs.request.headers[propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER];
if (xrayTraceId) {
middlewareArgs.request.headers[exports.AWSXRAY_TRACE_ID_HEADER_CAPITALIZED] = xrayTraceId;
delete middlewareArgs.request.headers[propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER];
}
const result = await next(middlewareArgs);
return result;
}, {
step: 'build',
name: '_adotInjectXrayContextMiddleware',
override: true,
});
(_b = this.middlewareStack) === null || _b === void 0 ? void 0 : _b.add((next, context) => async (middlewareArgs) => {
const activeContext = api_1.context.active();
// Skip credential extraction if this is a nested call from another credential extraction
// This prevents infinite recursion when credential providers make AWS API calls
if (activeContext.getValue(exports.SKIP_CREDENTIAL_CAPTURE_KEY)) {
return await next(middlewareArgs);
}
const span = api_1.trace.getSpan(activeContext);
if (span) {
// suppressTracing prevents span generation for internal credential extraction calls
// which are implementation details and not relevant to the application's telemetry
const suppressedContext = (0, core_1.suppressTracing)(activeContext).setValue(exports.SKIP_CREDENTIAL_CAPTURE_KEY, true);
// Skip credential extraction if the context is not injectable
if (suppressedContext.getValue(exports.SKIP_CREDENTIAL_CAPTURE_KEY)) {
await api_1.context.with(suppressedContext, async () => {
try {
const credsProvider = this.config.credentials;
if (credsProvider instanceof Function) {
const credentials = await credsProvider();
if (credentials === null || credentials === void 0 ? void 0 : credentials.accessKeyId) {
span.setAttribute(aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_AUTH_ACCOUNT_ACCESS_KEY, credentials.accessKeyId);
}
}
if (this.config.region instanceof Function) {
const region = await this.config.region();
if (region) {
span.setAttribute(aws_attribute_keys_1.AWS_ATTRIBUTE_KEYS.AWS_AUTH_REGION, region);
}
}
}
catch (err) {
api_1.diag.debug('Failed to get auth account access key and region:', err);
}
});
}
}
return await next(middlewareArgs);
}, {
step: 'build',
name: '_adotExtractSignerCredentials',
override: true,
});
this.__adotMiddlewarePatched = true;
}
command[V3_CLIENT_CONFIG_KEY] = this.config;
return original.apply(this, [command, ...args]);
};
};
}
}
//# sourceMappingURL=instrumentation-patch.js.map