UNPKG

@erickluis00/otelviewer

Version:

Shared OpenTelemetry tracing utilities, types, and batch processor for Realtime OpenTelemetry Viewer [WIP]

1,025 lines 44.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getRemoteExecutables = getRemoteExecutables; exports.executeRemoteMethod = executeRemoteMethod; exports.configureAddEventCallback = configureAddEventCallback; exports.track = track; exports.trackWrapper = trackWrapper; exports.trackAll = trackAll; exports.addSpanAttributes = addSpanAttributes; exports.addSpanEvent = addSpanEvent; exports.getCurrentSpan = getCurrentSpan; exports.createSpan = createSpan; exports.TrackClass = TrackClass; exports.createTrackedInstanceMethod = createTrackedInstanceMethod; exports.createTrackedStaticMethod = createTrackedStaticMethod; const api_1 = require("@opentelemetry/api"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const ts = __importStar(require("typescript")); const remoteExecutableRegistry = new Map(); /** * Register a method for remote execution */ function registerRemoteExecutable(config) { const key = `${config.className}.${config.methodName}`; remoteExecutableRegistry.set(key, config); if (process.env.NODE_ENV !== 'production') { console.log(`[OTEL Remote] Registered: ${key}`); } } /** * Get all registered remote executable methods */ function getRemoteExecutables() { return remoteExecutableRegistry; } /** * Execute a registered method by its key with full tracing */ async function executeRemoteMethod(key, args) { const method = remoteExecutableRegistry.get(key); if (!method) { return { success: false, error: `Method not found: ${key}` }; } try { let result; let traceId; if (method.isStatic) { // Check if there's already an active span const existingSpan = api_1.trace.getActiveSpan(); if (existingSpan) { traceId = existingSpan.spanContext().traceId; existingSpan.setAttributes({ 'remote.execution': true }); // Execute within existing span context result = await method.originalFunction.apply(method.constructor, args); } else { // Create a new root span for remote execution const tracer = getTracer(); const spanName = `Remote: ${key}`; result = await tracer.startActiveSpan(spanName, { attributes: { 'remote.execution': true, } }, async (span) => { try { // Execute the method as a child of this span const methodResult = await method.originalFunction.apply(method.constructor, args); // Capture trace ID from the span traceId = span.spanContext().traceId; span.setStatus({ code: api_1.SpanStatusCode.OK }); return methodResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); throw error; } finally { span.end(); } }); } } else { // For instance methods, we'd need an instance - for now, only support static throw new Error('Instance methods not supported yet'); } return { success: true, result, traceId }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Get trace ID from active span if available const activeSpan = api_1.trace.getActiveSpan(); const traceId = activeSpan?.spanContext().traceId; return { success: false, error: errorMessage, traceId }; } } // Global configuration for breadcrumb integration let addEventCallback = null; /** * Configure breadcrumb callback for span events */ function configureAddEventCallback(callback) { addEventCallback = callback; } function getTracer() { return api_1.trace.getTracer('shared-otel-tracer'); } /** * Helper function to monkey patch span.addEvent for breadcrumb integration */ function patchSpanForBreadcrumbs(span) { if (addEventCallback && span.addEvent) { const originalAddEvent = span.addEvent.bind(span); span.addEvent = function (eventName, attributes) { const result = originalAddEvent(eventName, attributes); try { addEventCallback(eventName, attributes); } catch (e) { // Don't let breadcrumb errors break the original functionality } return result; }; } } /** * Extracts source code information from a function using stack traces * Only works in development mode with source files available */ function extractSourceCodeInfo(fn, functionName) { if (process.env.NODE_ENV === 'production') { return {}; } try { // Create a new Error to get the stack trace const originalPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (_, stack) => stack; const obj = {}; Error.captureStackTrace(obj, extractSourceCodeInfo); const stack = obj.stack; Error.prepareStackTrace = originalPrepareStackTrace; // Look for the caller's file (skip this function and track function) let targetCallSite; for (let i = 0; i < stack.length; i++) { const site = stack[i]; const fileName = site.getFileName(); if (fileName) { // Handle file:// URLs for filtering const cleanFileName = fileName.startsWith('file://') ? fileName.replace('file://', '') : fileName; if (!cleanFileName.includes('tracing-utils') && !cleanFileName.includes('node_modules') && (cleanFileName.endsWith('.ts') || cleanFileName.endsWith('.js'))) { targetCallSite = site; break; } } } if (!targetCallSite) { return {}; } const fileName = targetCallSite.getFileName(); const lineNumber = targetCallSite.getLineNumber(); const columnNumber = targetCallSite.getColumnNumber(); if (!fileName || !lineNumber) { return {}; } // Handle file:// URLs - convert to regular file path const filePath = fileName.startsWith('file://') ? fileName.replace('file://', '') : fileName; // Read the source file const sourceCode = fs.readFileSync(filePath, 'utf8'); // Use TypeScript AST to find the function let functionInfo = findFunctionWithAST(sourceCode, functionName); let functionCode; let actualLineNumber = lineNumber; if (functionInfo) { functionCode = functionInfo.code; actualLineNumber = functionInfo.lineNumber; } else { // If named function not found, try to extract anonymous function at the call site const anonymousFunctionInfo = findAnonymousFunctionAtLocation(sourceCode, lineNumber, columnNumber || undefined, functionName); if (anonymousFunctionInfo) { functionCode = anonymousFunctionInfo.code; actualLineNumber = anonymousFunctionInfo.lineNumber; } } return { functionCode, fileName: path.relative(process.cwd(), filePath), lineNumber: actualLineNumber || undefined, columnNumber: columnNumber || undefined }; } catch (error) { console.warn(`[OTEL] Failed to extract source code for ${functionName}:`, error); return {}; } } /** * Finds a function definition using TypeScript AST parsing */ function findFunctionWithAST(sourceCode, functionName) { try { // Create a TypeScript source file const sourceFile = ts.createSourceFile('temp.ts', sourceCode, ts.ScriptTarget.Latest, true); let foundFunction; const visit = (node) => { // Check for function declarations if (ts.isFunctionDeclaration(node) && node.name?.getText() === functionName) { const startPos = node.getFullStart(); const endPos = node.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const lineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; foundFunction = { node, code, lineNumber }; return; } // Check for method definitions in objects/classes if (ts.isMethodDeclaration(node) && node.name?.getText() === functionName) { const startPos = node.getFullStart(); const endPos = node.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const lineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; foundFunction = { node, code, lineNumber }; return; } // Check for property assignments with function expressions if (ts.isPropertyAssignment(node) && node.name?.getText() === functionName) { if (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer)) { const startPos = node.getFullStart(); const endPos = node.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const lineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; foundFunction = { node, code, lineNumber }; return; } } // Check for variable declarations with function expressions if (ts.isVariableDeclaration(node) && node.name?.getText() === functionName) { if (node.initializer && (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer))) { const startPos = node.getFullStart(); const endPos = node.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const lineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; foundFunction = { node, code, lineNumber }; return; } } // Check for object literal methods (shorthand methods in objects) if (ts.isMethodDeclaration(node) && node.name?.getText() === functionName) { const startPos = node.getFullStart(); const endPos = node.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const lineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; foundFunction = { node, code, lineNumber }; return; } // Continue traversing the AST if (!foundFunction) { ts.forEachChild(node, visit); } }; visit(sourceFile); if (foundFunction) { return { code: foundFunction.code, lineNumber: foundFunction.lineNumber }; } return undefined; } catch (error) { console.warn(`[OTEL] Failed to parse TypeScript AST for function ${functionName}:`, error); return undefined; } } /** * Finds an anonymous function at a specific location in the source code * Uses both function name and position for more accurate matching */ function findAnonymousFunctionAtLocation(sourceCode, lineNumber, columnNumber, functionName) { if (!lineNumber) { return undefined; } try { // Create a TypeScript source file const sourceFile = ts.createSourceFile('temp.ts', sourceCode, ts.ScriptTarget.Latest, true); // Convert line number to position (TypeScript uses 0-based positions) const targetLineIndex = lineNumber - 1; const lines = sourceCode.split('\n'); if (targetLineIndex >= lines.length) { return undefined; } // Calculate the approximate position from line and column let targetPosition = 0; for (let i = 0; i < targetLineIndex; i++) { targetPosition += lines[i].length + 1; // +1 for newline } if (columnNumber) { targetPosition += Math.max(0, columnNumber - 1); } let allMatches = []; const findAllTrackCalls = (node) => { // Look for track() calls with anonymous functions if (ts.isCallExpression(node)) { const expression = node.expression; if (ts.isIdentifier(expression) && expression.text === 'track') { // Check if it has the expected structure: track('name', function) if (node.arguments.length >= 2) { const firstArg = node.arguments[0]; const secondArg = node.arguments[1]; let trackFunctionName; if (ts.isStringLiteral(firstArg)) { trackFunctionName = firstArg.text; } if (ts.isFunctionExpression(secondArg) || ts.isArrowFunction(secondArg)) { const startPos = secondArg.getFullStart(); const endPos = secondArg.getEnd(); const code = sourceCode.substring(startPos, endPos).trim(); const nodeLineNumber = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1; const callSiteLineNumber = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; const specificity = endPos - startPos; // Calculate distance from target position (both line-wise and position-wise) const lineDistance = Math.abs(callSiteLineNumber - lineNumber); const positionDistance = Math.abs(node.getStart() - targetPosition); const distanceFromTarget = lineDistance * 1000 + positionDistance; // prioritize line accuracy allMatches.push({ node: secondArg, code, lineNumber: nodeLineNumber, specificity, functionName: trackFunctionName, distanceFromTarget }); } } } } // Continue searching child nodes ts.forEachChild(node, findAllTrackCalls); }; findAllTrackCalls(sourceFile); if (allMatches.length === 0) { return undefined; } // Sort matches by priority: // 1. Exact function name match (if provided) // 2. Closest to target position // 3. Most specific (smallest function) allMatches.sort((a, b) => { // Priority 1: Exact function name match if (functionName) { const aNameMatch = a.functionName === functionName ? 0 : 1; const bNameMatch = b.functionName === functionName ? 0 : 1; if (aNameMatch !== bNameMatch) { return aNameMatch - bNameMatch; } } // Priority 2: Distance from target position if (a.distanceFromTarget !== b.distanceFromTarget) { return a.distanceFromTarget - b.distanceFromTarget; } // Priority 3: Specificity (smaller functions are more specific) return a.specificity - b.specificity; }); const bestMatch = allMatches[0]; return { code: bestMatch.code, lineNumber: bestMatch.lineNumber }; } catch (error) { console.warn(`[OTEL] Failed to find anonymous function at location:`, error); return undefined; } } /** * Extracts parameter names from a function signature and creates input object * @param fn The function to extract parameters from * @param args The arguments passed to the function * @param expectSpanParam Whether to expect 'span' as the first parameter (for track() functions) * @returns Input object with parameter names as keys, or object with generic names */ function extractParameterInput(fn, args, expectSpanParam = false) { if (args.length === 0) return undefined; try { // Extract parameter names from function signature const funcString = fn.toString(); // More robust regex to capture parameters, handling various function formats let paramMatch; if (expectSpanParam) { // For track() functions that have span as first parameter paramMatch = funcString.match(/\(\s*span\s*(?:,\s*([^)]+))?\)/); } else { // For regular class methods - capture all parameters // This handles: function(params), (params) =>, async function(params), static method(params) paramMatch = funcString.match(/(?:function\s*\w*|(?:async\s+)?(?:static\s+)?\w*)\s*\(\s*([^)]*)\s*\)/) || funcString.match(/\(\s*([^)]*)\s*\)\s*=>/) || funcString.match(/\(\s*([^)]*)\s*\)/); } if (paramMatch && paramMatch[1]) { let rawParams = paramMatch[1].trim(); if (!rawParams) { // No parameters, return undefined return undefined; } // Use smart parameter parsing that handles nested types const paramNames = parseParameterNames(rawParams, expectSpanParam); if (paramNames.length > 0) { // If we have parameter names, try to match with args if (paramNames.length === args.length) { // Perfect match - create object with parameter names as keys const input = {}; paramNames.forEach((name, index) => { input[name] = args[index]; }); return input; } else { // Mismatch in count, but we still have some names // Use available names and fall back to generic names for extras const input = {}; args.forEach((arg, index) => { const paramName = paramNames[index] || `arg${index + 1}`; input[paramName] = arg; }); return input; } } } } catch (error) { // If parsing fails, log and fall back console.warn('[OTEL] Parameter extraction failed:', error); } // Fallback: create object with generic parameter names if (args.length === 1) { return { arg1: args[0] }; } else if (args.length > 1) { const input = {}; args.forEach((arg, index) => { input[`arg${index + 1}`] = arg; }); return input; } return undefined; } /** * Smart parameter name parser that handles nested types, generics, and complex TypeScript signatures */ function parseParameterNames(parametersString, expectSpanParam = false) { const parameters = []; let current = ''; let depth = 0; let inString = false; let stringChar = ''; for (let i = 0; i < parametersString.length; i++) { const char = parametersString[i]; // Handle string literals if (!inString && (char === '"' || char === "'" || char === '`')) { inString = true; stringChar = char; current += char; continue; } if (inString) { current += char; if (char === stringChar && parametersString[i - 1] !== '\\') { inString = false; stringChar = ''; } continue; } // Handle nested structures if (char === '{' || char === '[' || char === '(' || char === '<') { depth++; current += char; continue; } if (char === '}' || char === ']' || char === ')' || char === '>') { depth--; current += char; continue; } // Handle commas - only split on top-level commas if (char === ',' && depth === 0) { if (current.trim()) { parameters.push(current.trim()); } current = ''; continue; } current += char; } // Add the last parameter if (current.trim()) { parameters.push(current.trim()); } // Extract clean parameter names from each parameter string const cleanNames = parameters .map(param => extractCleanParameterName(param)) .filter(name => name && name !== 'span'); return cleanNames; } /** * Extract clean parameter name from a parameter string like "name: string = 'default'" or "{ id, name }: User" */ function extractCleanParameterName(paramString) { // Remove leading/trailing whitespace paramString = paramString.trim(); // Handle destructuring patterns like "{ id, name }: Type" -> use first property if (paramString.startsWith('{')) { const match = paramString.match(/{\s*(\w+)/); if (match) { return match[1]; } } // Handle array destructuring like "[first, second]: Type" -> use first element if (paramString.startsWith('[')) { const match = paramString.match(/\[\s*(\w+)/); if (match) { return match[1]; } } // Handle rest parameters like "...args: Type[]" -> "args" if (paramString.startsWith('...')) { paramString = paramString.substring(3); } // Handle regular parameters: "name: Type = default" -> "name" // Split by colon first to remove type annotation const beforeColon = paramString.split(':')[0].trim(); // Then split by equals to remove default value const beforeEquals = beforeColon.split('=')[0].trim(); // Extract just the parameter name (handle any remaining special characters) const match = beforeEquals.match(/(\w+)/); return match ? match[1] : ''; } /** * Executes an async function with OpenTelemetry tracing immediately. * * This function automatically: * - Creates a new span for the function execution. * - Sets the function arguments as an 'input' attribute on the span. * - Sets the function's return value as an 'output' attribute. * - Records any exceptions and sets the span status to ERROR. * - Ends the span. * * The wrapped function can access the active span using `getCurrentSpan()`, allowing * for custom attributes or events to be added. If 'input' or 'output' attributes * are set within the wrapped function, the automatic attributes will not be applied. * * @param functionName The name for the span. * @param fn The async function to be traced. * @param args The arguments to pass to the function. * @returns A promise with the result of the function execution. */ function track(functionName, fn, ...args) { const tracer = getTracer(); const spanAttributes = {}; return tracer.startActiveSpan(functionName, async (span) => { // Apply breadcrumb integration patchSpanForBreadcrumbs(span); // Proxy setAttribute to capture input/output const originalSetAttribute = span.setAttribute.bind(span); span.setAttribute = (key, value) => { if (key === 'input' || key === 'output') { spanAttributes[key] = String(value); } return originalSetAttribute(key, value); }; // Extract source code information in development mode const sourceCodeInfo = extractSourceCodeInfo(fn, functionName); if (sourceCodeInfo.functionCode) { span.setAttribute('functionCode', sourceCodeInfo.functionCode); } if (sourceCodeInfo.fileName) { span.setAttribute('sourceFile', sourceCodeInfo.fileName); span.setAttribute('sourceLine', sourceCodeInfo.lineNumber || 0); } // Set default input attribute BEFORE function execution so it's captured even on failures if (!('input' in spanAttributes) && args.length > 0) { const input = extractParameterInput(fn, args, false); // false = don't expect span as first param span.setAttribute('input', JSON.stringify(input, null, 2)); } try { // NOW execute the function after input has been captured const result = await fn(...args); // Set default output attribute if not set by the function if (!('output' in spanAttributes) && typeof result !== 'undefined') { span.setAttribute('output', JSON.stringify(result, null, 2)); } span.setStatus({ code: api_1.SpanStatusCode.OK }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); if (!('output' in spanAttributes)) { span.setAttribute('output', JSON.stringify({ success: false, error: errorMessage, })); } throw error; } finally { span.end(); } }); } /** * Wraps an async function with OpenTelemetry tracing and returns a callable function. * * This function creates a wrapper that can be called multiple times. * Use this when you need to create reusable traced functions. * * @param functionName The name for the span. * @param fn The async function to be traced. * @returns A new async function with tracing enabled. */ function trackWrapper(functionName, fn) { return async (...args) => { return track(functionName, fn, ...args); }; } /** * Wraps all functions in an object with OpenTelemetry tracing. * * This function iterates over an object of functions and applies the `trackWrapper` * decorator to each one, using the function's key as the span name. * * @param functions An object where keys are function names and values are async functions to be traced. * @returns A new object with the same keys, where each function is wrapped with tracing. */ function trackAll(functions) { const trackedFunctions = {}; for (const key in functions) { if (Object.prototype.hasOwnProperty.call(functions, key)) { const fn = functions[key]; trackedFunctions[key] = trackWrapper(key, fn); } } return trackedFunctions; } /** * Simple function to add custom attributes to the current active span */ function addSpanAttributes(attributes) { const span = api_1.trace.getActiveSpan(); if (span) { Object.entries(attributes).forEach(([key, value]) => { span.setAttribute(key, value); }); } } /** * Simple function to add an event to the current active span */ function addSpanEvent(name, attributes) { const span = api_1.trace.getActiveSpan(); if (span) { span.addEvent(name, attributes); } } /** * Gets the current active span (useful inside @TrackClass methods) * Returns undefined if no span is currently active */ function getCurrentSpan() { return api_1.trace.getActiveSpan(); } /** * Create a manual span for custom tracing scenarios */ function createSpan(name, fn, attributes) { const tracer = getTracer(); return tracer.startActiveSpan(name, async (span) => { try { // Apply breadcrumb integration patchSpanForBreadcrumbs(span); // if (attributes) { // Object.entries(attributes).forEach(([key, value]) => { // span.setAttribute(key, value) // }) // } const result = await fn(span); span.setStatus({ code: api_1.SpanStatusCode.OK }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); throw error; } finally { span.end(); } }); } /** * Gets all method names from a class prototype, excluding built-in Object methods */ function getClassMethodNames(prototype) { const methods = new Set(); let currentProto = prototype; // Walk up the prototype chain but stop at Object.prototype while (currentProto && currentProto !== Object.prototype) { Object.getOwnPropertyNames(currentProto).forEach(name => { if (name !== 'constructor' && typeof currentProto[name] === 'function') { methods.add(name); } }); currentProto = Object.getPrototypeOf(currentProto); } return Array.from(methods); } /** * Filters method names based on include/exclude options */ function filterMethodNames(methods, options) { let filteredMethods = [...methods]; if (options.includeMethods && options.includeMethods.length > 0) { filteredMethods = filteredMethods.filter(method => options.includeMethods.includes(method)); } if (options.excludeMethods && options.excludeMethods.length > 0) { filteredMethods = filteredMethods.filter(method => !options.excludeMethods.includes(method)); } return filteredMethods; } /** * Class decorator that automatically instruments ALL methods of a class (both static and instance). * Also tracks internal calls (this.method() and ClassName.staticMethod()). * * Features: * - Automatic span creation for each method call * - Input/output parameter extraction and logging * - Source code extraction in development mode (like track functions) * - Support for both sync and async methods * - Breadcrumb integration support * - Access to current span via getCurrentSpan() within methods * * Usage: * ```typescript * @TrackClass() * class MyService { * async getData() { * const span = getCurrentSpan() // ✅ Access current span * span?.addEvent('Custom event') * return "data" * } * async processData(data: string) { * return MyService.staticHelper(data.toUpperCase()) // ✅ Also tracked! * } * static async staticHelper(input: string) { return input } // ✅ Tracked! * } * ``` * * @param options Configuration options for tracking */ function TrackClass(options = {}) { return function (constructor) { const className = constructor.name; const spanPrefix = options.spanPrefix || className; const icon = options.icon; const iconColor = options.iconColor; const allowRemoteRun = options.allowRemoteRun && process.env.NODE_ENV !== 'production'; // Get instance method names from the prototype const instanceMethodNames = getClassMethodNames(constructor.prototype); const filteredInstanceMethods = filterMethodNames(instanceMethodNames, options); // Get static method names from the constructor const staticMethodNames = Object.getOwnPropertyNames(constructor) .filter(name => name !== 'prototype' && name !== 'name' && name !== 'length') .filter(name => typeof constructor[name] === 'function'); const filteredStaticMethods = filterMethodNames(staticMethodNames, options); // APPROACH: Modify the original constructor in-place to track static methods // This ensures that internal calls like ExampleAPI.getName() use tracked versions const originalStaticMethods = {}; filteredStaticMethods.forEach(methodName => { const originalMethod = constructor[methodName]; if (typeof originalMethod === 'function') { // Store original and replace with tracked version originalStaticMethods[methodName] = originalMethod; // Create the tracked version const trackedMethod = createTrackedStaticMethod(originalMethod, methodName, spanPrefix, icon, iconColor, constructor, allowRemoteRun); // Register for remote execution if enabled - use the TRACKED version if (allowRemoteRun) { registerRemoteExecutable({ className, methodName, isStatic: true, originalFunction: trackedMethod, // ✅ Store the WRAPPED version constructor, spanPrefix, icon, iconColor }); } try { // Replace the static method on the original constructor Object.defineProperty(constructor, methodName, { value: trackedMethod, writable: true, enumerable: true, configurable: true }); } catch (error) { console.warn(`[TrackClass] Could not replace static method ${methodName}:`, error); // Fallback: just store the original method } } }); // Wrap instance methods in the prototype filteredInstanceMethods.forEach(methodName => { const originalMethod = constructor.prototype[methodName]; if (typeof originalMethod === 'function') { constructor.prototype[methodName] = function (...args) { return createTrackedInstanceMethod(originalMethod, methodName, spanPrefix, icon, iconColor).apply(this, args); }; } }); // Return the modified original constructor return constructor; }; } /** * Creates a tracked version of an instance method */ function createTrackedInstanceMethod(originalMethod, methodName, spanPrefix, icon, iconColor) { const spanName = `${spanPrefix}.${methodName}`; // Extract source code information ONCE during decorator application, not during execution // This ensures we capture the actual definition location, not the call site const sourceCodeInfo = extractSourceCodeInfo(originalMethod, methodName); return function (...args) { // Don't pre-execute! Execute once within the span const tracer = getTracer(); return tracer.startActiveSpan(spanName, (span) => { try { // Apply breadcrumb integration patchSpanForBreadcrumbs(span); // Set icon attribute - use provided icon or default to nodejs span.setAttribute('span.icon', icon || 'logos:nodejs-icon-alt'); if (iconColor) { span.setAttribute('span.iconColor', iconColor); } span.setAttribute('method.type', 'instance'); // Use the pre-extracted source code information if (sourceCodeInfo.functionCode) { span.setAttribute('functionCode', sourceCodeInfo.functionCode); } if (sourceCodeInfo.fileName) { span.setAttribute('sourceFile', sourceCodeInfo.fileName); span.setAttribute('sourceLine', sourceCodeInfo.lineNumber || 0); } // Extract parameter names from original method signature const input = extractParameterInput(originalMethod, args, false); // false = don't expect span param span.setAttribute('input', JSON.stringify(input, null, 2)); // Execute the original method ONCE const result = originalMethod.apply(this, args); if (result instanceof Promise) { // If it's a Promise, handle it asynchronously span.setAttribute('method.sync', false); return result.then((resolvedValue) => { span.setAttribute('output', JSON.stringify(resolvedValue, null, 2)); span.setStatus({ code: api_1.SpanStatusCode.OK }); span.end(); return resolvedValue; }, (error) => { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); span.end(); throw error; }); } else { // Synchronous result span.setAttribute('method.sync', true); if (typeof result !== 'undefined') { span.setAttribute('output', JSON.stringify(result, null, 2)); } span.setStatus({ code: api_1.SpanStatusCode.OK }); span.end(); return result; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); span.end(); throw error; } }); }; } /** * Creates a tracked version of a static method */ function createTrackedStaticMethod(originalMethod, methodName, spanPrefix, icon, iconColor, constructor, allowRemoteRun = false) { const spanName = `${spanPrefix}.${methodName}`; const className = constructor.name; // Extract source code information ONCE during decorator application, not during execution // This ensures we capture the actual definition location, not the call site const sourceCodeInfo = extractSourceCodeInfo(originalMethod, methodName); return function (...args) { // Don't pre-execute! Instead, check the function signature or use a different approach const tracer = getTracer(); return tracer.startActiveSpan(spanName, (span) => { try { // Apply breadcrumb integration patchSpanForBreadcrumbs(span); // Set icon attribute - use provided icon or default to nodejs span.setAttribute('span.icon', icon || 'logos:nodejs-icon-alt'); if (iconColor) { span.setAttribute('span.iconColor', iconColor); } span.setAttribute('method.sync', true); span.setAttribute('method.static', true); span.setAttribute('method.type', 'static'); // Add remote execution metadata (only in non-production environments) if (allowRemoteRun && process.env.NODE_ENV !== 'production') { span.setAttribute('remote.executable', true); span.setAttribute('remote.className', spanPrefix); // for UI only not key. span.setAttribute('remote.methodName', methodName); span.setAttribute('remote.key', `${className}.${methodName}`); } else { console.log(`[OTEL Remote] ⚠️ Remote execution is disabled in production mode`); } // Use the pre-extracted source code information if (sourceCodeInfo.functionCode) { span.setAttribute('functionCode', sourceCodeInfo.functionCode); } if (sourceCodeInfo.fileName) { span.setAttribute('sourceFile', sourceCodeInfo.fileName); span.setAttribute('sourceLine', sourceCodeInfo.lineNumber || 0); } // Extract parameter names from original method signature const input = extractParameterInput(originalMethod, args, false); // false = don't expect span param span.setAttribute('input', JSON.stringify(input, null, 2)); // Execute the original method ONCE const result = originalMethod.apply(constructor, args); if (result instanceof Promise) { // If it's a Promise, handle it asynchronously return result.then((resolvedValue) => { span.setAttribute('output', JSON.stringify(resolvedValue, null, 2)); span.setStatus({ code: api_1.SpanStatusCode.OK }); span.end(); return resolvedValue; }, (error) => { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); span.end(); throw error; }); } else { // Synchronous result if (typeof result !== 'undefined') { span.setAttribute('output', JSON.stringify(result, null, 2)); } span.setStatus({ code: api_1.SpanStatusCode.OK }); span.end(); return result; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage }); span.end(); throw error; } }); }; } //# sourceMappingURL=tracing-utils.js.map