@revai-care/instrumentation-adonisjs-6
Version:
OpenTelemetry instrumentation for Adonisjs server side applications framework
293 lines (292 loc) • 11.8 kB
JavaScript
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';
}
}
}