UNPKG

autotel

Version:
300 lines (296 loc) 9.93 kB
import { Link, SpanContext } from '@opentelemetry/api'; import { T as TraceContext } from './trace-context-t5X1AP-e.js'; /** * Webhook and callback tracing with the "Parking Lot" pattern * * When initiating async operations that return hours/days later (webhooks, * payment callbacks, human approvals), you can't keep a span open. This module * provides utilities to "park" trace context and retrieve it when callbacks arrive. * * @example Stripe payment webhook * ```typescript * import { createParkingLot, InMemoryTraceContextStore } from 'autotel/webhook'; * * const parkingLot = createParkingLot({ * store: new InMemoryTraceContextStore(), * defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours * }); * * // When initiating payment * export const initiatePayment = trace(ctx => async (orderId: string) => { * await parkingLot.park(`payment:${orderId}`, { orderId }); * await stripeClient.createPaymentIntent({ metadata: { orderId } }); * }); * * // When Stripe webhook arrives (hours later) * export const handleStripeWebhook = parkingLot.traceCallback({ * name: 'stripe.webhook.payment_intent.succeeded', * correlationKeyFrom: (event) => `payment:${event.data.object.metadata.orderId}`, * })(ctx => async (event: Stripe.Event) => { * // ctx.parkedContext contains the original trace context * // ctx.elapsedMs shows time since payment was initiated * await fulfillOrder(event.data.object); * }); * ``` * * @module */ /** * Stored trace context for parking lot pattern */ interface StoredTraceContext { /** Trace ID from the original span */ traceId: string; /** Span ID from the original span */ spanId: string; /** Trace flags (sampling decision) */ traceFlags: number; /** When the context was parked */ parkedAt: number; /** Optional TTL in milliseconds */ ttlMs?: number; /** User-provided metadata */ metadata?: Record<string, string>; } /** * Interface for trace context storage backends * * Implement this interface to use different storage backends (Redis, DynamoDB, etc.) */ interface TraceContextStore { /** * Save trace context with a correlation key * * @param key - Unique correlation key (e.g., "payment:order-123") * @param context - The trace context to store */ save(key: string, context: StoredTraceContext): Promise<void>; /** * Load trace context by correlation key * * @param key - The correlation key used when parking * @returns The stored context, or null if not found/expired */ load(key: string): Promise<StoredTraceContext | null>; /** * Delete trace context by correlation key * * @param key - The correlation key to delete */ delete(key: string): Promise<void>; } /** * Configuration for creating a parking lot */ interface ParkingLotConfig { /** Storage backend for parked contexts */ store: TraceContextStore; /** Default TTL in milliseconds (default: 24 hours) */ defaultTTLMs?: number; /** Prefix for all correlation keys (default: "parkingLot:") */ keyPrefix?: string; /** Whether to auto-delete after retrieval (default: true) */ autoDeleteOnRetrieve?: boolean; /** Callback when context expires or is not found */ onMiss?: (correlationKey: string) => void; } /** * Configuration for traceCallback wrapper */ interface CallbackConfig { /** Span name for the callback handler */ name: string; /** * Extract correlation key from callback arguments * * @example * ```typescript * correlationKeyFrom: (event) => `payment:${event.data.orderId}` * ``` */ correlationKeyFrom: (args: unknown[]) => string; /** Additional span attributes */ attributes?: Record<string, string | number | boolean>; /** Whether to fail if parked context is not found (default: false) */ requireParkedContext?: boolean; } /** * Extended context for callback handlers */ interface CallbackContext extends TraceContext { /** The retrieved parked context, if found */ parkedContext: StoredTraceContext | null; /** Time elapsed since context was parked (ms), or null if not found */ elapsedMs: number | null; /** The correlation key used for retrieval */ correlationKey: string; } /** * The parking lot instance */ interface ParkingLot { /** * Park current trace context before initiating async operation * * Call this before sending a webhook, initiating a payment, or starting * any operation that will complete via callback. * * @param correlationKey - Unique key to retrieve context later (e.g., "payment:order-123") * @param metadata - Optional metadata to store with the context * @returns The correlation key (with prefix applied) * * @example * ```typescript * await parkingLot.park(`payment:${orderId}`, { * customerId: customer.id, * amount: payment.amount.toString(), * }); * ``` */ park(correlationKey: string, metadata?: Record<string, string>): Promise<string>; /** * Retrieve parked context when callback arrives * * @param correlationKey - The key used when parking * @returns The stored context, or null if not found/expired */ retrieve(correlationKey: string): Promise<StoredTraceContext | null>; /** * Wrap a callback handler with automatic context retrieval and linking * * Creates a traced function that: * 1. Extracts correlation key from arguments * 2. Retrieves parked context from storage * 3. Creates a span link to the original trace * 4. Provides elapsed time since parking * * @param config - Callback configuration * @returns Factory function for the callback handler * * @example * ```typescript * export const handleWebhook = parkingLot.traceCallback({ * name: 'webhook.payment.completed', * correlationKeyFrom: (args) => `payment:${args[0].orderId}`, * })(ctx => async (event) => { * console.log(`Payment completed after ${ctx.elapsedMs}ms`); * await processPayment(event); * }); * ``` */ traceCallback<TArgs extends unknown[], TReturn>(config: CallbackConfig): (fnFactory: (ctx: CallbackContext) => (...args: TArgs) => Promise<TReturn>) => (...args: TArgs) => Promise<TReturn>; /** * Manually create a span link from stored context * * Useful when you need more control over span creation. * * @param storedContext - The stored trace context * @returns A span link that can be added to a span */ createLink(storedContext: StoredTraceContext): Link; /** * Check if a parked context exists (without retrieving/deleting it) * * @param correlationKey - The key to check * @returns True if context exists and hasn't expired */ exists(correlationKey: string): Promise<boolean>; } /** * In-memory trace context store * * Useful for testing and development. For production, use a persistent * store like Redis or DynamoDB. * * @example * ```typescript * const store = new InMemoryTraceContextStore(); * const parkingLot = createParkingLot({ store }); * ``` */ declare class InMemoryTraceContextStore implements TraceContextStore { private options; private store; private cleanupInterval; constructor(options?: { /** Cleanup interval in ms (default: 60000) */ cleanupIntervalMs?: number; }); save(key: string, context: StoredTraceContext): Promise<void>; load(key: string): Promise<StoredTraceContext | null>; delete(key: string): Promise<void>; /** * Get number of stored contexts (for testing) */ get size(): number; /** * Clear all stored contexts (for testing) */ clear(): void; /** * Stop the cleanup interval */ destroy(): void; private cleanup; } /** * Create a parking lot for trace context storage and retrieval * * @param config - Parking lot configuration * @returns A parking lot instance * * @example Basic usage * ```typescript * const parkingLot = createParkingLot({ * store: new InMemoryTraceContextStore(), * defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours * }); * ``` * * @example With Redis store * ```typescript * class RedisTraceContextStore implements TraceContextStore { * constructor(private redis: Redis) {} * * async save(key: string, context: StoredTraceContext) { * const ttlSeconds = context.ttlMs ? Math.ceil(context.ttlMs / 1000) : 86400; * await this.redis.setex(key, ttlSeconds, JSON.stringify(context)); * } * * async load(key: string) { * const data = await this.redis.get(key); * return data ? JSON.parse(data) : null; * } * * async delete(key: string) { * await this.redis.del(key); * } * } * * const parkingLot = createParkingLot({ * store: new RedisTraceContextStore(redis), * }); * ``` */ declare function createParkingLot(config: ParkingLotConfig): ParkingLot; /** * Create a correlation key from multiple parts * * @param parts - Key parts to join * @returns A correlation key string * * @example * ```typescript * const key = createCorrelationKey('payment', orderId, 'stripe'); * // Returns: "payment:order-123:stripe" * ``` */ declare function createCorrelationKey(...parts: (string | number)[]): string; /** * Extract span context from stored context for manual linking * * @param storedContext - The stored trace context * @returns SpanContext compatible object */ declare function toSpanContext(storedContext: StoredTraceContext): SpanContext; export { type CallbackConfig, type CallbackContext, InMemoryTraceContextStore, type ParkingLot, type ParkingLotConfig, type StoredTraceContext, type TraceContextStore, createCorrelationKey, createParkingLot, toSpanContext };