enhanced-adot-node-autoinstrumentation
Version:
This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.
263 lines (261 loc) • 15 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.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");
exports.traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
exports.AWSXRAY_TRACE_ID_HEADER_CAPITALIZED = 'X-Amzn-Trace-Id';
const awsPropagator = new propagator_aws_xray_1.AWSXRayPropagator();
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'));
}
}
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 first attempts to extract the trace context from the AWS X-Ray header, which is stored in the Lambda environment variables.
* If a valid span context is extracted from the environment, it uses this as the parent context for the function's tracing.
* If the X-Ray header is missing or invalid, it falls back to extracting trace context from the Lambda handler's event headers.
* If neither approach succeeds, it defaults to using the root Otel context, ensuring the function is still instrumented for tracing.
*/
const customExtractor = (event, _handlerContext) => {
var _a, _b;
let parent = undefined;
const lambdaTraceHeader = process.env[exports.traceContextEnvironmentKey];
if (lambdaTraceHeader) {
parent = awsPropagator.extract(api_1.context.active(), { [propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER]: lambdaTraceHeader }, exports.headerGetter);
}
if (parent) {
const spanContext = (_a = api_1.trace.getSpan(parent)) === null || _a === void 0 ? void 0 : _a.spanContext();
if (spanContext && (0, api_1.isSpanContextValid)(spanContext)) {
return parent;
}
}
const httpHeaders = event.headers || {};
const extractedContext = api_1.propagation.extract(api_1.context.active(), httpHeaders, exports.headerGetter);
if ((_b = api_1.trace.getSpan(extractedContext)) === null || _b === void 0 ? void 0 : _b.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 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);
}
}
};
}
}
}
// 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
function patchAwsLambdaInstrumentation(instrumentation) {
if (instrumentation) {
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.');
}
Promise.all(flushers).then(callback, callback);
};
}
}
// Override the upstream private _getV3SmithyClientSendPatch method to add middleware to inject X-Ray Trace Context into HTTP Headers
// 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 awsXrayPropagator = new propagator_aws_xray_1.AWSXRayPropagator();
const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.client.config');
function patchAwsSdkInstrumentation(instrumentation) {
if (instrumentation) {
instrumentation['_getV3SmithyClientSendPatch'] = function (original) {
return function send(command, ...args) {
var _a;
(_a = this.middlewareStack) === null || _a === void 0 ? void 0 : _a.add((next, context) => async (middlewareArgs) => {
awsXrayPropagator.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,
});
command[V3_CLIENT_CONFIG_KEY] = this.config;
return original.apply(this, [command, ...args]);
};
};
}
}
//# sourceMappingURL=instrumentation-patch.js.map