@erickluis00/otelviewer
Version:
Shared OpenTelemetry tracing utilities, types, and batch processor for Realtime OpenTelemetry Viewer [WIP]
1,025 lines • 44.4 kB
JavaScript
;
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