UNPKG

@opentelemetry/instrumentation-hapi

Version:
303 lines 14.7 kB
"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) { 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?.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 route.options?.handler === 'function') { route.options.handler = wrapHandler(route.options.handler); } return route; } } exports.HapiInstrumentation = HapiInstrumentation; //# sourceMappingURL=instrumentation.js.map