UNPKG

@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
"use strict"; // 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