autotel
Version:
Write Once, Observe Anywhere
840 lines (837 loc) • 29.5 kB
JavaScript
import { getForceFlushableProvider } from "./tracer-provider.js";
import { getConfig } from "./config.js";
import { AUTOTEL_SAMPLING_RATE, AUTOTEL_SAMPLING_TAIL_EVALUATED, AUTOTEL_SAMPLING_TAIL_KEEP, AlwaysSampler } from "./sampling.js";
import { a as getSdk, n as getConfig$1 } from "./init-CdEWjhvl.js";
import { g as getContextStorage, h as getActiveContextWithBaggage, p as createTraceContext, t as getEventQueue } from "./track-BBcxEJEC.js";
import { setSpanName } from "./trace-helpers.js";
import { n as runInOperationContext } from "./operation-context-CKBoA4Qy.js";
import { SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
import * as nodeFs from "node:fs";
import * as nodeUrl from "node:url";
//#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 = nodeUrl.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 nodeFs.readFileSync !== "function") return;
return nodeFs.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(async (data) => {
* getActiveTraceContext()?.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' })
* ```
*/
let unknownSpanNameWarningEmitted = false;
function resolveVariableName(named, variableName) {
const suppliedFunctionName = inferFunctionName(named);
const callStackVariableName = suppliedFunctionName ? void 0 : inferVariableNameFromCallStack();
return variableName || suppliedFunctionName || callStackVariableName;
}
/**
* Wrap an explicit context factory `(ctx) => (...args) => result`.
* Used by {@link withTracing}; the input is a factory by contract, so there is
* no plain-vs-factory detection. The factory is never invoked at construction
* time — only when the wrapper is called.
*/
function wrapFactoryWithTracing(factory, options, variableName) {
return wrapWithTracingSync(factory, options, resolveVariableName(factory, variableName));
}
/**
* Wrap a plain function `(...args) => result`. Used by {@link trace} and
* {@link instrument}: the function receives its real arguments and no context
* is injected. Reach the active span via {@link getActiveTraceContext} inside
* the body (or any helper it calls).
*/
function wrapPlainWithTracing(fn, options, variableName) {
const effectiveVariableName = resolveVariableName(fn, variableName);
const factory = (_ctx) => fn;
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 };
}
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)`;
}
/**
* Terminal handling for a value thrown by a traced function. Shared by all four
* `trace()` execution paths (immediate + wrapper, sync + async) so the outcome
* rules live in exactly one place. Ends the span; the caller flushes (await/void)
* and rethrows.
*
* If `isError` classifies the throw as expected control flow — e.g. a framework
* `redirect()` / `notFound()` signal — it is recorded as a success outcome with
* an OK status and no exception. Otherwise it is recorded as an error.
*/
function finalizeThrownSpan(error, isError, ctx) {
const { span, spanName, duration, callCounter, durationHistogram, handleTailSampling, extraAttributes } = ctx;
const baseAttributes = {
...extraAttributes,
"operation.name": spanName,
"code.function": spanName,
"operation.duration": duration
};
if (isError && !isError(error)) {
callCounter?.add(1, {
operation: spanName,
status: "success"
});
durationHistogram?.record(duration, {
operation: spanName,
status: "success"
});
span.setStatus({ code: SpanStatusCode.OK });
span.setAttributes({
...baseAttributes,
"operation.success": true
});
handleTailSampling(true, duration);
span.end();
return;
}
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: SpanStatusCode.ERROR,
message: truncatedMessage
});
span.setAttributes({
...baseAttributes,
"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));
const thrown = error instanceof Error ? error : new Error(String(error));
span.recordException(thrown);
handleTailSampling(false, duration, thrown);
span.end();
}
/**
* 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;
const initConfig = getConfig$1();
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && !unknownSpanNameWarningEmitted && initConfig?.logger?.warn) {
unknownSpanNameWarningEmitted = true;
initConfig.logger.warn({}, "[autotel] Span name resolved to \"unknown\". Pass an explicit name, for example trace(\"operation.name\", fn).");
}
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 = trace.getActiveSpan();
if (!activeSpan) return null;
return createTraceContext(activeSpan);
}
/**
* Get the autotel {@link TraceContext} for the currently active span.
*
* This is the ambient accessor for the functional API: instead of threading a
* `ctx` parameter through a factory, call this inside any traced function (or a
* helper it calls) to reach `setAttribute`, `setUser`, `getBaggage`, and the
* rest of the context surface. Returns `undefined` when no span is active.
*
* @example
* ```typescript
* const getUser = trace('getUser', async (id: string) => {
* getActiveTraceContext()?.setAttribute('user.id', id);
* return db.users.find(id);
* });
* ```
*
* @see getActiveSpan for the raw OpenTelemetry span
* @see getRequestLogger which reads the active context when called with no args
*/
function getActiveTraceContext() {
return getCtxValue() ?? void 0;
}
/**
* 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);
}
});
/**
* Build the root-operation flush policy once and reuse it for wrapped and
* immediate execution. Async operations await this function; sync operations
* trigger it without blocking.
*/
function createRootTelemetryFlusher(options, isRootSpan) {
const initConfig = getConfig$1();
const shouldFlush = options.flushOnRootSpanEnd ?? initConfig?.flushOnRootSpanEnd ?? true;
const shouldFlushSpans = initConfig?.forceFlushOnShutdown ?? false;
return async () => {
if (!shouldFlush || !isRootSpan) return;
try {
const queue = getEventQueue();
if (queue && queue.size() > 0) await queue.flush();
if (shouldFlushSpans) await getForceFlushableProvider(getSdk())?.forceFlush();
} catch (error) {
(getConfig$1()?.logger)?.error?.({ err: error instanceof Error ? error : void 0 }, `[autotel] Auto-flush failed${error instanceof Error ? "" : `: ${String(error)}`}`);
}
};
}
/**
* Give every root operation its own baggage context without replacing a
* caller's existing scope. AsyncLocalStorage keeps the scope alive until an
* async result settles and restores the previous store afterwards.
*/
function runWithTraceContextStorage(fn) {
const storage = getContextStorage();
if (storage.getStore()) return fn();
return storage.run({ value: context.active() }, fn);
}
/**
* Core tracing wrapper for sync functions (internal implementation)
*/
function wrapWithTracingSync(fnFactory, options, variableName) {
if (hasInstrumentationFlag(fnFactory)) {}
const config = getConfig();
const tracer = config.tracer;
const meter = config.meter;
const sampler = options.sampler || new AlwaysSampler();
const spanName = getSpanName(options, fnFactory, 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 sampleRate = sampler.sampleRate?.(samplingContext);
const needsTailSampling = sampler.needsTailSampling?.() ?? false;
if (!shouldSample && !needsTailSampling) return fnFactory(createDummyCtx()).call(this, ...args);
const startTime = performance.now();
const flushRootTelemetry = createRootTelemetryFlusher(options, options.startNewRoot || trace.getActiveSpan() === void 0);
const spanOptions = {};
if (options.startNewRoot) spanOptions.root = true;
if (options.spanKind !== void 0) spanOptions.kind = options.spanKind;
const parentContext = getActiveContextWithBaggage();
return tracer.startActiveSpan(spanName, spanOptions, parentContext, (span) => {
return runInOperationContext(spanName, () => runWithTraceContextStorage(() => {
let shouldKeepSpan = true;
setSpanName(span, spanName);
if (sampleRate !== void 0 && sampleRate > 1) span.setAttribute(AUTOTEL_SAMPLING_RATE, sampleRate);
const fn = fnFactory(createTraceContext(span));
const argsAttributes = {
...captureInputAttrs(args, options.captureInput),
...options.attributesFromArgs ? options.attributesFromArgs(args) : {}
};
const handleTailSampling = (success, duration, error) => {
if (needsTailSampling && sampler.shouldKeepTrace) {
shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {
success,
duration,
error
});
span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
span.setAttribute(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: SpanStatusCode.OK });
span.setAttributes({
...argsAttributes,
...resultAttributes,
"operation.name": spanName,
"code.function": spanName,
"operation.duration": duration,
"operation.success": true
});
handleTailSampling(true, duration);
span.end();
return result;
};
const onError = (error) => {
finalizeThrownSpan(error, options.isError, {
span,
spanName,
duration: performance.now() - startTime,
callCounter,
durationHistogram,
handleTailSampling,
extraAttributes: argsAttributes
});
throw error;
};
try {
callCounter?.add(1, {
operation: spanName,
status: "started"
});
const result = context.with(getActiveContextWithBaggage(), () => fn.call(this, ...args));
if (result instanceof Promise) return result.then(async (value) => {
const completed = onSuccess(value);
await flushRootTelemetry();
return completed;
}, async (error) => {
try {
return onError(error);
} finally {
await flushRootTelemetry();
}
});
const completed = onSuccess(result);
flushRootTelemetry();
return completed;
} catch (error) {
try {
return onError(error);
} finally {
flushRootTelemetry();
}
}
}));
});
}
wrappedFunction[INSTRUMENTED_SYMBOL] = true;
Object.defineProperty(wrappedFunction, "name", {
value: variableName || "trace",
configurable: true
});
return wrappedFunction;
}
function trace$1(fnOrNameOrOptions, maybeFn) {
if (typeof fnOrNameOrOptions === "function") return wrapPlainWithTracing(fnOrNameOrOptions, {});
if (!maybeFn) throw new Error("trace(name|options, fn): fn is required");
return wrapPlainWithTracing(maybeFn, typeof fnOrNameOrOptions === "string" ? { name: fnOrNameOrOptions } : 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 tracer = withTracing({ serviceName: 'user' })
*
* export const createUser = tracer(ctx => async (data) => { })
* export const updateUser = tracer(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);
}
function instrument(options) {
if (!options || typeof options !== "object") throw new TypeError("instrument: expected { key, fn } or { functions: { name: fn } }");
if ("key" in options || "fn" in options) {
const { key, fn, ...tracingOptions } = options;
if (typeof key !== "string" || key.trim() === "") throw new TypeError("instrument: \"key\" must be a non-empty string in the { key, fn } form");
if (typeof fn !== "function") throw new TypeError("instrument: \"fn\" must be a function in the { key, fn } form");
return wrapPlainWithTracing(fn, tracingOptions, key);
}
const { functions, ...tracingOptions } = options;
if (!functions || typeof functions !== "object") throw new TypeError("instrument: expected { key, fn } or { functions: { name: fn } }");
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 = getConfig().tracer;
const { name, attributes, spanKind } = options;
const executeSpan = (span) => {
return 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: 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: SpanStatusCode.ERROR,
message: errorMessage
});
span.recordException(error instanceof Error ? error : new Error(String(error)));
span.end();
throw error;
});
else {
span.setStatus({ code: 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: SpanStatusCode.ERROR,
message: errorMessage
});
span.recordException(error instanceof Error ? error : new Error(String(error)));
span.end();
throw error;
}
});
};
const parentContext = getActiveContextWithBaggage();
const result = tracer.startActiveSpan(name, spanKind === void 0 ? {} : { kind: spanKind }, 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 span('webhook.process', async () => {
* await processWebhookPayload(payload)
* })
* }
* })
* }
* ```
*/
async function withNewContext(options) {
const { fn } = options;
return getConfig().tracer.startActiveSpan("root", { root: true }, async (span) => {
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error instanceof Error ? error : new Error(String(error)));
span.setStatus({ code: 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 { withTracing, withBaggage } from 'autotel';
*
* export const createOrder = withTracing({ name: 'order.create' })((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 = withTracing({ name: 'order.process' })((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 = context.active();
let updatedBaggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
for (const [key, value] of Object.entries(baggageEntries)) updatedBaggage = updatedBaggage.setEntry(key, { value });
const newContext = propagation.setBaggage(currentContext, updatedBaggage);
const ctxStorage = getContextStorage();
const previousStored = ctxStorage.getStore();
const baggageEnrichedStored = previousStored ? { value: propagation.setBaggage(previousStored.value, updatedBaggage) } : { value: newContext };
const result = previousStored ? ctxStorage.run(baggageEnrichedStored, () => context.with(newContext, fn)) : 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
export { trace$1 as a, withTracing as c, span as i, getActiveTraceContext as n, withBaggage as o, instrument as r, withNewContext as s, ctx as t };
//# sourceMappingURL=functional-Bit6noRu.js.map