291 lines
13 KiB
JavaScript
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
|