autotel
Version:
Write Once, Observe Anywhere
1,315 lines (1,312 loc) • 46.2 kB
JavaScript
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