autotel
Version:
Write Once, Observe Anywhere
476 lines • 18.2 kB
text/typescript
import { n as TraceContext } from "./trace-context-f7Xq0Q2q.cjs";
import { m as Sampler } from "./sampling-Er217Mge.cjs";
import { Attributes, Span, SpanKind } from "@opentelemetry/api";
//#region src/functional-wrapper.d.ts
type WrappedFunction<TArgs extends unknown[], TReturn> = (...args: TArgs) => TReturn | Promise<TReturn>;
/**
* Constraint alias for `instrument()` and friends. `never[]` parameters make
* every concretely-typed function satisfy the constraint under
* `strictFunctionTypes` while preserving its inferred call signature.
*/
type AnyInstrumentable = ((...args: never[]) => unknown) & {
displayName?: string;
name?: string;
};
/** Common options for functional tracing. */
interface TracingOptions<TArgs extends unknown[] = unknown[], TReturn = unknown> {
/**
* Span name (highest priority).
* If provided, this is used as the span name.
*/
name?: string;
/**
* Service name used to compose `${serviceName}.${functionName}` when no
* explicit name is provided.
*/
serviceName?: string;
/**
* Sampling strategy.
* @default AlwaysSampler
*/
sampler?: Sampler;
/**
* Enable call and duration metrics.
* @default false
*/
withMetrics?: boolean;
/** Extract attributes from function arguments. */
attributesFromArgs?: (args: TArgs) => Attributes;
/** Extract attributes from the function result. */
/** Receives the resolved value when the function returns a Promise. */
attributesFromResult?: (result: Awaited<TReturn>) => Attributes;
/**
* Capture arguments on the span as the truncated JSON `autotel.input`
* attribute. One argument is captured directly; multiple arguments are an
* array. Avoid this for secrets/PII, or pair it with a redacting processor.
*/
captureInput?: boolean;
/**
* Capture the result on the span as the truncated JSON `autotel.output`
* attribute. The same sensitive-data caveats as {@link captureInput} apply.
*/
captureOutput?: boolean;
/**
* Start a new root span instead of creating a child.
* Useful for serverless entry points.
* @default false
*/
startNewRoot?: boolean;
/**
* Flush telemetry when a root span ends.
* @default true
*/
flushOnRootSpanEnd?: boolean;
/**
* OpenTelemetry span kind for semantic convention compliance.
* @default SpanKind.INTERNAL
*/
spanKind?: SpanKind;
/**
* Classify a thrown value as a real error. Returning false treats the throw
* as expected control flow: the span is marked OK, no exception is recorded,
* and the original value is rethrown. This supports framework signals such
* as `redirect()` and `notFound()`.
* @default every throw is treated as an error
*/
isError?: (cause: unknown) => boolean;
}
/** Options for `instrument()` batch instrumentation. */
interface InstrumentOptions<T extends Record<string, AnyInstrumentable> = Record<string, AnyInstrumentable>> extends TracingOptions {
/** Object whose function properties should be instrumented. */
functions: T;
/** Per-function configuration overrides. */
overrides?: Record<string, Partial<TracingOptions>>;
/**
* Functions to skip. Supports string keys, regular expressions, and
* predicates. Functions whose keys start with `_` are skipped by default.
*/
skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[];
}
/** Options for instrumenting one function with an explicit stable key. */
interface SingleInstrumentOptions<TFunction extends AnyInstrumentable = AnyInstrumentable> extends TracingOptions {
/** Stable function key used for span naming. */
key: string;
/** Function to instrument. */
fn: TFunction;
}
//#endregion
//#region src/functional.d.ts
/**
* Complete trace context containing trace identifiers and span methods
*
* The ctx parameter in trace() functions provides:
* - traceId, spanId, correlationId from the active span
* - Span manipulation methods (setAttribute, setAttributes, setStatus, recordException)
*
* For custom context, access it directly in your functions (standard OpenTelemetry pattern).
*
* @example
* ```typescript
* import { trace } from 'autotel'
*
* export const createUser = withTracing({})((ctx) => async (data: CreateUserData) => {
* // Get custom context directly (standard OTel approach)
* const userId = getCurrentUserId()
* const tenantId = getCurrentTenant()
*
* // Use ctx for span operations and trace IDs
* ctx.setAttribute('user.id', data.id)
* ctx.setAttribute('user.tenant', tenantId)
* console.log(ctx.traceId) // Trace IDs available
* })
* ```
*/
/** Baggage entries: values that survive the W3C baggage header. */
type BaggageValues = Record<string, string | number | boolean>;
/**
* 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(async function getUser(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
*/
declare function getActiveTraceContext<TBaggage extends BaggageValues | undefined = undefined>(): TraceContext<TBaggage> | undefined;
declare const ctx: TraceContext;
/**
* The ambient {@link ctx}, aimed at the **request** span rather than at
* whatever span the calling code happens to be inside.
*
* Framework instrumentation nests spans per layer - express opens one per
* middleware and per route handler - so `ctx.setAttribute()` from a shared
* middleware records on a span that ends the moment `next()` fires. Attributes
* that describe the request as a whole (the authenticated user, the tenant, the
* plan) belong on the request span, which is what a backend shows as the
* resource and what a canonical log line is built from.
*
* Falls back to the active span outside a request, and no-ops when nothing is
* active at all, exactly as `ctx` does.
*
* @example
* ```typescript
* app.use((req, _res, next) => {
* requestCtx.setAttributes({ user: req.user }); // user.id, user.plan, ...
* next();
* });
* ```
*/
declare const requestCtx: TraceContext;
/**
* Wrap a plain function with a traced wrapper, or run one named operation
* immediately with {@link trace.run}.
*
* Every `trace(...)` call returns a **wrapper**. Nothing runs until you call
* what you get back, so a `trace()` call can never execute your function at
* module load. Reach the span from inside the body through the ambient
* {@link ctx} - it resolves at any depth, so a helper three frames down sees
* the same span without being handed anything.
*
* @example Auto-inferred name
* ```typescript
* export const createUser = trace(async (data) => {
* return await db.users.create(data)
* })
* ```
*
* @example Explicit name
* ```typescript
* export const createUser = trace('user.create', async (data) => {
* return await db.users.create(data)
* })
* ```
*
* @example Ambient context, at any depth
* ```typescript
* import { trace, ctx } from 'autotel'
*
* export const createUser = trace('user.create', async (data) => {
* ctx.setAttribute('user.id', data.id)
* return await db.users.create(data)
* })
* ```
*
* @example Curried, for a shared configuration
* ```typescript
* const traced = trace({ serviceName: 'users' })
* export const createUser = traced(async (data) => db.users.create(data))
* ```
*
* @example One operation, run now - see {@link trace.run}
* ```typescript
* const user = await trace.run('user.create', async (ctx) => {
* ctx.setAttribute('user.id', input.id)
* return db.users.create(input)
* })
* ```
*/
declare function traceImpl<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => PromiseLike<TReturn>): (...args: TArgs) => Promise<TReturn>;
declare function traceImpl<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
declare function traceImpl<TArgs extends unknown[], TReturn>(name: string, fn: (...args: TArgs) => PromiseLike<TReturn>): (...args: TArgs) => Promise<TReturn>;
declare function traceImpl<TArgs extends unknown[], TReturn>(name: string, fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
declare function traceImpl<TArgs extends unknown[], TReturn>(options: TracingOptions<TArgs, TReturn>, fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
declare function traceImpl(name: string): <TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn) => (...args: TArgs) => TReturn;
declare function traceImpl<TArgs extends unknown[], TReturn>(options: TracingOptions<TArgs, TReturn>): (fn: (...args: TArgs) => TReturn) => (...args: TArgs) => TReturn;
/**
* Run one named operation immediately and return its result, with the
* {@link TraceContext} passed in.
*
* This is the counterpart to {@link trace}: `trace(...)` always hands back a
* wrapper to call later, `trace.run(...)` runs the operation now. Keeping them
* under separate names is deliberate - a single call shape that sometimes
* wrapped and sometimes ran is what made dispatch depend on a callback's
* parameter name, which a minifier is free to rewrite (#166).
*
* The body can equally read the ambient {@link ctx}; the parameter is here for
* when an explicit binding reads better.
*
* @example
* ```typescript
* const user = await trace.run('user.create', async (ctx) => {
* ctx.setAttribute('user.id', input.id)
* return db.users.create(input)
* })
* ```
*/
declare function run<TReturn>(name: string, operation: (ctx: TraceContext) => Promise<TReturn>): Promise<TReturn>;
declare function run<TReturn>(name: string, operation: (ctx: TraceContext) => TReturn): TReturn;
declare function run<TReturn>(options: TracingOptions<[], TReturn>, operation: (ctx: TraceContext) => Promise<TReturn>): Promise<TReturn>;
declare function run<TReturn>(options: TracingOptions<[], TReturn>, operation: (ctx: TraceContext) => TReturn): TReturn;
declare const trace$1: typeof traceImpl & {
run: typeof run;
};
/**
* 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) => { })
* ```
*/
declare function withTracing<TCfgArgs extends unknown[] = unknown[], TCfgReturn = unknown>(options?: TracingOptions<TCfgArgs, TCfgReturn>): <TArgs extends TCfgArgs, TReturn extends TCfgReturn>(fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn | PromiseLike<TReturn>) => WrappedFunction<TArgs, TReturn>;
/**
* 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')]
* })
* ```
*/
type InstrumentedFunction<TFunction extends AnyInstrumentable> = TFunction extends ((...args: infer TArgs) => infer TReturn) ? (...args: TArgs) => TReturn extends PromiseLike<infer TValue> ? Promise<TValue> : TReturn : never;
type InstrumentedFunctions<T extends Record<string, AnyInstrumentable>> = { [TKey in keyof T]: InstrumentedFunction<T[TKey]>; };
declare function instrument<TFunction extends AnyInstrumentable>(options: SingleInstrumentOptions<TFunction> & {
fn: TFunction & ((...args: Parameters<TFunction>) => PromiseLike<unknown>);
}): (...args: Parameters<TFunction>) => Promise<Awaited<ReturnType<TFunction>>>;
declare function instrument<TFunction extends AnyInstrumentable>(options: SingleInstrumentOptions<TFunction>): TFunction;
declare function instrument<T extends Record<string, AnyInstrumentable>>(options: InstrumentOptions<T>): InstrumentedFunctions<T>;
/**
* Options for span() function
*/
interface SpanOptions {
/** Span name */
name: string;
/** Attributes to set on the span */
attributes?: Record<string, string | number | boolean>;
/** OpenTelemetry span kind */
spanKind?: import('@opentelemetry/api').SpanKind;
}
/**
* Execute a function within a named span
*
* Useful for adding tracing to specific code blocks without wrapping
* the entire function. Supports both synchronous and asynchronous functions.
*
* Mirrors `trace()`: pass a span name as the first argument for the common
* case, or full `SpanOptions` when you need to attach attributes.
*
* @example
* ```typescript
* // Name shorthand
* await span('payment.charge', async (span) => {
* await chargeCustomer(order);
* })
*
* // Full options when attributes are needed
* await span(
* { name: 'payment.charge', attributes: { amount: order.total } },
* async (span) => {
* await chargeCustomer(order);
* },
* )
*
* // Sync
* const total = span('calculateTotal', (span) => {
* return items.reduce((sum, item) => sum + item.price, 0);
* })
* ```
*/
declare function span<T = unknown>(name: string, fn: (span: Span) => PromiseLike<T>): Promise<T>;
declare function span<T = unknown>(options: SpanOptions, fn: (span: Span) => PromiseLike<T>): Promise<T>;
declare function span<T = unknown>(name: string, fn: (span: Span) => T): T;
declare function span<T = unknown>(options: SpanOptions, fn: (span: Span) => T): T;
/**
* Options for withNewContext() function
*/
interface WithNewContextOptions<T = unknown> {
/** Function to execute in new root context */
fn: () => Promise<T>;
}
/**
* 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)
* })
* }
* })
* }
* ```
*/
declare function withNewContext<T = unknown>(options: WithNewContextOptions<T>): Promise<T>;
/**
* Options for withBaggage() function
*/
interface WithBaggageOptions<T = unknown> {
/** Baggage entries to set (key-value pairs) */
baggage: Record<string, string>;
/** Function to execute with the updated baggage */
fn: () => T | Promise<T>;
}
/**
* 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);
* },
* });
* });
* ```
*/
declare function withBaggage<T = unknown>(options: WithBaggageOptions<T>): T | Promise<T>;
//#endregion
export { getActiveTraceContext as a, span as c, withNewContext as d, withTracing as f, TracingOptions as h, ctx as i, trace$1 as l, SingleInstrumentOptions as m, WithBaggageOptions as n, instrument as o, InstrumentOptions as p, WithNewContextOptions as r, requestCtx as s, SpanOptions as t, withBaggage as u };