Files
2026-04-09 19:01:53 +08:00

291 lines
13 KiB
JavaScript

"use strict";
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AwsLambdaInstrumentation = exports.traceContextEnvironmentKey = void 0;
const path = require("path");
const fs = require("fs");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const api_1 = require("@opentelemetry/api");
const propagator_aws_xray_1 = require("@opentelemetry/propagator-aws-xray");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const version_1 = require("./version");
const process_1 = require("process");
const awsPropagator = new propagator_aws_xray_1.AWSXRayPropagator();
const headerGetter = {
keys(carrier) {
return Object.keys(carrier);
},
get(carrier, key) {
return carrier[key];
},
};
exports.traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
class AwsLambdaInstrumentation extends instrumentation_1.InstrumentationBase {
constructor(config = {}) {
super(version_1.PACKAGE_NAME, version_1.PACKAGE_VERSION, config);
if (this._config.disableAwsContextPropagation == null) {
if (typeof process_1.env['OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION'] ===
'string' &&
process_1.env['OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION'].toLocaleLowerCase() === 'true') {
this._config.disableAwsContextPropagation = true;
}
}
}
setConfig(config = {}) {
this._config = config;
}
init() {
var _a;
const taskRoot = process.env.LAMBDA_TASK_ROOT;
const handlerDef = (_a = this._config.lambdaHandler) !== null && _a !== void 0 ? _a : process.env._HANDLER;
// _HANDLER and LAMBDA_TASK_ROOT are always defined in Lambda but guard bail out if in the future this changes.
if (!taskRoot || !handlerDef) {
this._diag.debug('Skipping lambda instrumentation: no _HANDLER/lambdaHandler or LAMBDA_TASK_ROOT.', { taskRoot, handlerDef });
return [];
}
const handler = path.basename(handlerDef);
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);
const [module, functionName] = handler.split('.', 2);
// Lambda loads user function using an absolute path.
let filename = path.resolve(taskRoot, moduleRoot, module);
if (!filename.endsWith('.js')) {
// its impossible to know in advance if the user has a cjs or js file.
// check that the .js file exists otherwise fallback to next known possibility
try {
fs.statSync(`${filename}.js`);
filename += '.js';
}
catch (e) {
// fallback to .cjs
filename += '.cjs';
}
}
api_1.diag.debug('Instrumenting lambda handler', {
taskRoot,
handlerDef,
handler,
moduleRoot,
module,
filename,
functionName,
});
return [
new instrumentation_1.InstrumentationNodeModuleDefinition(
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
// InstrumentationNodeModuleFile must be the module name.
filename, ['*'], undefined, undefined, [
new instrumentation_1.InstrumentationNodeModuleFile(module, ['*'], (moduleExports) => {
if ((0, instrumentation_1.isWrapped)(moduleExports[functionName])) {
this._unwrap(moduleExports, functionName);
}
this._wrap(moduleExports, functionName, this._getHandler());
return moduleExports;
}, (moduleExports) => {
if (moduleExports == null)
return;
this._unwrap(moduleExports, functionName);
}),
]),
];
}
_getHandler() {
return (original) => {
return this._getPatchHandler(original);
};
}
_getPatchHandler(original) {
api_1.diag.debug('patch handler function');
const plugin = this;
return function patchedHandler(
// The event can be a user type, it truly is any.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
event, context, callback) {
const config = plugin._config;
const parent = AwsLambdaInstrumentation._determineParent(event, context, config.disableAwsContextPropagation === true, config.eventContextExtractor ||
AwsLambdaInstrumentation._defaultEventContextExtractor);
const name = context.functionName;
const span = plugin.tracer.startSpan(name, {
kind: api_1.SpanKind.SERVER,
attributes: {
[semantic_conventions_1.SEMATTRS_FAAS_EXECUTION]: context.awsRequestId,
[semantic_conventions_1.SEMRESATTRS_FAAS_ID]: context.invokedFunctionArn,
[semantic_conventions_1.SEMRESATTRS_CLOUD_ACCOUNT_ID]: AwsLambdaInstrumentation._extractAccountId(context.invokedFunctionArn),
},
}, parent);
if (config.requestHook) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => config.requestHook(span, { event, context }), e => {
if (e)
api_1.diag.error('aws-lambda instrumentation: requestHook error', e);
}, true);
}
return api_1.context.with(api_1.trace.setSpan(parent, span), () => {
// Lambda seems to pass a callback even if handler is of Promise form, so we wrap all the time before calling
// the handler and see if the result is a Promise or not. In such a case, the callback is usually ignored. If
// the handler happened to both call the callback and complete a returned Promise, whichever happens first will
// win and the latter will be ignored.
const wrappedCallback = plugin._wrapCallback(callback, span);
const maybePromise = (0, instrumentation_1.safeExecuteInTheMiddle)(() => original.apply(this, [event, context, wrappedCallback]), error => {
if (error != null) {
// Exception thrown synchronously before resolving callback / promise.
plugin._applyResponseHook(span, error);
plugin._endSpan(span, error, () => { });
}
});
if (typeof (maybePromise === null || maybePromise === void 0 ? void 0 : maybePromise.then) === 'function') {
return maybePromise.then(value => {
plugin._applyResponseHook(span, null, value);
return new Promise(resolve => plugin._endSpan(span, undefined, () => resolve(value)));
}, (err) => {
plugin._applyResponseHook(span, err);
return new Promise((resolve, reject) => plugin._endSpan(span, err, () => reject(err)));
});
}
return maybePromise;
});
};
}
setTracerProvider(tracerProvider) {
super.setTracerProvider(tracerProvider);
this._traceForceFlusher = this._traceForceFlush(tracerProvider);
}
_traceForceFlush(tracerProvider) {
if (!tracerProvider)
return undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let currentProvider = tracerProvider;
if (typeof currentProvider.getDelegate === 'function') {
currentProvider = currentProvider.getDelegate();
}
if (typeof currentProvider.forceFlush === 'function') {
return currentProvider.forceFlush.bind(currentProvider);
}
return undefined;
}
setMeterProvider(meterProvider) {
super.setMeterProvider(meterProvider);
this._metricForceFlusher = this._metricForceFlush(meterProvider);
}
_metricForceFlush(meterProvider) {
if (!meterProvider)
return undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentProvider = meterProvider;
if (typeof currentProvider.forceFlush === 'function') {
return currentProvider.forceFlush.bind(currentProvider);
}
return undefined;
}
_wrapCallback(original, span) {
const plugin = this;
return function wrappedCallback(err, res) {
api_1.diag.debug('executing wrapped lookup callback function');
plugin._applyResponseHook(span, err, res);
plugin._endSpan(span, err, () => {
api_1.diag.debug('executing original lookup callback function');
return original.apply(this, [err, res]);
});
};
}
_endSpan(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.error('Metrics may not be exported for the lambda function because we are not force flushing before callback.');
}
Promise.all(flushers).then(callback, callback);
}
_applyResponseHook(span, err, res) {
var _a;
if ((_a = this._config) === null || _a === void 0 ? void 0 : _a.responseHook) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => this._config.responseHook(span, { err, res }), e => {
if (e)
api_1.diag.error('aws-lambda instrumentation: responseHook error', e);
}, true);
}
}
static _extractAccountId(arn) {
const parts = arn.split(':');
if (parts.length >= 5) {
return parts[4];
}
return undefined;
}
static _defaultEventContextExtractor(event) {
// The default extractor tries to get sampled trace header from HTTP headers.
const httpHeaders = event.headers || {};
return api_1.propagation.extract(api_1.context.active(), httpHeaders, headerGetter);
}
static _determineParent(event, context, disableAwsContextPropagation, eventContextExtractor) {
var _a, _b;
let parent = undefined;
if (!disableAwsContextPropagation) {
const lambdaTraceHeader = process.env[exports.traceContextEnvironmentKey];
if (lambdaTraceHeader) {
parent = awsPropagator.extract(api_1.context.active(), { [propagator_aws_xray_1.AWSXRAY_TRACE_ID_HEADER]: lambdaTraceHeader }, headerGetter);
}
if (parent) {
const spanContext = (_a = api_1.trace.getSpan(parent)) === null || _a === void 0 ? void 0 : _a.spanContext();
if (spanContext &&
(spanContext.traceFlags & api_1.TraceFlags.SAMPLED) === api_1.TraceFlags.SAMPLED) {
// Trace header provided by Lambda only sampled if a sampled context was propagated from
// an upstream cloud service such as S3, or the user is using X-Ray. In these cases, we
// need to use it as the parent.
return parent;
}
}
}
const extractedContext = (0, instrumentation_1.safeExecuteInTheMiddle)(() => eventContextExtractor(event, context), e => {
if (e)
api_1.diag.error('aws-lambda instrumentation: eventContextExtractor error', e);
}, true);
if ((_b = api_1.trace.getSpan(extractedContext)) === null || _b === void 0 ? void 0 : _b.spanContext()) {
return extractedContext;
}
if (!parent) {
// No context in Lambda environment or HTTP headers.
return api_1.ROOT_CONTEXT;
}
return parent;
}
}
exports.AwsLambdaInstrumentation = AwsLambdaInstrumentation;
//# sourceMappingURL=instrumentation.js.map