"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 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 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('@opentelemetry/instrumentation-aws-lambda', version_1.VERSION, _config);
        this._config = _config;
    }
    setConfig(config = {}) {
        this._config = config;
    }
    init() {
        const taskRoot = process.env.LAMBDA_TASK_ROOT;
        const handlerDef = 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) {
            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')) {
            // Patching infrastructure currently requires a filename when requiring with an absolute path.
            filename += '.js';
        }
        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) => {
                    api_1.diag.debug('Applying patch for lambda handler');
                    if (instrumentation_1.isWrapped(moduleExports[functionName])) {
                        this._unwrap(moduleExports, functionName);
                    }
                    this._wrap(moduleExports, functionName, this._getHandler());
                    return moduleExports;
                }, (moduleExports) => {
                    if (moduleExports == undefined)
                        return;
                    api_1.diag.debug('Removing patch for lambda handler');
                    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, 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.SemanticAttributes.FAAS_EXECUTION]: context.awsRequestId,
                    [semantic_conventions_1.SemanticResourceAttributes.FAAS_ID]: context.invokedFunctionArn,
                    [semantic_conventions_1.SemanticResourceAttributes.CLOUD_ACCOUNT_ID]: AwsLambdaInstrumentation._extractAccountId(context.invokedFunctionArn),
                },
            }, parent);
            if (config.requestHook) {
                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(api_1.context.active(), 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 = 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._forceFlush = this._getForceFlush(tracerProvider);
    }
    _getForceFlush(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;
    }
    _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();
        if (this._forceFlush) {
            this._forceFlush().then(() => callback(), () => callback());
        }
        else {
            api_1.diag.error('Spans may not be exported for the lambda function because we are not force flushing before callback.');
            callback();
        }
    }
    _applyResponseHook(span, err, res) {
        var _a;
        if ((_a = this._config) === null || _a === void 0 ? void 0 : _a.responseHook) {
            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, 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 = instrumentation_1.safeExecuteInTheMiddle(() => eventContextExtractor(event), 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