UNPKG

autotel

Version:
823 lines (822 loc) 24.8 kB
import { n as TraceContext } from "./trace-context-Cijqoi6e.js"; import { AttributeValue, Attributes, Link, SpanContext } from "@opentelemetry/api"; //#region src/messaging.d.ts /** * Supported messaging systems */ type MessagingSystem = 'kafka' | 'rabbitmq' | 'sqs' | 'sns' | 'pubsub' | 'activemq' | 'azure_servicebus' | 'eventhubs' | (string & {}); /** * Messaging operation types */ type MessagingOperation = 'publish' | 'receive' | 'process' | 'settle'; /** * Configuration for producer tracing */ interface ProducerConfig { /** Messaging system (kafka, rabbitmq, sqs, etc.) */ system: MessagingSystem; /** Destination name (topic/queue) */ destination: string; /** Extract message ID from arguments */ messageIdFrom?: string | ((args: unknown[]) => string | undefined); /** Extract partition from arguments (Kafka-specific) */ partitionFrom?: string | ((args: unknown[]) => number | undefined); /** Extract message key from arguments (Kafka-specific) */ keyFrom?: string | ((args: unknown[]) => string | undefined); /** Additional attributes to set on span */ attributes?: Attributes; /** Propagate baggage in message headers */ propagateBaggage?: boolean; /** Callback before sending (for custom attributes) */ beforeSend?: (ctx: ProducerContext, args: unknown[]) => void; /** Callback on error */ onError?: (error: Error, ctx: ProducerContext) => void; /** * Hook to add system-specific attributes * * Use this to add attributes for messaging systems not explicitly supported * (e.g., NATS, Temporal, Cloudflare Queues, Redis Streams). * * @example NATS attributes * ```typescript * customAttributes: (ctx, args) => ({ * 'nats.subject': args[0].subject, * 'nats.reply_to': args[0].replyTo, * 'nats.stream': 'orders', * }) * ``` * * @example Temporal attributes * ```typescript * customAttributes: (ctx, args) => ({ * 'temporal.workflow_id': args[0].workflowId, * 'temporal.run_id': args[0].runId, * 'temporal.task_queue': 'orders-queue', * }) * ``` */ customAttributes?: (ctx: ProducerContext, args: unknown[]) => Record<string, AttributeValue>; /** * Hook for custom header injection (beyond W3C traceparent) * * Use this to inject headers for systems that use non-standard * context propagation formats. * * @example Datadog headers * ```typescript * customHeaders: (ctx) => ({ * 'x-datadog-trace-id': ctx.getTraceId(), * 'x-datadog-parent-id': ctx.getSpanId(), * }) * ``` * * @example Custom correlation headers * ```typescript * customHeaders: (ctx) => ({ * 'x-correlation-id': correlationId, * 'x-request-id': requestId, * }) * ``` */ customHeaders?: (ctx: ProducerContext) => Record<string, string>; } /** * Configuration for consumer tracing */ interface ConsumerConfig { /** Messaging system (kafka, rabbitmq, sqs, etc.) */ system: MessagingSystem; /** Destination name (topic/queue) */ destination: string; /** Consumer group name */ consumerGroup?: string; /** Extract headers from message for link creation */ headersFrom?: string | ((msg: unknown) => Record<string, string> | undefined); /** Enable batch mode - extract links from all messages */ batchMode?: boolean; /** Extract baggage from message headers */ extractBaggage?: boolean; /** Additional attributes to set on span */ attributes?: Attributes; /** Consumer lag metrics extraction */ lagMetrics?: LagMetricsConfig; /** Callback when message goes to DLQ */ onDLQ?: (ctx: ConsumerContext, reason: string) => void; /** Callback on error */ onError?: (error: Error, ctx: ConsumerContext) => void; /** * Message ordering configuration * * Enable sequence tracking, out-of-order detection, and deduplication. * * @example Kafka ordering * ```typescript * ordering: { * sequenceFrom: (msg) => msg.offset, * partitionKeyFrom: (msg) => msg.key, * detectOutOfOrder: true, * onOutOfOrder: (ctx, info) => { * console.warn(`Out of order: expected ${info.expectedSequence}, got ${info.currentSequence}`); * }, * } * ``` */ ordering?: OrderingConfig; /** * Consumer group tracking configuration * * Enables observability of consumer group state, including membership, * partition assignments, and rebalancing events. * * @example Kafka consumer group tracking * ```typescript * consumerGroupTracking: { * memberId: () => consumer.memberId, * groupInstanceId: process.env.KAFKA_GROUP_INSTANCE_ID, * onRebalance: (ctx, event) => { * if (event.type === 'revoked') { * logger.warn('Partitions revoked', event.partitions); * } * }, * trackPartitionLag: true, * } * ``` */ consumerGroupTracking?: ConsumerGroupTrackingConfig; /** * Hook to add system-specific attributes * * Use this to add attributes for messaging systems not explicitly supported * (e.g., NATS, Temporal, Cloudflare Queues, Redis Streams). * * @example NATS consumer attributes * ```typescript * customAttributes: (ctx, msg) => ({ * 'nats.subject': msg.subject, * 'nats.stream': msg.info?.stream, * 'nats.consumer': msg.info?.consumer, * 'nats.delivered_count': msg.info?.redeliveryCount, * }) * ``` * * @example Cloudflare Queue attributes * ```typescript * customAttributes: (ctx, msg) => ({ * 'cloudflare.queue_id': msg.id, * 'cloudflare.timestamp_ms': msg.timestamp.getTime(), * 'cloudflare.attempts': msg.attempts, * }) * ``` */ customAttributes?: (ctx: ConsumerContext, msg: unknown) => Record<string, AttributeValue>; /** * Hook for custom context extraction (beyond W3C traceparent) * * Use this to extract parent span context from systems that use * non-standard header formats. * * @example Datadog context extraction * ```typescript * customContextExtractor: (headers) => { * const traceId = headers['x-datadog-trace-id']; * const spanId = headers['x-datadog-parent-id']; * if (!traceId || !spanId) return null; * return { * traceId: traceIdToOtel(traceId), * spanId: spanIdToOtel(spanId), * traceFlags: TraceFlags.SAMPLED, * }; * } * ``` * * @example B3 format extraction * ```typescript * customContextExtractor: (headers) => { * const traceId = headers['x-b3-traceid']; * const spanId = headers['x-b3-spanid']; * const sampled = headers['x-b3-sampled'] === '1'; * if (!traceId || !spanId) return null; * return { * traceId, * spanId, * traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, * }; * } * ``` */ customContextExtractor?: (headers: Record<string, string>) => SpanContext | null; } /** * Configuration for consumer lag metrics */ interface LagMetricsConfig { /** Get current message offset */ getCurrentOffset?: (msg: unknown) => number | undefined; /** Get end offset (high watermark) - can be async */ getEndOffset?: () => number | Promise<number>; /** Get committed offset - can be async */ getCommittedOffset?: () => number | Promise<number>; /** Get partition from message */ getPartition?: (msg: unknown) => number | undefined; } /** * Configuration for message ordering tracking */ interface OrderingConfig { /** * Extract sequence number from message * * Sequence numbers enable out-of-order detection and gap analysis. * * @example Kafka offset * ```typescript * sequenceFrom: (msg) => msg.offset * ``` */ sequenceFrom?: (msg: unknown) => number | undefined; /** * Extract partition key from message * * Partition keys determine message ordering in Kafka. * * @example Message key * ```typescript * partitionKeyFrom: (msg) => msg.key * ``` */ partitionKeyFrom?: (msg: unknown) => string | undefined; /** * Extract message ID for deduplication * * Used to detect duplicate messages. * * @example Idempotency key * ```typescript * messageIdFrom: (msg) => msg.headers['idempotency-key'] * ``` */ messageIdFrom?: (msg: unknown) => string | undefined; /** * Enable out-of-order detection * * Tracks sequence numbers per partition and detects when messages * arrive out of order. * * @default false */ detectOutOfOrder?: boolean; /** * Enable deduplication detection * * Tracks message IDs and detects duplicates within the window. * * @default false */ detectDuplicates?: boolean; /** * Deduplication window size (number of message IDs to track) * * @default 1000 */ deduplicationWindowSize?: number; /** * Callback when out-of-order message detected */ onOutOfOrder?: (ctx: ConsumerContext, info: OutOfOrderInfo) => void; /** * Callback when duplicate message detected */ onDuplicate?: (ctx: ConsumerContext, messageId: string) => void; } /** * Information about out-of-order message */ interface OutOfOrderInfo { /** Current sequence number */ currentSequence: number; /** Expected sequence number */ expectedSequence: number; /** Partition key (if available) */ partitionKey?: string; /** Gap size (positive = gap, negative = out of order) */ gap: number; } /** * Configuration for consumer group tracking * * Enables observability of consumer group state, including membership, * partition assignments, and rebalancing events. * * @example Kafka consumer group tracking * ```typescript * consumerGroupTracking: { * memberId: consumer.memberId, * groupInstanceId: process.env.CONSUMER_ID, * onRebalance: (ctx, event) => { * if (event.type === 'assigned') { * console.log(`Assigned partitions: ${event.partitions}`); * } * }, * } * ``` */ interface ConsumerGroupTrackingConfig { /** * Consumer member ID * * Unique identifier assigned by the broker to this consumer. */ memberId?: string | (() => string | undefined); /** * Static group instance ID (for static membership) * * If set, enables static group membership which prevents * rebalances when consumers restart. */ groupInstanceId?: string | (() => string | undefined); /** * Callback when rebalance occurs */ onRebalance?: (ctx: ConsumerContext, event: RebalanceEvent) => void; /** * Callback when partitions are assigned */ onPartitionsAssigned?: (ctx: ConsumerContext, partitions: PartitionAssignment[]) => void; /** * Callback when partitions are revoked */ onPartitionsRevoked?: (ctx: ConsumerContext, partitions: PartitionAssignment[]) => void; /** * Track consumer lag per partition * * @default true */ trackPartitionLag?: boolean; /** * Track consumer heartbeat health * * @default false */ trackHeartbeat?: boolean; /** * Heartbeat interval in milliseconds (for health tracking) */ heartbeatIntervalMs?: number; } /** * Rebalance event types */ type RebalanceType = 'assigned' | 'revoked' | 'lost'; /** * Rebalance event information */ interface RebalanceEvent { /** Type of rebalance event */ type: RebalanceType; /** Partitions affected by the rebalance */ partitions: PartitionAssignment[]; /** Timestamp of the rebalance event */ timestamp: number; /** Generation ID (increments on each rebalance) */ generation?: number; /** Consumer member ID */ memberId?: string; /** Reason for the rebalance (if available) */ reason?: string; } /** * Partition assignment information */ interface PartitionAssignment { /** Topic name */ topic: string; /** Partition number */ partition: number; /** Initial offset (if available) */ offset?: number; /** Metadata (if available) */ metadata?: string; } /** * Consumer group state snapshot */ interface ConsumerGroupState { /** Consumer group name */ groupId: string; /** Consumer member ID */ memberId?: string; /** Static instance ID (if using static membership) */ groupInstanceId?: string; /** Currently assigned partitions */ assignedPartitions: PartitionAssignment[]; /** Group generation ID */ generation?: number; /** Whether the consumer is currently active */ isActive: boolean; /** Last heartbeat timestamp */ lastHeartbeat?: number; /** Consumer state (stable, preparing_rebalance, completing_rebalance, dead) */ state?: 'stable' | 'preparing_rebalance' | 'completing_rebalance' | 'dead' | 'empty'; } /** * Partition lag information */ interface PartitionLag { /** Topic name */ topic: string; /** Partition number */ partition: number; /** Current consumer offset */ currentOffset: number; /** End offset (high watermark) */ endOffset: number; /** Calculated lag */ lag: number; /** Timestamp of measurement */ timestamp: number; } /** * DLQ failure category types */ type DLQReasonCategory = 'validation' | 'processing' | 'timeout' | 'poison' | 'unknown'; /** * Options for enhanced DLQ recording */ interface DLQOptions { /** * Automatically link to the producer span context * * When true, creates a span link from the DLQ event back to * the original producer span for correlation. * * @default true */ linkToProducer?: boolean; /** * Category of the failure that caused DLQ routing * * - validation: Message failed schema/format validation * - processing: Business logic error during processing * - timeout: Processing exceeded allowed time * - poison: Message causes repeated failures (poison pill) * - unknown: Uncategorized failure */ reasonCategory?: DLQReasonCategory; /** * Number of processing attempts before DLQ routing */ attemptCount?: number; /** * The original error that caused DLQ routing * * Error details are recorded as span attributes for debugging. */ originalError?: Error; /** * Additional metadata to record with the DLQ event */ metadata?: Record<string, string | number | boolean>; } /** * Options for recording DLQ replay */ interface DLQReplayOptions { /** * Original span context from DLQ message * * If provided, creates a span link to correlate with the original failure. */ originalDLQSpanContext?: SpanContext; /** * Time spent in DLQ before replay (milliseconds) */ dlqDwellTimeMs?: number; /** * Retry attempt number for this replay */ replayAttempt?: number; } /** * Extended trace context for producers with header injection */ interface ProducerContext extends TraceContext { /** * Get W3C trace context headers to inject into message * * @returns Headers object with traceparent and optionally tracestate * * @example * ```typescript * const headers = ctx.getTraceHeaders(); * await producer.send({ * topic: 'events', * messages: [{ value: data, headers }] * }); * ``` */ getTraceHeaders(): { traceparent: string; tracestate?: string; }; /** * Get all propagation headers including baggage if enabled * * @returns Headers object with all W3C trace context headers */ getAllPropagationHeaders(): Record<string, string>; /** * Get all headers including custom headers from customHeaders hook * * This combines W3C trace context headers, baggage (if enabled), * and any custom headers defined via the customHeaders hook. * * @returns Combined headers object * * @example * ```typescript * const headers = ctx.getFullHeaders(); * // Contains: traceparent, tracestate, baggage (if enabled), and custom headers * await producer.send({ topic, messages: [{ value, headers }] }); * ``` */ getFullHeaders(): Record<string, string>; } /** * Extended trace context for consumers */ interface ConsumerContext extends TraceContext { /** * Record that a message is being sent to DLQ * * Enhanced with auto-linking to producer span, failure categorization, * and detailed error recording for comprehensive DLQ observability. * * @param reason - Human-readable reason for DLQ routing * @param dlqNameOrOptions - DLQ name (string) or enhanced options object * @param options - Enhanced DLQ options (when second param is dlqName) * * @example Basic usage (backwards compatible) * ```typescript * ctx.recordDLQ('Schema validation failed', 'orders-dlq'); * ``` * * @example Enhanced usage with options * ```typescript * ctx.recordDLQ('Invalid order total', 'orders-dlq', { * reasonCategory: 'validation', * attemptCount: 3, * originalError: error, * linkToProducer: true, // Auto-links to producer span * }); * ``` * * @example Using options object as second param * ```typescript * ctx.recordDLQ('Processing timeout', { * reasonCategory: 'timeout', * attemptCount: 5, * metadata: { processingTimeMs: 30000 }, * }); * ``` */ recordDLQ(reason: string, dlqName?: string, options?: DLQOptions): void; recordDLQ(reason: string, options?: DLQOptions): void; /** * Record replay of a message from DLQ * * Use this when processing a message that was replayed from the DLQ * to create links for correlation and track replay metrics. * * @param options - Replay tracking options * * @example * ```typescript * export const processReplay = traceConsumer({ * system: 'kafka', * destination: 'orders-dlq-replay', * })(ctx => async (message) => { * ctx.recordReplay({ * originalDLQSpanContext: extractSpanContext(message.headers), * dlqDwellTimeMs: Date.now() - message.timestamp, * replayAttempt: message.replayCount, * }); * await processOrder(message); * }); * ``` */ recordReplay(options?: DLQReplayOptions): void; /** * Record retry attempt * * @param attemptNumber - Current retry attempt (1-based) * @param maxAttempts - Maximum retry attempts */ recordRetry(attemptNumber: number, maxAttempts?: number): void; /** * Get the producer span context links extracted from message headers * * Useful for accessing the producer span context when implementing * custom DLQ or retry logic. * * @returns Array of span links extracted from the message, or empty array */ getProducerLinks(): Link[]; /** * Check if the current message is a duplicate * * @returns True if the message has been seen before */ isDuplicate(): boolean; /** * Check if the current message arrived out of order * * @returns Out of order info, or null if in order */ getOutOfOrderInfo(): OutOfOrderInfo | null; /** * Get current sequence number * * @returns The sequence number, or null if not configured */ getSequenceNumber(): number | null; /** * Get partition key * * @returns The partition key, or null if not configured */ getPartitionKey(): string | null; /** * Record a rebalance event * * Call this when the consumer group undergoes a rebalance to capture * the event as a span event with partition assignment details. * * @param event - The rebalance event details * * @example * ```typescript * consumer.on('rebalance', (event) => { * ctx.recordRebalance({ * type: event.type, * partitions: event.assignment, * generation: event.generationId, * timestamp: Date.now(), * }); * }); * ``` */ recordRebalance(event: RebalanceEvent): void; /** * Record a heartbeat event * * Call this on each heartbeat to track consumer health. * * @param healthy - Whether the heartbeat was successful * @param latencyMs - Optional latency of the heartbeat in milliseconds */ recordHeartbeat(healthy: boolean, latencyMs?: number): void; /** * Record partition lag for a specific partition * * @param lag - The partition lag information */ recordPartitionLag(lag: PartitionLag): void; /** * Get the current consumer group state * * @returns The current consumer group state, or null if not configured */ getConsumerGroupState(): ConsumerGroupState | null; /** * Get the consumer member ID * * @returns The member ID, or null if not available */ getMemberId(): string | null; /** * Get the current partition assignments * * @returns Array of assigned partitions, or empty array if none */ getAssignedPartitions(): PartitionAssignment[]; } /** * Create a traced message producer function * * Sets SpanKind.PRODUCER, OTel messaging semantic attributes, * and provides context injection helpers. * * @param config - Producer configuration * @returns Factory function that wraps your producer logic * * @example Kafka producer * ```typescript * export const publishUserEvent = traceProducer({ * system: 'kafka', * destination: 'user-events', * messageIdFrom: (args) => args[0]?.eventId, * })(ctx => async (event: UserEvent) => { * const headers = ctx.getTraceHeaders(); * await producer.send({ * topic: 'user-events', * messages: [{ * key: event.userId, * value: JSON.stringify(event), * headers, * }] * }); * }); * ``` * * @example SQS producer * ```typescript * export const sendToSQS = traceProducer({ * system: 'sqs', * destination: 'orders-queue', * })(ctx => async (order: Order) => { * const headers = ctx.getAllPropagationHeaders(); * await sqs.sendMessage({ * QueueUrl: QUEUE_URL, * MessageBody: JSON.stringify(order), * MessageAttributes: headersToSQSAttributes(headers), * }); * }); * ``` */ declare function traceProducer<TArgs extends unknown[], TReturn>(config: ProducerConfig): (fnFactory: (ctx: ProducerContext) => (...args: TArgs) => Promise<TReturn>) => ((...args: TArgs) => Promise<TReturn>); /** * Create a traced message consumer function * * Sets SpanKind.CONSUMER, OTel messaging semantic attributes, * automatically extracts links from producer trace headers, * and provides DLQ/retry recording helpers. * * @param config - Consumer configuration * @returns Factory function that wraps your consumer logic * * @example Kafka consumer (single message) * ```typescript * export const processUserEvent = traceConsumer({ * system: 'kafka', * destination: 'user-events', * consumerGroup: 'event-processor', * headersFrom: (msg) => msg.headers, * })(ctx => async (message: KafkaMessage) => { * // Link to producer span is automatically created * const event = JSON.parse(message.value.toString()); * await processEvent(event); * }); * ``` * * @example Kafka consumer (batch mode) * ```typescript * export const processUserEventBatch = traceConsumer({ * system: 'kafka', * destination: 'user-events', * consumerGroup: 'event-processor', * batchMode: true, * headersFrom: (msg) => msg.headers, * lagMetrics: { * getCurrentOffset: (msg) => msg.offset, * getEndOffset: () => consumer.getHighWatermark(), * getPartition: (msg) => msg.partition, * }, * })(ctx => async (messages: KafkaMessage[]) => { * // Links to all producer spans are automatically created * for (const msg of messages) { * await processEvent(JSON.parse(msg.value.toString())); * } * }); * ``` * * @example SQS consumer with DLQ handling * ```typescript * export const processSQSMessage = traceConsumer({ * system: 'sqs', * destination: 'orders-queue', * headersFrom: (msg) => sqsAttributesToHeaders(msg.MessageAttributes), * onDLQ: (ctx, reason) => { * ctx.recordDLQ(reason, 'orders-dlq'); * }, * })(ctx => async (message: SQSMessage) => { * try { * await processOrder(JSON.parse(message.Body)); * } catch (error) { * if (message.ApproximateReceiveCount > 3) { * ctx.recordDLQ(error.message); * throw error; * } * ctx.recordRetry(message.ApproximateReceiveCount, 3); * throw error; * } * }); * ``` */ declare function traceConsumer<TArgs extends unknown[], TReturn>(config: ConsumerConfig): (fnFactory: (ctx: ConsumerContext) => (...args: TArgs) => Promise<TReturn>) => ((...args: TArgs) => Promise<TReturn>); /** * Clear sequence tracking state (useful for testing) */ declare function clearOrderingState(): void; //#endregion export { ConsumerConfig, ConsumerContext, ConsumerGroupState, ConsumerGroupTrackingConfig, DLQOptions, DLQReasonCategory, DLQReplayOptions, LagMetricsConfig, MessagingOperation, MessagingSystem, OrderingConfig, OutOfOrderInfo, PartitionAssignment, PartitionLag, ProducerConfig, ProducerContext, RebalanceEvent, RebalanceType, clearOrderingState, traceConsumer, traceProducer }; //# sourceMappingURL=messaging.d.ts.map