UNPKG

@revai-care/instrumentation-adonisjs-6

Version:

OpenTelemetry instrumentation for Adonisjs server side applications framework

293 lines (292 loc) 11.8 kB
import { InstrumentationBase, isWrapped } from '@opentelemetry/instrumentation'; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import { SpanStatusCode, diag, trace, context } from '@opentelemetry/api'; import { Router, Server, ExceptionHandler } from '@adonisjs/core/http'; import { join } from 'path'; import { readFileSync } from 'fs'; import { AttributeNames } from './enums/AttributeNames.js'; import { AdonisType } from './enums/AdonisType.js'; /** * Attribute name for the HTTP path of the request. */ export const ATTR_HTTP_PATH = 'http.path'; const PACKAGE_NAME = '@revai-care/instrumentation-adonisjs-6'; const PACKAGE_VERSION = '0.1.2'; /** * Adds error information to a span. * * @param span The OpenTelemetry span to add error information to. * @param error The error object. * @returns The error object. */ function addError(span, error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); return error; } /** * AdonisJS instrumentation for OpenTelemetry. */ export class AdonisInstrumentation extends InstrumentationBase { /** * The component name for AdonisJS. */ static COMPONENT = '@adonisjs/core'; /** * Common attributes to be added to all spans. */ static COMMON_ATTRIBUTES = { component: AdonisInstrumentation.COMPONENT, [AttributeNames.VERSION]: 'unknown', }; constructor(config = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); AdonisInstrumentation.COMMON_ATTRIBUTES[AttributeNames.VERSION] = this.getAdonisVersion(); } init() { try { this.applyPatch(); } catch (error) { diag.error('Unable to initialize Adonis Instrumentation', error); } } /** * Applies patches to AdonisJS core modules. */ applyPatch() { this.ensureWrapped(Server.prototype, 'use', this.createWrapUse()); this.ensureWrapped(Router.prototype, 'use', this.createWrapUse()); this.ensureWrapped(Router.prototype, 'named', this.createWrapRouterNamed()); this.ensureWrapped(Router.prototype, 'route', this.createWrapRouteHandler()); this.ensureWrapped(ExceptionHandler.prototype, 'report', this.createWrapReportError()); } /** * Creates a wrapper for the `Server.prototype.use` method. * * @returns The wrapped `use` method. */ createWrapUse() { const instrumentation = this; return function useWrapper(use) { return async function wrappedUse(...args) { const middlewares = args?.[0]; await Promise.all(middlewares.map(async (middleware) => { const { default: Middleware } = await middleware(); instrumentation.ensureWrapped(Middleware.prototype, 'handle', instrumentation.createWrapServerMiddlewareHandler(Middleware.name)); })); return use.apply(this, args); }; }; } /** * Creates a wrapper for the `Router.prototype.named` method. * * @returns The wrapped `named` method. */ createWrapRouterNamed() { const instrumentation = this; return function namedWrapper(named) { return function wrappedNamed(...args) { const result = named.apply(this, args); Object.keys(result).forEach((key) => { const middlewareFn = result[key]; const middleware = middlewareFn(); instrumentation.ensureWrapped(middleware, 'handle', instrumentation.createWrapMiddlewareHandler(middleware.name)); // eslint-disable-next-line func-names, @typescript-eslint/no-explicit-any result[key] = function (...middlewareArgs) { return { ...middleware, args: middlewareArgs[0], }; }; }); return result; }; }; } /** * Creates a wrapper for the `Router.prototype.route` method. * * @returns The wrapped `route` method. */ createWrapRouteHandler() { const instrumentation = this; return function routeWrapper(route) { return function wrappedRoute(...args) { const handler = args[2]; if (Array.isArray(handler)) { const ControllerClass = handler[0]; const methodName = handler[1]; instrumentation.ensureWrapped(ControllerClass.prototype, methodName, instrumentation.createWrapRequestHandler(ControllerClass.name, methodName)); } return route.apply(this, args); }; }; } /** * Creates a wrapper for the `ExceptionHandler.prototype.report` method. * * @returns The wrapped `report` method. */ createWrapReportError() { const instrumentation = this; return function reportWrapper(report) { return async function wrappedReport(...args) { const [error, ctx] = args; const span = instrumentation.tracer.startSpan('ExceptionHandler.report', { attributes: { ...AdonisInstrumentation.COMMON_ATTRIBUTES, [ATTR_HTTP_REQUEST_METHOD]: ctx.request.method(), [ATTR_HTTP_ROUTE]: ctx.request.url(true), [ATTR_HTTP_PATH]: ctx.route?.pattern, [AttributeNames.NAMESPACE]: 'ExceptionHandler', [AttributeNames.METHOD]: 'report', }, }); addError(span, error); try { return report.apply(this, args); } finally { span.end(); } }; }; } /** * Creates a wrapper for request handler methods. * * @param nameSpace The namespace of the request handler. * @param methodName The name of the request handler method. * @returns The wrapped request handler method. */ createWrapRequestHandler(nameSpace, methodName) { const instrumentation = this; return function wrapRequestHandler(handler) { return async function wrappedHandler(...args) { const ctx = args[0]; return instrumentation.wrappedRequestHandler(instrumentation.tracer, () => handler.apply(this, args), ctx, instrumentation.getRequestHandlerSpanName(ctx), AdonisType.REQUEST_HANDLER, nameSpace, methodName); }; }; } /** * Creates a wrapper for middleware handler methods. * * @param nameSpace The namespace of the middleware handler. * @returns The wrapped middleware handler method. */ createWrapMiddlewareHandler(nameSpace) { const instrumentation = this; return function wrapMiddlewareHandler(handler) { return async function wrappedHandler(...args) { const ctx = args?.[1]; return instrumentation.wrappedRequestHandler(instrumentation.tracer, () => handler.apply(this, args), ctx, `${AdonisType.MIDDLEWARE} - ${nameSpace}`, AdonisType.MIDDLEWARE, nameSpace, AdonisType.MIDDLEWARE_HANLDER_METHOD); }; }; } /** * Creates a wrapper for server middleware handler methods. * * @param nameSpace The namespace of the middleware handler. * @returns The wrapped middleware handler method. */ createWrapServerMiddlewareHandler(nameSpace) { const instrumentation = this; // eslint-disable-next-line @typescript-eslint/no-explicit-any return function wrapMiddlewareHandler(handler) { return async function wrappedHandler(...args) { const ctx = args?.[0]; return instrumentation.wrappedRequestHandler(instrumentation.tracer, () => handler.apply(this, args), ctx, `${AdonisType.MIDDLEWARE} - ${nameSpace}`, AdonisType.MIDDLEWARE, nameSpace, AdonisType.MIDDLEWARE_HANLDER_METHOD); }; }; } /** * Wraps a request handler function to create a span and handle errors. * * @private * @async * @param {Tracer} tracer The OpenTelemetry tracer. * @param {() => Promise<any>} handlerCb The request handler callback function. * @param {HttpContext} ctx The AdonisJS HTTP context. * @param {string} spanName The name of the span. * @param {string} type The type of the handler (e.g., 'handler', 'middleware'). * @param {string} nameSpace The namespace of the handler. * @param {string} methodName The name of the handler method. * @returns {ReturnType<typeof handlerCb>} The result of the handler callback. */ async wrappedRequestHandler(tracer, // eslint-disable-next-line @typescript-eslint/no-explicit-any handlerCb, ctx, spanName, type, nameSpace, methodName) { const span = tracer.startSpan(spanName, { attributes: { ...AdonisInstrumentation.COMMON_ATTRIBUTES, [ATTR_HTTP_REQUEST_METHOD]: ctx.request.method(), [ATTR_HTTP_ROUTE]: ctx.request.url(true), [ATTR_HTTP_PATH]: ctx.route?.pattern, [AttributeNames.TYPE]: type, [AttributeNames.NAMESPACE]: nameSpace, [AttributeNames.METHOD]: methodName, }, }); const parentSpan = trace.getSpan(context.active()); const spanContext = trace.setSpan(context.active(), parentSpan ?? span); return context.with(spanContext, async () => { try { const response = await handlerCb(); return response; } catch (error) { throw addError(span, error); } finally { span.end(); } }); } /** * Ensures that a method on an object is wrapped with a given wrapper function. * If the method is already wrapped, it unwraps it before wrapping again. * * @private * @template T The type of the object. * @param {T} obj The object containing the method to wrap. * @param {keyof T} methodName The name of the method to wrap. * @param {(original: any) => any} wrapper The wrapper function. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any ensureWrapped(obj, methodName, wrapper) { if (isWrapped(obj[methodName])) { this._unwrap(obj, methodName); } this._wrap(obj, methodName, wrapper); } /** * Generates a span name for a request handler based on the HTTP context. * * @private * @param {HttpContext} ctx The AdonisJS HTTP context. * @returns {string} The generated span name. */ getRequestHandlerSpanName(ctx) { const path = ctx.route?.pattern ?? ctx.request.url(); return `request handler - ${path}`; } /** * Retrieves the version of the AdonisJS core package. * * @private * @returns {string} The AdonisJS core version, or 'unknown' if retrieval fails. */ getAdonisVersion() { try { const pkgPath = join(process.cwd(), 'node_modules', '@adonisjs/core/package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); return pkg.version; } catch (error) { diag.debug('Unable to get adonis version', error); return 'unknown'; } } }