"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.HapiInstrumentation = void 0;
const api = require("@opentelemetry/api");
const core_1 = require("@opentelemetry/core");
const instrumentation_1 = require("@opentelemetry/instrumentation");
/** @knipignore */
const version_1 = require("./version");
const internal_types_1 = require("./internal-types");
const utils_1 = require("./utils");
/** Hapi instrumentation for OpenTelemetry */
class HapiInstrumentation extends instrumentation_1.InstrumentationBase {
    constructor(config = {}) {
        super(version_1.PACKAGE_NAME, version_1.PACKAGE_VERSION, config);
    }
    init() {
        return new instrumentation_1.InstrumentationNodeModuleDefinition(internal_types_1.HapiComponentName, ['>=17.0.0 <22'], (module) => {
            const moduleExports = module[Symbol.toStringTag] === 'Module' ? module.default : module;
            if (!(0, instrumentation_1.isWrapped)(moduleExports.server)) {
                this._wrap(moduleExports, 'server', this._getServerPatch.bind(this));
            }
            if (!(0, instrumentation_1.isWrapped)(moduleExports.Server)) {
                this._wrap(moduleExports, 'Server', this._getServerPatch.bind(this));
            }
            return moduleExports;
        }, (module) => {
            const moduleExports = module[Symbol.toStringTag] === 'Module' ? module.default : module;
            this._massUnwrap([moduleExports], ['server', 'Server']);
        });
    }
    /**
     * Patches the Hapi.server and Hapi.Server functions in order to instrument
     * the server.route, server.ext, and server.register functions via calls to the
     * @function _getServerRoutePatch, @function _getServerExtPatch, and
     * @function _getServerRegisterPatch functions
     * @param original - the original Hapi Server creation function
     */
    _getServerPatch(original) {
        const instrumentation = this;
        const self = this;
        return function server(opts) {
            const newServer = original.apply(this, [opts]);
            self._wrap(newServer, 'route', originalRouter => {
                return instrumentation._getServerRoutePatch.bind(instrumentation)(originalRouter);
            });
            // Casting as any is necessary here due to multiple overloads on the Hapi.ext
            // function, which requires supporting a variety of different parameters
            // as extension inputs
            self._wrap(newServer, 'ext', originalExtHandler => {
                return instrumentation._getServerExtPatch.bind(instrumentation)(
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                originalExtHandler);
            });
            // Casting as any is necessary here due to multiple overloads on the Hapi.Server.register
            // function, which requires supporting a variety of different types of Plugin inputs
            self._wrap(newServer, 'register', 
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            instrumentation._getServerRegisterPatch.bind(instrumentation));
            return newServer;
        };
    }
    /**
     * Patches the plugin register function used by the Hapi Server. This function
     * goes through each plugin that is being registered and adds instrumentation
     * via a call to the @function _wrapRegisterHandler function.
     * @param {RegisterFunction<T>} original - the original register function which
     * registers each plugin on the server
     */
    _getServerRegisterPatch(original) {
        const instrumentation = this;
        return function register(pluginInput, options) {
            if (Array.isArray(pluginInput)) {
                for (const pluginObj of pluginInput) {
                    const plugin = (0, utils_1.getPluginFromInput)(pluginObj);
                    instrumentation._wrapRegisterHandler(plugin);
                }
            }
            else {
                const plugin = (0, utils_1.getPluginFromInput)(pluginInput);
                instrumentation._wrapRegisterHandler(plugin);
            }
            return original.apply(this, [pluginInput, options]);
        };
    }
    /**
     * Patches the Server.ext function which adds extension methods to the specified
     * point along the request lifecycle. This function accepts the full range of
     * accepted input into the standard Hapi `server.ext` function. For each extension,
     * it adds instrumentation to the handler via a call to the @function _wrapExtMethods
     * function.
     * @param original - the original ext function which adds the extension method to the server
     * @param {string} [pluginName] - if present, represents the name of the plugin responsible
     * for adding this server extension. Else, signifies that the extension was added directly
     */
    _getServerExtPatch(original, pluginName) {
        const instrumentation = this;
        return function ext(...args) {
            if (Array.isArray(args[0])) {
                const eventsList = args[0];
                for (let i = 0; i < eventsList.length; i++) {
                    const eventObj = eventsList[i];
                    if ((0, utils_1.isLifecycleExtType)(eventObj.type)) {
                        const lifecycleEventObj = eventObj;
                        const handler = instrumentation._wrapExtMethods(lifecycleEventObj.method, eventObj.type, pluginName);
                        lifecycleEventObj.method = handler;
                        eventsList[i] = lifecycleEventObj;
                    }
                }
                return original.apply(this, args);
            }
            else if ((0, utils_1.isDirectExtInput)(args)) {
                const extInput = args;
                const method = extInput[1];
                const handler = instrumentation._wrapExtMethods(method, extInput[0], pluginName);
                return original.apply(this, [extInput[0], handler, extInput[2]]);
            }
            else if ((0, utils_1.isLifecycleExtEventObj)(args[0])) {
                const lifecycleEventObj = args[0];
                const handler = instrumentation._wrapExtMethods(lifecycleEventObj.method, lifecycleEventObj.type, pluginName);
                lifecycleEventObj.method = handler;
                return original.call(this, lifecycleEventObj);
            }
            return original.apply(this, args);
        };
    }
    /**
     * Patches the Server.route function. This function accepts either one or an array
     * of Hapi.ServerRoute objects and adds instrumentation on each route via a call to
     * the @function _wrapRouteHandler function.
     * @param {HapiServerRouteInputMethod} original - the original route function which adds
     * the route to the server
     * @param {string} [pluginName] - if present, represents the name of the plugin responsible
     * for adding this server route. Else, signifies that the route was added directly
     */
    _getServerRoutePatch(original, pluginName) {
        const instrumentation = this;
        return function route(route) {
            if (Array.isArray(route)) {
                for (let i = 0; i < route.length; i++) {
                    const newRoute = instrumentation._wrapRouteHandler.call(instrumentation, route[i], pluginName);
                    route[i] = newRoute;
                }
            }
            else {
                route = instrumentation._wrapRouteHandler.call(instrumentation, route, pluginName);
            }
            return original.apply(this, [route]);
        };
    }
    /**
     * Wraps newly registered plugins to add instrumentation to the plugin's clone of
     * the original server. Specifically, wraps the server.route and server.ext functions
     * via calls to @function _getServerRoutePatch and @function _getServerExtPatch
     * @param {Hapi.Plugin<T>} plugin - the new plugin which is being instrumented
     */
    _wrapRegisterHandler(plugin) {
        const instrumentation = this;
        const pluginName = (0, utils_1.getPluginName)(plugin);
        const oldRegister = plugin.register;
        const self = this;
        const newRegisterHandler = function (server, options) {
            self._wrap(server, 'route', original => {
                return instrumentation._getServerRoutePatch.bind(instrumentation)(original, pluginName);
            });
            // Casting as any is necessary here due to multiple overloads on the Hapi.ext
            // function, which requires supporting a variety of different parameters
            // as extension inputs
            self._wrap(server, 'ext', originalExtHandler => {
                return instrumentation._getServerExtPatch.bind(instrumentation)(
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                originalExtHandler, pluginName);
            });
            return oldRegister.call(this, server, options);
        };
        plugin.register = newRegisterHandler;
    }
    /**
     * Wraps request extension methods to add instrumentation to each new extension handler.
     * Patches each individual extension in order to create the
     * span and propagate context. It does not create spans when there is no parent span.
     * @param {PatchableExtMethod | PatchableExtMethod[]} method - the request extension
     * handler which is being instrumented
     * @param {Hapi.ServerRequestExtType} extPoint - the point in the Hapi request lifecycle
     * which this extension targets
     * @param {string} [pluginName] - if present, represents the name of the plugin responsible
     * for adding this server route. Else, signifies that the route was added directly
     */
    _wrapExtMethods(method, extPoint, pluginName) {
        const instrumentation = this;
        if (method instanceof Array) {
            for (let i = 0; i < method.length; i++) {
                method[i] = instrumentation._wrapExtMethods(method[i], extPoint);
            }
            return method;
        }
        else if ((0, utils_1.isPatchableExtMethod)(method)) {
            if (method[internal_types_1.handlerPatched] === true)
                return method;
            method[internal_types_1.handlerPatched] = true;
            const newHandler = async function (...params) {
                if (api.trace.getSpan(api.context.active()) === undefined) {
                    return await method.apply(this, params);
                }
                const metadata = (0, utils_1.getExtMetadata)(extPoint, pluginName);
                const span = instrumentation.tracer.startSpan(metadata.name, {
                    attributes: metadata.attributes,
                });
                try {
                    return await api.context.with(api.trace.setSpan(api.context.active(), span), method, undefined, ...params);
                }
                catch (err) {
                    span.recordException(err);
                    span.setStatus({
                        code: api.SpanStatusCode.ERROR,
                        message: err.message,
                    });
                    throw err;
                }
                finally {
                    span.end();
                }
            };
            return newHandler;
        }
        return method;
    }
    /**
     * Patches each individual route handler method in order to create the
     * span and propagate context. It does not create spans when there is no parent span.
     * @param {PatchableServerRoute} route - the route handler which is being instrumented
     * @param {string} [pluginName] - if present, represents the name of the plugin responsible
     * for adding this server route. Else, signifies that the route was added directly
     */
    _wrapRouteHandler(route, pluginName) {
        var _a;
        const instrumentation = this;
        if (route[internal_types_1.handlerPatched] === true)
            return route;
        route[internal_types_1.handlerPatched] = true;
        const wrapHandler = oldHandler => {
            return async function (...params) {
                if (api.trace.getSpan(api.context.active()) === undefined) {
                    return await oldHandler.call(this, ...params);
                }
                const rpcMetadata = (0, core_1.getRPCMetadata)(api.context.active());
                if ((rpcMetadata === null || rpcMetadata === void 0 ? void 0 : rpcMetadata.type) === core_1.RPCType.HTTP) {
                    rpcMetadata.route = route.path;
                }
                const metadata = (0, utils_1.getRouteMetadata)(route, pluginName);
                const span = instrumentation.tracer.startSpan(metadata.name, {
                    attributes: metadata.attributes,
                });
                try {
                    return await api.context.with(api.trace.setSpan(api.context.active(), span), () => oldHandler.call(this, ...params));
                }
                catch (err) {
                    span.recordException(err);
                    span.setStatus({
                        code: api.SpanStatusCode.ERROR,
                        message: err.message,
                    });
                    throw err;
                }
                finally {
                    span.end();
                }
            };
        };
        if (typeof route.handler === 'function') {
            route.handler = wrapHandler(route.handler);
        }
        else if (typeof route.options === 'function') {
            const oldOptions = route.options;
            route.options = function (server) {
                const options = oldOptions(server);
                if (typeof options.handler === 'function') {
                    options.handler = wrapHandler(options.handler);
                }
                return options;
            };
        }
        else if (typeof ((_a = route.options) === null || _a === void 0 ? void 0 : _a.handler) === 'function') {
            route.options.handler = wrapHandler(route.options.handler);
        }
        return route;
    }
}
exports.HapiInstrumentation = HapiInstrumentation;
//# sourceMappingURL=instrumentation.js.map