UNPKG

autotel

Version:
840 lines (837 loc) 29.5 kB
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