import { Context, context, Span, trace, Tracer } from '@opentelemetry/api'; import { sanitizeError } from './errorSanitizer.js'; /** * helper function to wrap a class method with tracing * @param className - The class name * @param methodName - The method name * @param descriptor - The method descriptor * @param tracer - The tracer */ export function traceMethod(className: string, methodName: string, descriptor: PropertyDescriptor, tracer: Tracer): PropertyDescriptor { const originalMethod = descriptor.value; // eslint-disable-next-line @typescript-eslint/no-explicit-any descriptor.value = function (...args: any[]) { // Potentially, we can pass this from tracing middleware to req and then to ActionHandler const parentSpan = trace.getActiveSpan() as Span | undefined; let ctx: Context; if (parentSpan) { ctx = trace.setSpan(context.active(), parentSpan); } else { ctx = context.active(); } // Use `startActiveSpan` to start and manage the span lifecycle return context.with(ctx, () => { return tracer.startActiveSpan(`${className}.${methodName}`, {}, async (span) => { try { const result = originalMethod.apply(this, args); if (result instanceof Promise) { return result .then((res) => { span.end(); return res; }) .catch((err) => { span.recordException(sanitizeError(err)); span.end(); throw err; }); } // For synchronous methods, end the span and return the result span.end(); return result; } catch (err) { span.recordException(sanitizeError(err)); span.end(); throw err; } }); }); }; return descriptor; } export function traceFunction({ name, tracer, fn, parent }: { name: string; tracer: Tracer; fn: (span: Span) => T | Promise; parent?: Span; }): Promise { const parentSpan = parent ?? (trace.getActiveSpan() as Span | undefined); const ctx = parentSpan ? trace.setSpan(context.active(), parentSpan) : context.active(); return context.with(ctx, () => tracer.startActiveSpan(name, {}, async (span) => { try { const result = fn(span); if (result instanceof Promise) { return result .then((res) => { span.end(); return res; }) .catch((err) => { span.recordException(sanitizeError(err)); span.end(); throw err; }); } span.end(); return result; } catch (err) { span.recordException(sanitizeError(err)); span.end(); throw err; } }) ); } /** * @deprecated Use `traceFunction` instead. */ export function executeTraced(name: string, tracer: Tracer, fn: (span: Span) => Promise, parent?: Span): Promise { const parentSpan = parent ?? (trace.getActiveSpan() as Span | undefined); if (parentSpan) { // use parent span as context return context.with(trace.setSpan(context.active(), parentSpan), () => { const span = tracer.startSpan(name); return fn(span).finally(() => { span.end(); }); }); } else { // use new span as context return context.with(context.active(), () => { const span = tracer.startSpan(name); return fn(span).finally(() => { span.end(); }); }); } } function findDescriptor(object: object, method: string): PropertyDescriptor | undefined { let currentProto = Object.getPrototypeOf(object); // Traverse up the prototype chain to find the method's descriptor while (currentProto) { const descriptor = Object.getOwnPropertyDescriptor(currentProto, method); if (descriptor) { return descriptor; // Return the found descriptor } currentProto = Object.getPrototypeOf(currentProto); // Move up the prototype chain } return undefined; // Return undefined if not found } export function addTracingToMethods(object: object, methods: string[], tracer: Tracer) { methods.forEach((method) => { if (typeof object[method] === 'function') { const descriptor = findDescriptor(object, method); if (descriptor) { Object.defineProperty(object, method, traceMethod(object.constructor.name, method, descriptor, tracer)); } } }); }