UNPKG

autotel

Version:
1,315 lines (1,312 loc) 46.2 kB
const require_chunk = require('./chunk-C_NdSu1c.cjs'); const require_sampling = require('./sampling.cjs'); const require_init = require('./init-BXiuPK6j.cjs'); const require_config = require('./config.cjs'); const require_track = require('./track-Cb3Q4QmS.cjs'); const require_trace_helpers = require('./trace-helpers.cjs'); const require_operation_context = require('./operation-context-D6LDf4W_.cjs'); let _opentelemetry_api = require("@opentelemetry/api"); let node_fs = require("node:fs"); node_fs = require_chunk.__toESM(node_fs, 1); let node_url = require("node:url"); node_url = require_chunk.__toESM(node_url, 1); //#region src/variable-name-inference.ts /** * Variable Name Inference Utility * * Attempts to infer variable names from const/export const assignments * by analyzing the call stack and parsing source code. * * This is a best-effort approach with graceful degradation - if inference * fails for any reason, it returns undefined without breaking the application. */ /** * LRU Cache for inferred variable names * Key: "file:line" (e.g., "/path/to/file.ts:42") * Value: inferred variable name or undefined */ const inferenceCache = /* @__PURE__ */ new Map(); const MAX_CACHE_SIZE = 50; /** * Captures the current call stack */ function captureStackTrace() { const originalStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 10; const stack = (/* @__PURE__ */ new Error("Stack trace capture")).stack || ""; Error.stackTraceLimit = originalStackTraceLimit; return stack; } /** * Parses the stack trace to find where trace() was called * * Stack trace format (Node.js): * at functionName (file:line:column) * at file:line:column * * We skip frames until we find one that's NOT in functional.ts or this file. * We also need to skip one additional frame (the trace/span/instrument function itself) * to get to the actual user code. */ function parseCallLocation(stack) { const lines = stack.split("\n"); let skippedExternalFrame = false; for (const line of lines) { if (line.includes("variable-name-inference.ts") || line.includes("variable-name-inference.js") || line.includes("functional.ts") || line.includes("functional.js")) continue; const match = line.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/) || line.match(/^.*?([^:]+):(\d+):(\d+)/); if (match) { let filePath = match[1].trim(); if (filePath.startsWith("file://")) try { filePath = node_url.fileURLToPath(filePath); } catch { continue; } if (!skippedExternalFrame) { skippedExternalFrame = true; continue; } return { file: filePath, line: Number.parseInt(match[2], 10), column: Number.parseInt(match[3], 10) }; } } } /** * Reads a specific line from a source file */ function readSourceLine(filePath, lineNumber) { try { if (typeof node_fs.readFileSync !== "function") return; return node_fs.readFileSync(filePath, "utf8").split("\n")[lineNumber - 1]; } catch { return; } } /** * Extracts variable name from source code line using regex patterns * * Supported patterns: * - const varName = anyFunction( * - export const varName = anyFunction( * - let varName = anyFunction( * - var varName = anyFunction( * * Note: This won't work with destructuring assignments or complex patterns */ function extractVariableName(sourceLine) { const trimmed = sourceLine.trim(); for (const pattern of [ /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, /const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, /export\s+let\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, /let\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, /export\s+var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, /var\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/ ]) { const match = trimmed.match(pattern); if (match && match[1]) return match[1]; } } /** * Adds an entry to the cache with LRU eviction */ function cacheInference(key, value) { if (inferenceCache.size >= MAX_CACHE_SIZE) { const firstKey = inferenceCache.keys().next().value; if (firstKey) inferenceCache.delete(firstKey); } inferenceCache.set(key, value); } /** * Main entry point: Attempts to infer the variable name from the call stack * * This function: * 1. Captures the call stack * 2. Parses it to find where trace() was called (file + line) * 3. Reads that line from the source file * 4. Extracts the variable name using regex * * Returns undefined if inference fails at any step (graceful degradation). * Results are cached to avoid repeated file I/O. * * @returns The inferred variable name, or undefined if inference failed */ function inferVariableNameFromCallStack() { try { const callLocation = parseCallLocation(captureStackTrace()); if (!callLocation) return; const cacheKey = `${callLocation.file}:${callLocation.line}`; if (inferenceCache.has(cacheKey)) return inferenceCache.get(cacheKey); const sourceLine = readSourceLine(callLocation.file, callLocation.line); if (!sourceLine) return; const variableName = extractVariableName(sourceLine); cacheInference(cacheKey, variableName); return variableName; } catch { return; } } //#endregion //#region src/functional.ts /** * Functional API for non-class code * * Three approaches for different use cases: * 1. trace() - Zero-ceremony HOF for single functions * 2. withTracing() - Middleware-style composable wrapper * 3. instrument() - Batch auto-instrumentation for modules * * @example trace() - Single function * ```typescript * export const createUser = trace(ctx => async (data) => { * ctx.setAttribute('user.id', data.id) * return await db.users.create(data) * }) * ``` * * @example withTracing() - Composable middleware * ```typescript * export const createUser = withTracing({ * name: 'user.create' * })(ctx => async (data) => { * ctx.setAttribute('user.id', data.id) * return await db.users.create(data) * }) * ``` * * @example instrument() - Batch instrumentation * ```typescript * export default instrument({ * createUser: async (data) => { }, * updateUser: async (id, data) => { } * }, { serviceName: 'user' }) * ``` */ const FACTORY_NAME_HINTS = new Set([ "ctx", "_ctx", "context", "tracecontext", "tracectx" ]); const TRACE_FACTORY_SET = /* @__PURE__ */ new WeakSet(); const SINGLE_LINE_COMMENT_REGEX = /\/\/.*$/gm; const MULTI_LINE_COMMENT_REGEX = /\/\*[\s\S]*?\*\//gm; const PARAM_TOKEN_SANITIZE_REGEX = new RegExp(String.raw`[{}\[\]\s]`, "g"); function markAsTraceFactory(fn) { TRACE_FACTORY_SET.add(fn); } function hasFactoryMark(fn) { return TRACE_FACTORY_SET.has(fn); } function sanitizeParameterToken(token) { const [firstToken] = token.split("="); return (firstToken ?? "").replaceAll(PARAM_TOKEN_SANITIZE_REGEX, "").trim(); } function getFirstParameterToken(fn) { let source = Function.prototype.toString.call(fn); source = source.replaceAll(MULTI_LINE_COMMENT_REGEX, "").replaceAll(SINGLE_LINE_COMMENT_REGEX, "").trim(); const arrowMatch = source.match(/^(?:async\s*)?(?:\(([^)]*)\)|([^=()]+))\s*=>/); if (arrowMatch) { const first = (arrowMatch[1] ?? arrowMatch[2] ?? "").split(",")[0]?.trim(); if (first) return sanitizeParameterToken(first); return null; } const functionMatch = source.match(/^[^(]*\(([^)]*)\)/); if (functionMatch) { const first = (functionMatch[1]?.split(","))?.[0]?.trim(); if (first) return sanitizeParameterToken(first); } return null; } /** * Symbol that explicitly marks a function as immediate-execution-with-ctx * (`(ctx) => result`), bypassing parameter-name introspection. Library * authors who wrap user handlers — like `autotel-aws/lambda`'s `wrapHandler` * — should mark their inner trace function with this so dispatch survives * downstream bundlers that minify parameter names. */ const IMMEDIATE_EXECUTION_SYMBOL = Symbol.for("autotel.immediate-execution"); function hasImmediateExecutionMark(fn) { return typeof fn === "function" && fn[IMMEDIATE_EXECUTION_SYMBOL] === true; } /** * Mark a function as immediate-execution-with-ctx so `trace(name, fn)` * dispatch doesn't depend on the first parameter being named `ctx`. * * Necessary when the function will be bundled by a minifier (esbuild, * terser, etc.) that renames identifiers. The name-allowlist heuristic in * `looksLikeTraceFactory` cannot recover from that; the marker can. * * @example * ```ts * import { markAsImmediate, trace } from 'autotel'; * * const inner = markAsImmediate(async (ctx) => { * ctx.setAttribute('user.id', '123'); * return { ok: true }; * }); * const result = await trace('user.read', inner); * ``` */ function markAsImmediate(fn) { if (typeof fn === "function") fn[IMMEDIATE_EXECUTION_SYMBOL] = true; return fn; } function looksLikeTraceFactory(fn) { if (hasFactoryMark(fn)) return true; if (hasImmediateExecutionMark(fn)) return true; if (fn.length === 0) { if (!isAsyncFunction(fn)) try { return typeof fn() === "function"; } catch { return false; } return false; } const firstParam = getFirstParameterToken(fn); if (!firstParam) return false; const normalized = firstParam.toLowerCase(); if (FACTORY_NAME_HINTS.has(normalized) || normalized.startsWith("ctx") || normalized.startsWith("_ctx") || normalized.startsWith("trace") || normalized.endsWith("ctx") || normalized.includes("context")) return true; return false; } /** * Check if a function that takes ctx returns another function (factory pattern) * vs returning a value directly (immediate execution pattern) * * IMPORTANT: For async functions, we skip probing entirely and assume immediate execution. * This is because: * - Factory pattern: `(ctx) => async (...args) => result` - outer function is SYNC * - Immediate execution: `async (ctx) => result` - function itself is ASYNC * * Probing async functions by executing them causes side effects (like creating orphan spans) * because the async function starts executing synchronously until the first await. */ function isFactoryReturningFunction(fnWithCtx) { if (isAsyncFunction(fnWithCtx)) return false; try { return typeof fnWithCtx(createDummyCtx()) === "function"; } catch { return false; } } function isTraceFactoryFunction(fn) { if (typeof fn !== "function") return false; if (hasFactoryMark(fn)) return true; if (looksLikeTraceFactory(fn)) { markAsTraceFactory(fn); return true; } return false; } function ensureTraceFactory(fnOrFactory) { if (isTraceFactoryFunction(fnOrFactory)) return fnOrFactory; const plainFn = fnOrFactory; const factory = (ctx) => { return plainFn; }; markAsTraceFactory(factory); return factory; } function wrapFactoryWithTracing(fnOrFactory, options, variableName) { const factory = ensureTraceFactory(fnOrFactory); const sampleFn = factory(createDummyCtx()); const innerFunctionName = inferFunctionName(sampleFn); const callStackVariableName = innerFunctionName ? void 0 : inferVariableNameFromCallStack(); const factoryName = inferFunctionName(factory); const effectiveVariableName = variableName || innerFunctionName || callStackVariableName || factoryName; if (isAsyncFunction(sampleFn)) return wrapWithTracing(factory, options, effectiveVariableName); return wrapWithTracingSync(factory, options, effectiveVariableName); } const MAX_ERROR_MESSAGE_LENGTH = 500; function createDummyCtx() { return { traceId: "", spanId: "", correlationId: "", setAttribute: () => {}, setAttributes: () => {}, setStatus: () => {}, recordException: () => {}, addEvent: () => {}, addLink: () => {}, addLinks: () => {}, updateName: () => {}, isRecording: () => false, getBaggage: () => {}, setBaggage: () => "", deleteBaggage: () => {}, getAllBaggage: () => /* @__PURE__ */ new Map() }; } /** Attribute keys for opt-in function I/O capture (see TracingOptions). */ const AUTOTEL_INPUT_ATTR = "autotel.input"; const AUTOTEL_OUTPUT_ATTR = "autotel.output"; const CAPTURE_MAX_CHARS = 4096; /** JSON-serialize a captured value, defensively (truncate, swallow cycles). */ function serializeCapture(value) { if (value === void 0) return void 0; try { const json = typeof value === "string" ? value : JSON.stringify(value); if (json === void 0) return void 0; return json.length > CAPTURE_MAX_CHARS ? `${json.slice(0, CAPTURE_MAX_CHARS)}…[truncated]` : json; } catch { return; } } /** `autotel.input` from args (single arg captured directly, else the array). */ function captureInputAttrs(args, enabled) { if (!enabled) return {}; const s = serializeCapture(args.length === 1 ? args[0] : args); return s === void 0 ? {} : { [AUTOTEL_INPUT_ATTR]: s }; } /** `autotel.output` from the return value. */ function captureOutputAttrs(result, enabled) { if (!enabled) return {}; const s = serializeCapture(result); return s === void 0 ? {} : { [AUTOTEL_OUTPUT_ATTR]: s }; } function isAsyncFunction(fn) { return typeof fn === "function" && fn.constructor?.name === "AsyncFunction"; } const INSTRUMENTED_SYMBOL = Symbol.for("autotel.functional.instrumented"); function hasInstrumentationFlag(value) { return (typeof value === "function" || typeof value === "object") && value !== null && Boolean(value[INSTRUMENTED_SYMBOL]); } /** * Truncate error message to prevent span bloat */ function truncateErrorMessage(message) { if (message.length <= MAX_ERROR_MESSAGE_LENGTH) return message; return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}... (truncated)`; } /** * Try to infer function name from function properties * Checks for displayName, name, or other metadata that might be set */ function inferFunctionName(fn) { const displayName = fn.displayName; if (displayName) return displayName; if (fn.name && fn.name !== "anonymous" && fn.name !== "") return fn.name; const match = Function.prototype.toString.call(fn).match(/function\s+([^(\s]+)/); if (match && match[1] && match[1] !== "anonymous") return match[1]; } /** * Determine span name using priority: * 1. Explicit name option * 2. serviceName + functionName * 3. Inferred from function/variable name (including stack trace fallback) * 4. Fallback to 'unknown' */ function getSpanName(options, fn, variableName) { if (options.name) return options.name; let fnName = variableName || inferFunctionName(fn); fnName = fnName || "anonymous"; if (options.serviceName) return `${options.serviceName}.${fnName}`; if (fnName && fnName !== "anonymous") return fnName; return "unknown"; } /** * Check if function should be skipped */ function shouldSkip(key, fn, skip) { if (key.startsWith("_")) return true; if (!skip || skip.length === 0) return false; for (const rule of skip) if (typeof rule === "string" && key === rule) return true; else if (rule instanceof RegExp && rule.test(key)) return true; else if (typeof rule === "function" && rule(key, fn)) return true; return false; } /** * Get current trace context value (internal helper) * * Returns base context (trace IDs) + span methods from the active span. */ function getCtxValue() { const activeSpan = _opentelemetry_api.trace.getActiveSpan(); if (!activeSpan) return null; return require_track.createTraceContext(activeSpan); } /** * Context object that lazily evaluates the active span on property access * * Access trace context directly without function call syntax. * * @example * ```typescript * import { trace, ctx } from 'autotel' * * export const createUser = trace(async (data) => { * // Direct property access - no function call! * if (ctx.traceId) { * ctx.setAttribute('user.id', data.id) * console.log('Trace:', ctx.traceId) * } * }) * ``` */ const ctx = new Proxy({}, { get(_target, prop) { const ctxValue = getCtxValue(); if (!ctxValue) return; return ctxValue[prop]; }, has(_target, prop) { const ctxValue = getCtxValue(); if (!ctxValue) return false; return prop in ctxValue; }, ownKeys() { const ctxValue = getCtxValue(); if (!ctxValue) return []; return Object.keys(ctxValue); }, getOwnPropertyDescriptor(_target, prop) { const ctxValue = getCtxValue(); if (!ctxValue) return; return Object.getOwnPropertyDescriptor(ctxValue, prop); } }); /** * Core tracing wrapper for async functions (internal implementation) */ function wrapWithTracing(fnFactory, options, variableName) { if (hasInstrumentationFlag(fnFactory)) {} const config = require_config.getConfig(); const tracer = config.tracer; const meter = config.meter; const sampler = options.sampler || new require_sampling.AlwaysSampler(); const tempFn = fnFactory(createDummyCtx()); const spanName = getSpanName(options, tempFn, variableName); const callCounter = options.withMetrics ? meter.createCounter(`${spanName}.calls`, { description: `Call count for ${spanName}`, unit: "1" }) : void 0; const durationHistogram = options.withMetrics ? meter.createHistogram(`${spanName}.duration`, { description: `Duration for ${spanName}`, unit: "ms" }) : void 0; const wrappedFunction = async function wrappedFunction(...args) { const samplingContext = { operationName: spanName, args, metadata: {} }; const shouldSample = sampler.shouldSample(samplingContext); const needsTailSampling = "needsTailSampling" in sampler && typeof sampler.needsTailSampling === "function" ? sampler.needsTailSampling() : false; if (!shouldSample && !needsTailSampling) return await fnFactory(createDummyCtx()).call(this, ...args); const startTime = performance.now(); const isRootSpan = options.startNewRoot || _opentelemetry_api.trace.getActiveSpan() === void 0; const shouldAutoFlush = options.flushOnRootSpanEnd ?? require_init.getConfig()?.flushOnRootSpanEnd ?? true; const shouldAutoFlushSpans = require_init.getConfig()?.forceFlushOnShutdown ?? false; const flushIfNeeded = async () => { if (!shouldAutoFlush || !isRootSpan) return; try { const queue = require_track.getEventQueue(); if (queue && queue.size() > 0) await queue.flush(); if (shouldAutoFlushSpans) { const sdk = require_init.getSdk(); if (sdk) try { const sdkAny = sdk; if (typeof sdkAny.getTracerProvider === "function") { const tracerProvider = sdkAny.getTracerProvider(); if (tracerProvider && typeof tracerProvider.forceFlush === "function") await tracerProvider.forceFlush(); } } catch {} } } catch (error) { const logger = require_init.getConfig()?.logger; if (logger?.error) logger.error({ err: error instanceof Error ? error : void 0 }, `[autotel] Auto-flush failed${error instanceof Error ? "" : `: ${String(error)}`}`); } }; const spanOptions = {}; if (options.startNewRoot) spanOptions.root = true; if (options.spanKind !== void 0) spanOptions.kind = options.spanKind; const parentContext = require_track.getActiveContextWithBaggage(); return tracer.startActiveSpan(spanName, spanOptions, parentContext, async (span) => { return require_operation_context.runInOperationContext(spanName, async () => { let shouldKeepSpan = true; require_trace_helpers.setSpanName(span, spanName); const initialContext = _opentelemetry_api.context.active(); const contextStorage = require_track.getContextStorage(); if (!contextStorage.getStore()) require_track.enterOrRun(contextStorage, initialContext); const fn = fnFactory(require_track.createTraceContext(span)); const argsAttributes = { ...captureInputAttrs(args, options.captureInput), ...options.attributesFromArgs ? options.attributesFromArgs(args) : {} }; const handleTailSampling = (success, duration, error) => { if (needsTailSampling && "shouldKeepTrace" in sampler && typeof sampler.shouldKeepTrace === "function") { shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, { success, duration, error }); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_EVALUATED, true); } }; const onSuccess = async (result) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "success" }); durationHistogram?.record(duration, { operation: spanName, status: "success" }); const resultAttributes = { ...captureOutputAttrs(result, options.captureOutput), ...options.attributesFromResult ? options.attributesFromResult(result) : {} }; span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.setAttributes({ ...argsAttributes, ...resultAttributes, "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": true }); handleTailSampling(true, duration); span.end(); await flushIfNeeded(); return result; }; const onError = async (error) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "error" }); durationHistogram?.record(duration, { operation: spanName, status: "error" }); const truncatedMessage = truncateErrorMessage(error instanceof Error ? error.message : "Unknown error"); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: truncatedMessage }); span.setAttributes({ ...argsAttributes, "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": false, error: true, "exception.type": error instanceof Error ? error.constructor.name : "Error", "exception.message": truncatedMessage }); if (error instanceof Error && error.stack) span.setAttribute("exception.stack", error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH)); span.recordException(error instanceof Error ? error : new Error(String(error))); handleTailSampling(false, duration, error); span.end(); await flushIfNeeded(); throw error; }; try { callCounter?.add(1, { operation: spanName, status: "started" }); const executeWithContext = async () => { const currentContext = require_track.getActiveContextWithBaggage(); return _opentelemetry_api.context.with(currentContext, async () => { return fn.call(this, ...args); }); }; return await onSuccess(await executeWithContext()); } catch (error) { await onError(error); throw error; } }); }); }; wrappedFunction[INSTRUMENTED_SYMBOL] = true; Object.defineProperty(wrappedFunction, "name", { value: tempFn.name || "trace", configurable: true }); return wrappedFunction; } /** * Core tracing wrapper for sync functions (internal implementation) */ function wrapWithTracingSync(fnFactory, options, variableName) { if (hasInstrumentationFlag(fnFactory)) {} const config = require_config.getConfig(); const tracer = config.tracer; const meter = config.meter; const sampler = options.sampler || new require_sampling.AlwaysSampler(); const tempFn = fnFactory(createDummyCtx()); const spanName = getSpanName(options, tempFn, variableName); const callCounter = options.withMetrics ? meter.createCounter(`${spanName}.calls`, { description: `Call count for ${spanName}`, unit: "1" }) : void 0; const durationHistogram = options.withMetrics ? meter.createHistogram(`${spanName}.duration`, { description: `Duration for ${spanName}`, unit: "ms" }) : void 0; function wrappedFunction(...args) { const samplingContext = { operationName: spanName, args, metadata: {} }; const shouldSample = sampler.shouldSample(samplingContext); const needsTailSampling = "needsTailSampling" in sampler && typeof sampler.needsTailSampling === "function" ? sampler.needsTailSampling() : false; if (!shouldSample && !needsTailSampling) return fnFactory(createDummyCtx()).call(this, ...args); const startTime = performance.now(); const isRootSpan = options.startNewRoot || _opentelemetry_api.trace.getActiveSpan() === void 0; const shouldAutoFlush = options.flushOnRootSpanEnd ?? require_init.getConfig()?.flushOnRootSpanEnd ?? true; const shouldAutoFlushSpans = require_init.getConfig()?.forceFlushOnShutdown ?? false; const flushIfNeeded = () => { if (!shouldAutoFlush || !isRootSpan) return; const queue = require_track.getEventQueue(); if (queue && queue.size() > 0) queue.flush().catch((error) => { const logger = require_init.getConfig()?.logger; if (logger?.error) logger.error({ err: error instanceof Error ? error : void 0 }, `[autotel] Auto-flush failed${error instanceof Error ? "" : `: ${String(error)}`}`); }); if (shouldAutoFlushSpans) { const sdk = require_init.getSdk(); if (sdk) try { const sdkAny = sdk; if (typeof sdkAny.getTracerProvider === "function") { const tracerProvider = sdkAny.getTracerProvider(); if (tracerProvider && typeof tracerProvider.forceFlush === "function") tracerProvider.forceFlush().catch((error) => { const logger = require_init.getConfig()?.logger; if (logger?.error) logger.error({ err: error instanceof Error ? error : void 0 }, `[autotel] Span flush failed${error instanceof Error ? "" : `: ${String(error)}`}`); }); } } catch {} } }; const spanOptions = {}; if (options.startNewRoot) spanOptions.root = true; if (options.spanKind !== void 0) spanOptions.kind = options.spanKind; const parentContext = require_track.getActiveContextWithBaggage(); return tracer.startActiveSpan(spanName, spanOptions, parentContext, (span) => { return require_operation_context.runInOperationContext(spanName, () => { let shouldKeepSpan = true; require_trace_helpers.setSpanName(span, spanName); const fn = fnFactory(require_track.createTraceContext(span)); const argsAttributes = { ...captureInputAttrs(args, options.captureInput), ...options.attributesFromArgs ? options.attributesFromArgs(args) : {} }; const handleTailSampling = (success, duration, error) => { if (needsTailSampling && "shouldKeepTrace" in sampler && typeof sampler.shouldKeepTrace === "function") { shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, { success, duration, error }); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_EVALUATED, true); } }; const onSuccess = (result) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "success" }); durationHistogram?.record(duration, { operation: spanName, status: "success" }); const resultAttributes = { ...captureOutputAttrs(result, options.captureOutput), ...options.attributesFromResult ? options.attributesFromResult(result) : {} }; span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.setAttributes({ ...argsAttributes, ...resultAttributes, "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": true }); handleTailSampling(true, duration); span.end(); flushIfNeeded(); return result; }; const onError = (error) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "error" }); durationHistogram?.record(duration, { operation: spanName, status: "error" }); const truncatedMessage = truncateErrorMessage(error instanceof Error ? error.message : "Unknown error"); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: truncatedMessage }); span.setAttributes({ ...argsAttributes, "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": false, error: true, "exception.type": error instanceof Error ? error.constructor.name : "Error", "exception.message": truncatedMessage }); span.recordException(error instanceof Error ? error : new Error(String(error))); handleTailSampling(false, duration, error); span.end(); flushIfNeeded(); throw error; }; try { callCounter?.add(1, { operation: spanName, status: "started" }); const result = fn.call(this, ...args); if (result instanceof Promise) return result.then(onSuccess, onError); return onSuccess(result); } catch (error) { return onError(error); } }); }); } wrappedFunction[INSTRUMENTED_SYMBOL] = true; Object.defineProperty(wrappedFunction, "name", { value: tempFn.name || "trace", configurable: true }); return wrappedFunction; } /** * Execute a function immediately within a trace span * Used for the immediate execution pattern: trace((ctx) => result) */ function executeImmediately(fn, options) { const config = require_config.getConfig(); const tracer = config.tracer; const meter = config.meter; const sampler = options.sampler || new require_sampling.AlwaysSampler(); const spanName = options.name || "anonymous"; const samplingContext = { operationName: spanName, args: [], metadata: {} }; const shouldSample = sampler.shouldSample(samplingContext); const needsTailSampling = "needsTailSampling" in sampler && typeof sampler.needsTailSampling === "function" ? sampler.needsTailSampling() : false; if (!shouldSample && !needsTailSampling) return fn(createDummyCtx()); const startTime = performance.now(); const isRootSpan = options.startNewRoot || _opentelemetry_api.trace.getActiveSpan() === void 0; const shouldAutoFlush = options.flushOnRootSpanEnd ?? require_init.getConfig()?.flushOnRootSpanEnd ?? true; const shouldAutoFlushSpans = require_init.getConfig()?.forceFlushOnShutdown ?? false; const callCounter = options.withMetrics ? meter.createCounter(`${spanName}.calls`, { description: `Call count for ${spanName}`, unit: "1" }) : void 0; const durationHistogram = options.withMetrics ? meter.createHistogram(`${spanName}.duration`, { description: `Duration for ${spanName}`, unit: "ms" }) : void 0; const flushIfNeeded = async () => { if (!shouldAutoFlush || !isRootSpan) return; try { const queue = require_track.getEventQueue(); if (queue && queue.size() > 0) await queue.flush(); if (shouldAutoFlushSpans) { const sdk = require_init.getSdk(); if (sdk) try { const sdkAny = sdk; if (typeof sdkAny.getTracerProvider === "function") { const tracerProvider = sdkAny.getTracerProvider(); if (tracerProvider && typeof tracerProvider.forceFlush === "function") await tracerProvider.forceFlush(); } } catch {} } } catch (error) { const logger = require_init.getConfig()?.logger; if (logger?.error) logger.error({ err: error instanceof Error ? error : void 0 }, `[autotel] Auto-flush failed${error instanceof Error ? "" : `: ${String(error)}`}`); } }; const spanOptions = {}; if (options.startNewRoot) spanOptions.root = true; if (options.spanKind !== void 0) spanOptions.kind = options.spanKind; const parentContext = require_track.getActiveContextWithBaggage(); return tracer.startActiveSpan(spanName, spanOptions, parentContext, (span) => { return require_operation_context.runInOperationContext(spanName, () => { let shouldKeepSpan = true; require_trace_helpers.setSpanName(span, spanName); const ctxValue = require_track.createTraceContext(span); const handleTailSampling = (success, duration, error) => { if (needsTailSampling && "shouldKeepTrace" in sampler && typeof sampler.shouldKeepTrace === "function") { shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, { success, duration, error }); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan); span.setAttribute(require_sampling.AUTOTEL_SAMPLING_TAIL_EVALUATED, true); } }; const onSuccessSync = (result) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "success" }); durationHistogram?.record(duration, { operation: spanName, status: "success" }); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.setAttributes({ "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": true }); handleTailSampling(true, duration); span.end(); flushIfNeeded(); return result; }; const onErrorSync = (error) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "error" }); durationHistogram?.record(duration, { operation: spanName, status: "error" }); const truncatedMessage = truncateErrorMessage(error instanceof Error ? error.message : "Unknown error"); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: truncatedMessage }); span.setAttributes({ "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": false, error: true, "exception.type": error instanceof Error ? error.constructor.name : "Error", "exception.message": truncatedMessage }); if (error instanceof Error && error.stack) span.setAttribute("exception.stack", error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH)); span.recordException(error instanceof Error ? error : new Error(String(error))); handleTailSampling(false, duration, error); span.end(); flushIfNeeded(); throw error; }; const onSuccessAsync = async (result) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "success" }); durationHistogram?.record(duration, { operation: spanName, status: "success" }); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.setAttributes({ "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": true }); handleTailSampling(true, duration); span.end(); await flushIfNeeded(); return result; }; const onErrorAsync = async (error) => { const duration = performance.now() - startTime; callCounter?.add(1, { operation: spanName, status: "error" }); durationHistogram?.record(duration, { operation: spanName, status: "error" }); const truncatedMessage = truncateErrorMessage(error instanceof Error ? error.message : "Unknown error"); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: truncatedMessage }); span.setAttributes({ "operation.name": spanName, "code.function": spanName, "operation.duration": duration, "operation.success": false, error: true, "exception.type": error instanceof Error ? error.constructor.name : "Error", "exception.message": truncatedMessage }); if (error instanceof Error && error.stack) span.setAttribute("exception.stack", error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH)); span.recordException(error instanceof Error ? error : new Error(String(error))); handleTailSampling(false, duration, error); span.end(); await flushIfNeeded(); throw error; }; try { callCounter?.add(1, { operation: spanName, status: "started" }); const result = fn(ctxValue); if (result instanceof Promise) return result.then(onSuccessAsync, onErrorAsync); return onSuccessSync(result); } catch (error) { return onErrorSync(error); } }); }); } function trace(fnOrNameOrOptions, maybeFn) { if (typeof fnOrNameOrOptions === "function") { if (looksLikeTraceFactory(fnOrNameOrOptions) && !isFactoryReturningFunction(fnOrNameOrOptions)) return executeImmediately(fnOrNameOrOptions, {}); return wrapFactoryWithTracing(fnOrNameOrOptions, {}); } if (typeof fnOrNameOrOptions === "string") { if (!maybeFn) throw new Error("trace(name, fn): fn is required"); if (looksLikeTraceFactory(maybeFn) && !isFactoryReturningFunction(maybeFn)) return executeImmediately(maybeFn, { name: fnOrNameOrOptions }); return wrapFactoryWithTracing(maybeFn, { name: fnOrNameOrOptions }); } if (!maybeFn) throw new Error("trace(options, fn): fn is required"); if (looksLikeTraceFactory(maybeFn) && !isFactoryReturningFunction(maybeFn)) return executeImmediately(maybeFn, fnOrNameOrOptions); return wrapFactoryWithTracing(maybeFn, fnOrNameOrOptions); } /** * Approach 2: withTracing() - Middleware-style composable wrapper * * Returns a HOF that wraps functions with tracing. * Perfect for composition and reusable configuration. * * @example Standard usage * ```typescript * export const createUser = withTracing({ * name: 'user.create' * })(ctx => async (data) => { * ctx.setAttribute('user.id', data.id) * return await db.users.create(data) * }) * ``` * * @example Composable * ```typescript * const trace = withTracing({ serviceName: 'user' }) * * export const createUser = trace(ctx => async (data) => { }) * export const updateUser = trace(ctx => async (id, data) => { }) * ``` * * @example With other middleware * ```typescript * export const createUser = compose( * withAuth({ role: 'admin' }), * withTracing({ name: 'user.create' }), * withRateLimit({ max: 100 }) * )(ctx => async (data) => { }) * ``` */ function withTracing(options = {}) { return (fnFactory) => wrapFactoryWithTracing(fnFactory, options); } /** * Approach 3: instrument() - Batch auto-instrumentation * * Instrument an entire module/object at once. * Closest to @Instrumented decorator pattern. * * @example Basic usage * ```typescript * export default instrument({ * functions: { * createUser: async (data) => { }, * updateUser: async (id, data) => { }, * deleteUser: async (id) => { } * }, * serviceName: 'user', * sampler: new AdaptiveSampler() * }) * // → Traced as "user.createUser", "user.updateUser", "user.deleteUser" * ``` * * @example Per-function overrides * ```typescript * export default instrument({ * functions: { * createUser: async (data) => { }, * deleteUser: async (id) => { } * }, * serviceName: 'user', * overrides: { * deleteUser: { * sampler: new AlwaysSampler(), * withMetrics: true * } * } * }) * ``` * * @example Skip functions * ```typescript * export default instrument({ * functions: { * createUser: async (data) => { }, * _internal: async () => { }, // Auto-skipped (_-prefix) * deleteUser: async (id) => { } * }, * serviceName: 'user', * skip: [/^test/, (key) => key.includes('debug')] * }) * ``` */ function instrument(options) { const { functions, ...tracingOptions } = options; const instrumented = {}; for (const key of Object.keys(functions)) { const typedKey = key; const fn = functions[typedKey]; if (!fn || typeof fn !== "function") { instrumented[typedKey] = fn; continue; } if (shouldSkip(key, fn, tracingOptions.skip)) { instrumented[typedKey] = fn; continue; } const fnOptions = { ...tracingOptions, ...tracingOptions.overrides?.[key], name: tracingOptions.overrides?.[key]?.name }; const boundFn = fn.bind(functions); const fnFactory = (ctx) => { return boundFn; }; instrumented[typedKey] = wrapFactoryWithTracing(fnFactory, fnOptions, key); } return instrumented; } function span(nameOrOptions, fn) { const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions; const tracer = require_config.getConfig().tracer; const { name, attributes } = options; const executeSpan = (span) => { return require_operation_context.runInOperationContext(name, () => { try { if (attributes) for (const [key, value] of Object.entries(attributes)) span.setAttribute(key, value); const result = fn(span); if (result instanceof Promise) return result.then((resolved) => { span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.end(); return resolved; }).catch((error) => { const errorMessage = error instanceof Error ? error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH) : String(error).slice(0, MAX_ERROR_MESSAGE_LENGTH); span.setAttribute("error.message", errorMessage); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: errorMessage }); span.recordException(error instanceof Error ? error : new Error(String(error))); span.end(); throw error; }); else { span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); span.end(); return result; } } catch (error) { const errorMessage = error instanceof Error ? error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH) : String(error).slice(0, MAX_ERROR_MESSAGE_LENGTH); span.setAttribute("error.message", errorMessage); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR, message: errorMessage }); span.recordException(error instanceof Error ? error : new Error(String(error))); span.end(); throw error; } }); }; const parentContext = require_track.getActiveContextWithBaggage(); const result = tracer.startActiveSpan(name, {}, parentContext, executeSpan); if (result instanceof Promise) return result; return result; } /** * Execute a function in a new root context (prevents span propagation) * * Useful when you want to start a completely new trace without * parent-child relationships. * * @example * ```typescript * async function handleWebhook(payload: WebhookPayload) { * // This creates a new root trace, not connected to the HTTP request trace * await withNewContext({ * fn: async () => { * await trace(ctx => async () => { * await processWebhookPayload(payload) * })() * } * }) * } * ``` */ async function withNewContext(options) { const { fn } = options; return require_config.getConfig().tracer.startActiveSpan("root", { root: true }, async (span) => { try { const result = await fn(); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK }); return result; } catch (error) { span.recordException(error instanceof Error ? error : new Error(String(error))); span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR }); throw error; } finally { span.end(); } }); } /** * Execute a function with updated baggage entries * * Baggage is immutable in OpenTelemetry, so this helper creates a new context * with the specified baggage entries and runs the function within that context. * All child spans created within the function will inherit the baggage. * * @example Setting baggage for downstream services * ```typescript * import { trace, withBaggage } from 'autotel'; * * export const createOrder = trace((ctx) => async (order: Order) => { * // Set baggage that will be propagated to downstream HTTP calls * return await withBaggage({ * baggage: { * 'tenant.id': order.tenantId, * 'user.id': order.userId, * }, * fn: async () => { * // This HTTP call will include the baggage in headers * await fetch('/api/charge', { * method: 'POST', * body: JSON.stringify(order), * }); * }, * }); * }); * ``` * * @example Using with existing baggage * ```typescript * export const processOrder = trace((ctx) => async (order: Order) => { * // Read existing baggage * const tenantId = ctx.getBaggage('tenant.id'); * * // Add additional baggage entries * return await withBaggage({ * baggage: { * 'order.id': order.id, * 'order.amount': String(order.amount), * }, * fn: async () => { * await charge(order); * }, * }); * }); * ``` */ function withBaggage(options) { const { baggage: baggageEntries, fn } = options; const currentContext = _opentelemetry_api.context.active(); let updatedBaggage = _opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage(); for (const [key, value] of Object.entries(baggageEntries)) updatedBaggage = updatedBaggage.setEntry(key, { value }); const newContext = _opentelemetry_api.propagation.setBaggage(currentContext, updatedBaggage); const ctxStorage = require_track.getContextStorage(); const previousStored = ctxStorage.getStore(); const baggageEnrichedStored = previousStored ? { value: _opentelemetry_api.propagation.setBaggage(previousStored.value, updatedBaggage) } : { value: newContext }; const result = previousStored ? ctxStorage.run(baggageEnrichedStored, () => _opentelemetry_api.context.with(newContext, fn)) : _opentelemetry_api.context.with(newContext, fn); if (result instanceof Promise) return result.then((value) => { if (previousStored) return ctxStorage.run(previousStored, () => value); return value; }, (error) => { if (previousStored) return ctxStorage.run(previousStored, () => { throw error; }); throw error; }); return result; } //#endregion Object.defineProperty(exports, 'ctx', { enumerable: true, get: function () { return ctx; } }); Object.defineProperty(exports, 'instrument', { enumerable: true, get: function () { return instrument; } }); Object.defineProperty(exports, 'markAsImmediate', { enumerable: true, get: function () { return markAsImmediate; } }); Object.defineProperty(exports, 'span', { enumerable: true, get: function () { return span; } }); Object.defineProperty(exports, 'trace', { enumerable: true, get: function () { return trace; } }); Object.defineProperty(exports, 'withBaggage', { enumerable: true, get: function () { return withBaggage; } }); Object.defineProperty(exports, 'withNewContext', { enumerable: true, get: function () { return withNewContext; } }); Object.defineProperty(exports, 'withTracing', { enumerable: true, get: function () { return withTracing; } }); //# sourceMappingURL=functional-C8B0Qa7o.cjs.map