autotel 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/package.json +1 -2
  2. package/src/attribute-redacting-processor.test.ts +0 -763
  3. package/src/attribute-redacting-processor.ts +0 -621
  4. package/src/attributes/attachers.ts +0 -161
  5. package/src/attributes/builders.ts +0 -529
  6. package/src/attributes/domains.ts +0 -42
  7. package/src/attributes/index.ts +0 -81
  8. package/src/attributes/registry.ts +0 -323
  9. package/src/attributes/types.ts +0 -211
  10. package/src/attributes/utils.ts +0 -64
  11. package/src/attributes/validators.ts +0 -266
  12. package/src/attributes.test.ts +0 -292
  13. package/src/auto.ts +0 -67
  14. package/src/autotel-logger.test.ts +0 -548
  15. package/src/autotel-logger.ts +0 -364
  16. package/src/baggage-span-processor.test.ts +0 -202
  17. package/src/baggage-span-processor.ts +0 -100
  18. package/src/business-baggage.test.ts +0 -500
  19. package/src/business-baggage.ts +0 -669
  20. package/src/circuit-breaker.test.ts +0 -341
  21. package/src/circuit-breaker.ts +0 -184
  22. package/src/config.test.ts +0 -94
  23. package/src/config.ts +0 -172
  24. package/src/correlated-events.test.ts +0 -151
  25. package/src/correlated-events.ts +0 -47
  26. package/src/correlation-id.test.ts +0 -163
  27. package/src/correlation-id.ts +0 -206
  28. package/src/db.test.ts +0 -252
  29. package/src/db.ts +0 -447
  30. package/src/decorators.test.ts +0 -153
  31. package/src/decorators.ts +0 -188
  32. package/src/define-event.test.ts +0 -41
  33. package/src/define-event.ts +0 -58
  34. package/src/devtools.ts +0 -60
  35. package/src/drain-pipeline.test.ts +0 -68
  36. package/src/drain-pipeline.ts +0 -199
  37. package/src/drain-toolkit.test.ts +0 -113
  38. package/src/drain-toolkit.ts +0 -129
  39. package/src/enricher-toolkit.test.ts +0 -67
  40. package/src/enricher-toolkit.ts +0 -79
  41. package/src/enrichers.test.ts +0 -150
  42. package/src/enrichers.ts +0 -145
  43. package/src/env-config.test.ts +0 -323
  44. package/src/env-config.ts +0 -309
  45. package/src/error-catalog.test.ts +0 -133
  46. package/src/error-catalog.ts +0 -262
  47. package/src/event-queue.test.ts +0 -864
  48. package/src/event-queue.ts +0 -699
  49. package/src/event-subscriber.ts +0 -262
  50. package/src/event-testing.ts +0 -197
  51. package/src/event.test.ts +0 -1104
  52. package/src/event.ts +0 -988
  53. package/src/events-config.ts +0 -235
  54. package/src/exporters.ts +0 -165
  55. package/src/filtering-span-processor.test.ts +0 -281
  56. package/src/filtering-span-processor.ts +0 -111
  57. package/src/flatten-attributes.test.ts +0 -76
  58. package/src/flatten-attributes.ts +0 -80
  59. package/src/functional.strict-types.typecheck.ts +0 -53
  60. package/src/functional.test.ts +0 -1464
  61. package/src/functional.ts +0 -2539
  62. package/src/functional.types.test.ts +0 -135
  63. package/src/hook.mjs +0 -15
  64. package/src/http.test.ts +0 -485
  65. package/src/http.ts +0 -424
  66. package/src/index.ts +0 -433
  67. package/src/init-auto-redactor.test.ts +0 -53
  68. package/src/init-redactor.test.ts +0 -8
  69. package/src/init.customization.test.ts +0 -665
  70. package/src/init.integrations.test.ts +0 -399
  71. package/src/init.openllmetry.test.ts +0 -194
  72. package/src/init.protocol.test.ts +0 -215
  73. package/src/init.ts +0 -2439
  74. package/src/instrumentation.test.ts +0 -108
  75. package/src/instrumentation.ts +0 -319
  76. package/src/logger.test.ts +0 -125
  77. package/src/logger.ts +0 -341
  78. package/src/messaging-adapters.test.ts +0 -595
  79. package/src/messaging-adapters.ts +0 -583
  80. package/src/messaging-testing.test.ts +0 -573
  81. package/src/messaging-testing.ts +0 -935
  82. package/src/messaging.test.ts +0 -1646
  83. package/src/messaging.ts +0 -2245
  84. package/src/metric-helpers.ts +0 -47
  85. package/src/metric-testing.ts +0 -197
  86. package/src/metric.ts +0 -446
  87. package/src/metrics.test.ts +0 -241
  88. package/src/node-require.ts +0 -123
  89. package/src/operation-context.ts +0 -93
  90. package/src/parse-error.test.ts +0 -73
  91. package/src/parse-error.ts +0 -112
  92. package/src/posthog-logs.test.ts +0 -115
  93. package/src/posthog-logs.ts +0 -77
  94. package/src/pretty-console-exporter.test.ts +0 -545
  95. package/src/pretty-console-exporter.ts +0 -413
  96. package/src/pretty-log-formatter.test.ts +0 -123
  97. package/src/pretty-log-formatter.ts +0 -210
  98. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  99. package/src/processors/canonical-log-line-processor.ts +0 -396
  100. package/src/processors.ts +0 -152
  101. package/src/rate-limiter.test.ts +0 -199
  102. package/src/rate-limiter.ts +0 -98
  103. package/src/redact-values.test.ts +0 -90
  104. package/src/redact-values.ts +0 -34
  105. package/src/register.ts +0 -37
  106. package/src/request-logger.test.ts +0 -545
  107. package/src/request-logger.ts +0 -342
  108. package/src/sampling.test.ts +0 -1060
  109. package/src/sampling.ts +0 -737
  110. package/src/security-schema.test.ts +0 -45
  111. package/src/security-schema.ts +0 -107
  112. package/src/semantic-conventions.ts +0 -15
  113. package/src/semantic-helpers.test.ts +0 -226
  114. package/src/semantic-helpers.ts +0 -438
  115. package/src/shutdown.test.ts +0 -364
  116. package/src/shutdown.ts +0 -246
  117. package/src/span-name-normalizer.test.ts +0 -377
  118. package/src/span-name-normalizer.ts +0 -213
  119. package/src/stable-hash.ts +0 -27
  120. package/src/structured-error.test.ts +0 -191
  121. package/src/structured-error.ts +0 -157
  122. package/src/stub.integration.test.ts +0 -361
  123. package/src/tail-sampling-processor.test.ts +0 -230
  124. package/src/tail-sampling-processor.ts +0 -55
  125. package/src/test-span-collector.test.ts +0 -234
  126. package/src/test-span-collector.ts +0 -150
  127. package/src/testing.ts +0 -705
  128. package/src/trace-context.test.ts +0 -73
  129. package/src/trace-context.ts +0 -567
  130. package/src/trace-helpers.new.test.ts +0 -278
  131. package/src/trace-helpers.test.ts +0 -290
  132. package/src/trace-helpers.ts +0 -710
  133. package/src/trace-hybrid.test.ts +0 -42
  134. package/src/trace-hybrid.ts +0 -37
  135. package/src/tracer-provider.test.ts +0 -183
  136. package/src/tracer-provider.ts +0 -266
  137. package/src/track.test.ts +0 -154
  138. package/src/track.ts +0 -216
  139. package/src/validate.test.ts +0 -287
  140. package/src/validate.ts +0 -307
  141. package/src/validation-attributes.ts +0 -43
  142. package/src/validation.test.ts +0 -330
  143. package/src/validation.ts +0 -246
  144. package/src/variable-name-inference.test.ts +0 -178
  145. package/src/variable-name-inference.ts +0 -242
  146. package/src/webhook.test.ts +0 -649
  147. package/src/webhook.ts +0 -637
  148. package/src/workflow-distributed.test.ts +0 -786
  149. package/src/workflow-distributed.ts +0 -916
  150. package/src/workflow.async-safety.integration.test.ts +0 -345
  151. package/src/workflow.test.ts +0 -647
  152. package/src/workflow.ts +0 -810
  153. package/src/yaml-config.test.ts +0 -373
  154. package/src/yaml-config.ts +0 -351
package/src/messaging.ts DELETED
@@ -1,2245 +0,0 @@
1
- /**
2
- * Messaging helpers for event-driven architectures
3
- *
4
- * Provides specialized tracing for message producers and consumers
5
- * with automatic context propagation, link extraction, and OTel
6
- * semantic convention compliance.
7
- *
8
- * @example Producer
9
- * ```typescript
10
- * import { traceProducer } from 'autotel/messaging';
11
- *
12
- * export const publishEvent = traceProducer({
13
- * system: 'kafka',
14
- * destination: 'user-events',
15
- * })(ctx => async (event: UserEvent) => {
16
- * const headers = ctx.getTraceHeaders();
17
- * await producer.send({
18
- * topic: 'user-events',
19
- * messages: [{ value: JSON.stringify(event), headers }]
20
- * });
21
- * });
22
- * ```
23
- *
24
- * @example Consumer
25
- * ```typescript
26
- * import { traceConsumer } from 'autotel/messaging';
27
- *
28
- * export const processEvents = traceConsumer({
29
- * system: 'kafka',
30
- * destination: 'user-events',
31
- * consumerGroup: 'event-processor',
32
- * batchMode: true,
33
- * })(ctx => async (messages: KafkaMessage[]) => {
34
- * // Links to producer spans are automatically extracted
35
- * for (const msg of messages) {
36
- * await processMessage(msg);
37
- * }
38
- * });
39
- * ```
40
- *
41
- * @module
42
- */
43
-
44
- import { SpanKind, context, propagation } from '@opentelemetry/api';
45
- import type {
46
- Attributes,
47
- AttributeValue,
48
- Link,
49
- SpanContext,
50
- } from '@opentelemetry/api';
51
- import { trace } from './functional';
52
- import type { TraceContext } from './trace-context';
53
- import { emitCorrelatedEvent } from './correlated-events';
54
- import { createLinkFromHeaders, extractLinksFromBatch } from './sampling';
55
-
56
- // ============================================================================
57
- // Types
58
- // ============================================================================
59
-
60
- /**
61
- * Supported messaging systems
62
- */
63
- export type MessagingSystem =
64
- | 'kafka'
65
- | 'rabbitmq'
66
- | 'sqs'
67
- | 'sns'
68
- | 'pubsub'
69
- | 'activemq'
70
- | 'azure_servicebus'
71
- | 'eventhubs'
72
- | (string & {});
73
-
74
- /**
75
- * Messaging operation types
76
- */
77
- export type MessagingOperation = 'publish' | 'receive' | 'process' | 'settle';
78
-
79
- /**
80
- * Configuration for producer tracing
81
- */
82
- export interface ProducerConfig {
83
- /** Messaging system (kafka, rabbitmq, sqs, etc.) */
84
- system: MessagingSystem;
85
-
86
- /** Destination name (topic/queue) */
87
- destination: string;
88
-
89
- /** Extract message ID from arguments */
90
- messageIdFrom?: string | ((args: unknown[]) => string | undefined);
91
-
92
- /** Extract partition from arguments (Kafka-specific) */
93
- partitionFrom?: string | ((args: unknown[]) => number | undefined);
94
-
95
- /** Extract message key from arguments (Kafka-specific) */
96
- keyFrom?: string | ((args: unknown[]) => string | undefined);
97
-
98
- /** Additional attributes to set on span */
99
- attributes?: Attributes;
100
-
101
- /** Propagate baggage in message headers */
102
- propagateBaggage?: boolean;
103
-
104
- /** Callback before sending (for custom attributes) */
105
- beforeSend?: (ctx: ProducerContext, args: unknown[]) => void;
106
-
107
- /** Callback on error */
108
- onError?: (error: Error, ctx: ProducerContext) => void;
109
-
110
- // ---- Extensible Hooks ("Bring Your Own" System Support) ----
111
-
112
- /**
113
- * Hook to add system-specific attributes
114
- *
115
- * Use this to add attributes for messaging systems not explicitly supported
116
- * (e.g., NATS, Temporal, Cloudflare Queues, Redis Streams).
117
- *
118
- * @example NATS attributes
119
- * ```typescript
120
- * customAttributes: (ctx, args) => ({
121
- * 'nats.subject': args[0].subject,
122
- * 'nats.reply_to': args[0].replyTo,
123
- * 'nats.stream': 'orders',
124
- * })
125
- * ```
126
- *
127
- * @example Temporal attributes
128
- * ```typescript
129
- * customAttributes: (ctx, args) => ({
130
- * 'temporal.workflow_id': args[0].workflowId,
131
- * 'temporal.run_id': args[0].runId,
132
- * 'temporal.task_queue': 'orders-queue',
133
- * })
134
- * ```
135
- */
136
- customAttributes?: (
137
- ctx: ProducerContext,
138
- args: unknown[],
139
- ) => Record<string, AttributeValue>;
140
-
141
- /**
142
- * Hook for custom header injection (beyond W3C traceparent)
143
- *
144
- * Use this to inject headers for systems that use non-standard
145
- * context propagation formats.
146
- *
147
- * @example Datadog headers
148
- * ```typescript
149
- * customHeaders: (ctx) => ({
150
- * 'x-datadog-trace-id': ctx.getTraceId(),
151
- * 'x-datadog-parent-id': ctx.getSpanId(),
152
- * })
153
- * ```
154
- *
155
- * @example Custom correlation headers
156
- * ```typescript
157
- * customHeaders: (ctx) => ({
158
- * 'x-correlation-id': correlationId,
159
- * 'x-request-id': requestId,
160
- * })
161
- * ```
162
- */
163
- customHeaders?: (ctx: ProducerContext) => Record<string, string>;
164
- }
165
-
166
- /**
167
- * Configuration for consumer tracing
168
- */
169
- export interface ConsumerConfig {
170
- /** Messaging system (kafka, rabbitmq, sqs, etc.) */
171
- system: MessagingSystem;
172
-
173
- /** Destination name (topic/queue) */
174
- destination: string;
175
-
176
- /** Consumer group name */
177
- consumerGroup?: string;
178
-
179
- /** Extract headers from message for link creation */
180
- headersFrom?: string | ((msg: unknown) => Record<string, string> | undefined);
181
-
182
- /** Enable batch mode - extract links from all messages */
183
- batchMode?: boolean;
184
-
185
- /** Extract baggage from message headers */
186
- extractBaggage?: boolean;
187
-
188
- /** Additional attributes to set on span */
189
- attributes?: Attributes;
190
-
191
- /** Consumer lag metrics extraction */
192
- lagMetrics?: LagMetricsConfig;
193
-
194
- /** Callback when message goes to DLQ */
195
- onDLQ?: (ctx: ConsumerContext, reason: string) => void;
196
-
197
- /** Callback on error */
198
- onError?: (error: Error, ctx: ConsumerContext) => void;
199
-
200
- // ---- Message Ordering Support ----
201
-
202
- /**
203
- * Message ordering configuration
204
- *
205
- * Enable sequence tracking, out-of-order detection, and deduplication.
206
- *
207
- * @example Kafka ordering
208
- * ```typescript
209
- * ordering: {
210
- * sequenceFrom: (msg) => msg.offset,
211
- * partitionKeyFrom: (msg) => msg.key,
212
- * detectOutOfOrder: true,
213
- * onOutOfOrder: (ctx, info) => {
214
- * console.warn(`Out of order: expected ${info.expectedSequence}, got ${info.currentSequence}`);
215
- * },
216
- * }
217
- * ```
218
- */
219
- ordering?: OrderingConfig;
220
-
221
- // ---- Consumer Group Tracking ----
222
-
223
- /**
224
- * Consumer group tracking configuration
225
- *
226
- * Enables observability of consumer group state, including membership,
227
- * partition assignments, and rebalancing events.
228
- *
229
- * @example Kafka consumer group tracking
230
- * ```typescript
231
- * consumerGroupTracking: {
232
- * memberId: () => consumer.memberId,
233
- * groupInstanceId: process.env.KAFKA_GROUP_INSTANCE_ID,
234
- * onRebalance: (ctx, event) => {
235
- * if (event.type === 'revoked') {
236
- * logger.warn('Partitions revoked', event.partitions);
237
- * }
238
- * },
239
- * trackPartitionLag: true,
240
- * }
241
- * ```
242
- */
243
- consumerGroupTracking?: ConsumerGroupTrackingConfig;
244
-
245
- // ---- Extensible Hooks ("Bring Your Own" System Support) ----
246
-
247
- /**
248
- * Hook to add system-specific attributes
249
- *
250
- * Use this to add attributes for messaging systems not explicitly supported
251
- * (e.g., NATS, Temporal, Cloudflare Queues, Redis Streams).
252
- *
253
- * @example NATS consumer attributes
254
- * ```typescript
255
- * customAttributes: (ctx, msg) => ({
256
- * 'nats.subject': msg.subject,
257
- * 'nats.stream': msg.info?.stream,
258
- * 'nats.consumer': msg.info?.consumer,
259
- * 'nats.delivered_count': msg.info?.redeliveryCount,
260
- * })
261
- * ```
262
- *
263
- * @example Cloudflare Queue attributes
264
- * ```typescript
265
- * customAttributes: (ctx, msg) => ({
266
- * 'cloudflare.queue_id': msg.id,
267
- * 'cloudflare.timestamp_ms': msg.timestamp.getTime(),
268
- * 'cloudflare.attempts': msg.attempts,
269
- * })
270
- * ```
271
- */
272
- customAttributes?: (
273
- ctx: ConsumerContext,
274
- msg: unknown,
275
- ) => Record<string, AttributeValue>;
276
-
277
- /**
278
- * Hook for custom context extraction (beyond W3C traceparent)
279
- *
280
- * Use this to extract parent span context from systems that use
281
- * non-standard header formats.
282
- *
283
- * @example Datadog context extraction
284
- * ```typescript
285
- * customContextExtractor: (headers) => {
286
- * const traceId = headers['x-datadog-trace-id'];
287
- * const spanId = headers['x-datadog-parent-id'];
288
- * if (!traceId || !spanId) return null;
289
- * return {
290
- * traceId: traceIdToOtel(traceId),
291
- * spanId: spanIdToOtel(spanId),
292
- * traceFlags: TraceFlags.SAMPLED,
293
- * };
294
- * }
295
- * ```
296
- *
297
- * @example B3 format extraction
298
- * ```typescript
299
- * customContextExtractor: (headers) => {
300
- * const traceId = headers['x-b3-traceid'];
301
- * const spanId = headers['x-b3-spanid'];
302
- * const sampled = headers['x-b3-sampled'] === '1';
303
- * if (!traceId || !spanId) return null;
304
- * return {
305
- * traceId,
306
- * spanId,
307
- * traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,
308
- * };
309
- * }
310
- * ```
311
- */
312
- customContextExtractor?: (
313
- headers: Record<string, string>,
314
- ) => SpanContext | null;
315
- }
316
-
317
- /**
318
- * Configuration for consumer lag metrics
319
- */
320
- export interface LagMetricsConfig {
321
- /** Get current message offset */
322
- getCurrentOffset?: (msg: unknown) => number | undefined;
323
-
324
- /** Get end offset (high watermark) - can be async */
325
- getEndOffset?: () => number | Promise<number>;
326
-
327
- /** Get committed offset - can be async */
328
- getCommittedOffset?: () => number | Promise<number>;
329
-
330
- /** Get partition from message */
331
- getPartition?: (msg: unknown) => number | undefined;
332
- }
333
-
334
- /**
335
- * Configuration for message ordering tracking
336
- */
337
- export interface OrderingConfig {
338
- /**
339
- * Extract sequence number from message
340
- *
341
- * Sequence numbers enable out-of-order detection and gap analysis.
342
- *
343
- * @example Kafka offset
344
- * ```typescript
345
- * sequenceFrom: (msg) => msg.offset
346
- * ```
347
- */
348
- sequenceFrom?: (msg: unknown) => number | undefined;
349
-
350
- /**
351
- * Extract partition key from message
352
- *
353
- * Partition keys determine message ordering in Kafka.
354
- *
355
- * @example Message key
356
- * ```typescript
357
- * partitionKeyFrom: (msg) => msg.key
358
- * ```
359
- */
360
- partitionKeyFrom?: (msg: unknown) => string | undefined;
361
-
362
- /**
363
- * Extract message ID for deduplication
364
- *
365
- * Used to detect duplicate messages.
366
- *
367
- * @example Idempotency key
368
- * ```typescript
369
- * messageIdFrom: (msg) => msg.headers['idempotency-key']
370
- * ```
371
- */
372
- messageIdFrom?: (msg: unknown) => string | undefined;
373
-
374
- /**
375
- * Enable out-of-order detection
376
- *
377
- * Tracks sequence numbers per partition and detects when messages
378
- * arrive out of order.
379
- *
380
- * @default false
381
- */
382
- detectOutOfOrder?: boolean;
383
-
384
- /**
385
- * Enable deduplication detection
386
- *
387
- * Tracks message IDs and detects duplicates within the window.
388
- *
389
- * @default false
390
- */
391
- detectDuplicates?: boolean;
392
-
393
- /**
394
- * Deduplication window size (number of message IDs to track)
395
- *
396
- * @default 1000
397
- */
398
- deduplicationWindowSize?: number;
399
-
400
- /**
401
- * Callback when out-of-order message detected
402
- */
403
- onOutOfOrder?: (ctx: ConsumerContext, info: OutOfOrderInfo) => void;
404
-
405
- /**
406
- * Callback when duplicate message detected
407
- */
408
- onDuplicate?: (ctx: ConsumerContext, messageId: string) => void;
409
- }
410
-
411
- /**
412
- * Information about out-of-order message
413
- */
414
- export interface OutOfOrderInfo {
415
- /** Current sequence number */
416
- currentSequence: number;
417
-
418
- /** Expected sequence number */
419
- expectedSequence: number;
420
-
421
- /** Partition key (if available) */
422
- partitionKey?: string;
423
-
424
- /** Gap size (positive = gap, negative = out of order) */
425
- gap: number;
426
- }
427
-
428
- // ============================================================================
429
- // Consumer Group Tracking Types
430
- // ============================================================================
431
-
432
- /**
433
- * Configuration for consumer group tracking
434
- *
435
- * Enables observability of consumer group state, including membership,
436
- * partition assignments, and rebalancing events.
437
- *
438
- * @example Kafka consumer group tracking
439
- * ```typescript
440
- * consumerGroupTracking: {
441
- * memberId: consumer.memberId,
442
- * groupInstanceId: process.env.CONSUMER_ID,
443
- * onRebalance: (ctx, event) => {
444
- * if (event.type === 'assigned') {
445
- * console.log(`Assigned partitions: ${event.partitions}`);
446
- * }
447
- * },
448
- * }
449
- * ```
450
- */
451
- export interface ConsumerGroupTrackingConfig {
452
- /**
453
- * Consumer member ID
454
- *
455
- * Unique identifier assigned by the broker to this consumer.
456
- */
457
- memberId?: string | (() => string | undefined);
458
-
459
- /**
460
- * Static group instance ID (for static membership)
461
- *
462
- * If set, enables static group membership which prevents
463
- * rebalances when consumers restart.
464
- */
465
- groupInstanceId?: string | (() => string | undefined);
466
-
467
- /**
468
- * Callback when rebalance occurs
469
- */
470
- onRebalance?: (ctx: ConsumerContext, event: RebalanceEvent) => void;
471
-
472
- /**
473
- * Callback when partitions are assigned
474
- */
475
- onPartitionsAssigned?: (
476
- ctx: ConsumerContext,
477
- partitions: PartitionAssignment[],
478
- ) => void;
479
-
480
- /**
481
- * Callback when partitions are revoked
482
- */
483
- onPartitionsRevoked?: (
484
- ctx: ConsumerContext,
485
- partitions: PartitionAssignment[],
486
- ) => void;
487
-
488
- /**
489
- * Track consumer lag per partition
490
- *
491
- * @default true
492
- */
493
- trackPartitionLag?: boolean;
494
-
495
- /**
496
- * Track consumer heartbeat health
497
- *
498
- * @default false
499
- */
500
- trackHeartbeat?: boolean;
501
-
502
- /**
503
- * Heartbeat interval in milliseconds (for health tracking)
504
- */
505
- heartbeatIntervalMs?: number;
506
- }
507
-
508
- /**
509
- * Rebalance event types
510
- */
511
- export type RebalanceType = 'assigned' | 'revoked' | 'lost';
512
-
513
- /**
514
- * Rebalance event information
515
- */
516
- export interface RebalanceEvent {
517
- /** Type of rebalance event */
518
- type: RebalanceType;
519
-
520
- /** Partitions affected by the rebalance */
521
- partitions: PartitionAssignment[];
522
-
523
- /** Timestamp of the rebalance event */
524
- timestamp: number;
525
-
526
- /** Generation ID (increments on each rebalance) */
527
- generation?: number;
528
-
529
- /** Consumer member ID */
530
- memberId?: string;
531
-
532
- /** Reason for the rebalance (if available) */
533
- reason?: string;
534
- }
535
-
536
- /**
537
- * Partition assignment information
538
- */
539
- export interface PartitionAssignment {
540
- /** Topic name */
541
- topic: string;
542
-
543
- /** Partition number */
544
- partition: number;
545
-
546
- /** Initial offset (if available) */
547
- offset?: number;
548
-
549
- /** Metadata (if available) */
550
- metadata?: string;
551
- }
552
-
553
- /**
554
- * Consumer group state snapshot
555
- */
556
- export interface ConsumerGroupState {
557
- /** Consumer group name */
558
- groupId: string;
559
-
560
- /** Consumer member ID */
561
- memberId?: string;
562
-
563
- /** Static instance ID (if using static membership) */
564
- groupInstanceId?: string;
565
-
566
- /** Currently assigned partitions */
567
- assignedPartitions: PartitionAssignment[];
568
-
569
- /** Group generation ID */
570
- generation?: number;
571
-
572
- /** Whether the consumer is currently active */
573
- isActive: boolean;
574
-
575
- /** Last heartbeat timestamp */
576
- lastHeartbeat?: number;
577
-
578
- /** Consumer state (stable, preparing_rebalance, completing_rebalance, dead) */
579
- state?:
580
- | 'stable'
581
- | 'preparing_rebalance'
582
- | 'completing_rebalance'
583
- | 'dead'
584
- | 'empty';
585
- }
586
-
587
- /**
588
- * Partition lag information
589
- */
590
- export interface PartitionLag {
591
- /** Topic name */
592
- topic: string;
593
-
594
- /** Partition number */
595
- partition: number;
596
-
597
- /** Current consumer offset */
598
- currentOffset: number;
599
-
600
- /** End offset (high watermark) */
601
- endOffset: number;
602
-
603
- /** Calculated lag */
604
- lag: number;
605
-
606
- /** Timestamp of measurement */
607
- timestamp: number;
608
- }
609
-
610
- /**
611
- * DLQ failure category types
612
- */
613
- export type DLQReasonCategory =
614
- | 'validation'
615
- | 'processing'
616
- | 'timeout'
617
- | 'poison'
618
- | 'unknown';
619
-
620
- /**
621
- * Options for enhanced DLQ recording
622
- */
623
- export interface DLQOptions {
624
- /**
625
- * Automatically link to the producer span context
626
- *
627
- * When true, creates a span link from the DLQ event back to
628
- * the original producer span for correlation.
629
- *
630
- * @default true
631
- */
632
- linkToProducer?: boolean;
633
-
634
- /**
635
- * Category of the failure that caused DLQ routing
636
- *
637
- * - validation: Message failed schema/format validation
638
- * - processing: Business logic error during processing
639
- * - timeout: Processing exceeded allowed time
640
- * - poison: Message causes repeated failures (poison pill)
641
- * - unknown: Uncategorized failure
642
- */
643
- reasonCategory?: DLQReasonCategory;
644
-
645
- /**
646
- * Number of processing attempts before DLQ routing
647
- */
648
- attemptCount?: number;
649
-
650
- /**
651
- * The original error that caused DLQ routing
652
- *
653
- * Error details are recorded as span attributes for debugging.
654
- */
655
- originalError?: Error;
656
-
657
- /**
658
- * Additional metadata to record with the DLQ event
659
- */
660
- metadata?: Record<string, string | number | boolean>;
661
- }
662
-
663
- /**
664
- * Options for recording DLQ replay
665
- */
666
- export interface DLQReplayOptions {
667
- /**
668
- * Original span context from DLQ message
669
- *
670
- * If provided, creates a span link to correlate with the original failure.
671
- */
672
- originalDLQSpanContext?: SpanContext;
673
-
674
- /**
675
- * Time spent in DLQ before replay (milliseconds)
676
- */
677
- dlqDwellTimeMs?: number;
678
-
679
- /**
680
- * Retry attempt number for this replay
681
- */
682
- replayAttempt?: number;
683
- }
684
-
685
- /**
686
- * Extended trace context for producers with header injection
687
- */
688
- export interface ProducerContext extends TraceContext {
689
- /**
690
- * Get W3C trace context headers to inject into message
691
- *
692
- * @returns Headers object with traceparent and optionally tracestate
693
- *
694
- * @example
695
- * ```typescript
696
- * const headers = ctx.getTraceHeaders();
697
- * await producer.send({
698
- * topic: 'events',
699
- * messages: [{ value: data, headers }]
700
- * });
701
- * ```
702
- */
703
- getTraceHeaders(): { traceparent: string; tracestate?: string };
704
-
705
- /**
706
- * Get all propagation headers including baggage if enabled
707
- *
708
- * @returns Headers object with all W3C trace context headers
709
- */
710
- getAllPropagationHeaders(): Record<string, string>;
711
-
712
- /**
713
- * Get all headers including custom headers from customHeaders hook
714
- *
715
- * This combines W3C trace context headers, baggage (if enabled),
716
- * and any custom headers defined via the customHeaders hook.
717
- *
718
- * @returns Combined headers object
719
- *
720
- * @example
721
- * ```typescript
722
- * const headers = ctx.getFullHeaders();
723
- * // Contains: traceparent, tracestate, baggage (if enabled), and custom headers
724
- * await producer.send({ topic, messages: [{ value, headers }] });
725
- * ```
726
- */
727
- getFullHeaders(): Record<string, string>;
728
- }
729
-
730
- /**
731
- * Extended trace context for consumers
732
- */
733
- export interface ConsumerContext extends TraceContext {
734
- /**
735
- * Record that a message is being sent to DLQ
736
- *
737
- * Enhanced with auto-linking to producer span, failure categorization,
738
- * and detailed error recording for comprehensive DLQ observability.
739
- *
740
- * @param reason - Human-readable reason for DLQ routing
741
- * @param dlqNameOrOptions - DLQ name (string) or enhanced options object
742
- * @param options - Enhanced DLQ options (when second param is dlqName)
743
- *
744
- * @example Basic usage (backwards compatible)
745
- * ```typescript
746
- * ctx.recordDLQ('Schema validation failed', 'orders-dlq');
747
- * ```
748
- *
749
- * @example Enhanced usage with options
750
- * ```typescript
751
- * ctx.recordDLQ('Invalid order total', 'orders-dlq', {
752
- * reasonCategory: 'validation',
753
- * attemptCount: 3,
754
- * originalError: error,
755
- * linkToProducer: true, // Auto-links to producer span
756
- * });
757
- * ```
758
- *
759
- * @example Using options object as second param
760
- * ```typescript
761
- * ctx.recordDLQ('Processing timeout', {
762
- * reasonCategory: 'timeout',
763
- * attemptCount: 5,
764
- * metadata: { processingTimeMs: 30000 },
765
- * });
766
- * ```
767
- */
768
- recordDLQ(reason: string, dlqName?: string, options?: DLQOptions): void;
769
- recordDLQ(reason: string, options?: DLQOptions): void;
770
-
771
- /**
772
- * Record replay of a message from DLQ
773
- *
774
- * Use this when processing a message that was replayed from the DLQ
775
- * to create links for correlation and track replay metrics.
776
- *
777
- * @param options - Replay tracking options
778
- *
779
- * @example
780
- * ```typescript
781
- * export const processReplay = traceConsumer({
782
- * system: 'kafka',
783
- * destination: 'orders-dlq-replay',
784
- * })(ctx => async (message) => {
785
- * ctx.recordReplay({
786
- * originalDLQSpanContext: extractSpanContext(message.headers),
787
- * dlqDwellTimeMs: Date.now() - message.timestamp,
788
- * replayAttempt: message.replayCount,
789
- * });
790
- * await processOrder(message);
791
- * });
792
- * ```
793
- */
794
- recordReplay(options?: DLQReplayOptions): void;
795
-
796
- /**
797
- * Record retry attempt
798
- *
799
- * @param attemptNumber - Current retry attempt (1-based)
800
- * @param maxAttempts - Maximum retry attempts
801
- */
802
- recordRetry(attemptNumber: number, maxAttempts?: number): void;
803
-
804
- /**
805
- * Get the producer span context links extracted from message headers
806
- *
807
- * Useful for accessing the producer span context when implementing
808
- * custom DLQ or retry logic.
809
- *
810
- * @returns Array of span links extracted from the message, or empty array
811
- */
812
- getProducerLinks(): Link[];
813
-
814
- // ---- Message Ordering Methods ----
815
-
816
- /**
817
- * Check if the current message is a duplicate
818
- *
819
- * @returns True if the message has been seen before
820
- */
821
- isDuplicate(): boolean;
822
-
823
- /**
824
- * Check if the current message arrived out of order
825
- *
826
- * @returns Out of order info, or null if in order
827
- */
828
- getOutOfOrderInfo(): OutOfOrderInfo | null;
829
-
830
- /**
831
- * Get current sequence number
832
- *
833
- * @returns The sequence number, or null if not configured
834
- */
835
- getSequenceNumber(): number | null;
836
-
837
- /**
838
- * Get partition key
839
- *
840
- * @returns The partition key, or null if not configured
841
- */
842
- getPartitionKey(): string | null;
843
-
844
- // ---- Consumer Group Methods ----
845
-
846
- /**
847
- * Record a rebalance event
848
- *
849
- * Call this when the consumer group undergoes a rebalance to capture
850
- * the event as a span event with partition assignment details.
851
- *
852
- * @param event - The rebalance event details
853
- *
854
- * @example
855
- * ```typescript
856
- * consumer.on('rebalance', (event) => {
857
- * ctx.recordRebalance({
858
- * type: event.type,
859
- * partitions: event.assignment,
860
- * generation: event.generationId,
861
- * timestamp: Date.now(),
862
- * });
863
- * });
864
- * ```
865
- */
866
- recordRebalance(event: RebalanceEvent): void;
867
-
868
- /**
869
- * Record a heartbeat event
870
- *
871
- * Call this on each heartbeat to track consumer health.
872
- *
873
- * @param healthy - Whether the heartbeat was successful
874
- * @param latencyMs - Optional latency of the heartbeat in milliseconds
875
- */
876
- recordHeartbeat(healthy: boolean, latencyMs?: number): void;
877
-
878
- /**
879
- * Record partition lag for a specific partition
880
- *
881
- * @param lag - The partition lag information
882
- */
883
- recordPartitionLag(lag: PartitionLag): void;
884
-
885
- /**
886
- * Get the current consumer group state
887
- *
888
- * @returns The current consumer group state, or null if not configured
889
- */
890
- getConsumerGroupState(): ConsumerGroupState | null;
891
-
892
- /**
893
- * Get the consumer member ID
894
- *
895
- * @returns The member ID, or null if not available
896
- */
897
- getMemberId(): string | null;
898
-
899
- /**
900
- * Get the current partition assignments
901
- *
902
- * @returns Array of assigned partitions, or empty array if none
903
- */
904
- getAssignedPartitions(): PartitionAssignment[];
905
- }
906
-
907
- // ============================================================================
908
- // Producer Helper
909
- // ============================================================================
910
-
911
- /**
912
- * Create a traced message producer function
913
- *
914
- * Sets SpanKind.PRODUCER, OTel messaging semantic attributes,
915
- * and provides context injection helpers.
916
- *
917
- * @param config - Producer configuration
918
- * @returns Factory function that wraps your producer logic
919
- *
920
- * @example Kafka producer
921
- * ```typescript
922
- * export const publishUserEvent = traceProducer({
923
- * system: 'kafka',
924
- * destination: 'user-events',
925
- * messageIdFrom: (args) => args[0]?.eventId,
926
- * })(ctx => async (event: UserEvent) => {
927
- * const headers = ctx.getTraceHeaders();
928
- * await producer.send({
929
- * topic: 'user-events',
930
- * messages: [{
931
- * key: event.userId,
932
- * value: JSON.stringify(event),
933
- * headers,
934
- * }]
935
- * });
936
- * });
937
- * ```
938
- *
939
- * @example SQS producer
940
- * ```typescript
941
- * export const sendToSQS = traceProducer({
942
- * system: 'sqs',
943
- * destination: 'orders-queue',
944
- * })(ctx => async (order: Order) => {
945
- * const headers = ctx.getAllPropagationHeaders();
946
- * await sqs.sendMessage({
947
- * QueueUrl: QUEUE_URL,
948
- * MessageBody: JSON.stringify(order),
949
- * MessageAttributes: headersToSQSAttributes(headers),
950
- * });
951
- * });
952
- * ```
953
- */
954
- export function traceProducer<TArgs extends unknown[], TReturn>(
955
- config: ProducerConfig,
956
- ) {
957
- const spanName = `${config.system}.publish ${config.destination}`;
958
-
959
- return (
960
- fnFactory: (ctx: ProducerContext) => (...args: TArgs) => Promise<TReturn>,
961
- ): ((...args: TArgs) => Promise<TReturn>) => {
962
- return trace<TArgs, TReturn>(
963
- { name: spanName, spanKind: SpanKind.PRODUCER },
964
- (baseCtx) => {
965
- // Extend context with producer-specific methods
966
- const ctx = extendContextForProducer(baseCtx, config);
967
-
968
- // Set semantic convention attributes
969
- setProducerAttributes(ctx, config);
970
-
971
- // Call beforeSend callback if provided
972
- return (...args: TArgs) => {
973
- // Extract dynamic attributes from args
974
- setDynamicProducerAttributes(ctx, config, args);
975
-
976
- // Apply custom attributes hook if provided
977
- if (config.customAttributes) {
978
- const customAttrs = config.customAttributes(ctx, args);
979
- for (const [key, value] of Object.entries(customAttrs)) {
980
- if (value !== undefined && value !== null) {
981
- ctx.setAttribute(key, value as string | number | boolean);
982
- }
983
- }
984
- }
985
-
986
- if (config.beforeSend) {
987
- config.beforeSend(ctx, args);
988
- }
989
-
990
- // Execute user's function
991
- const userFn = fnFactory(ctx);
992
- return userFn(...args).catch((error) => {
993
- if (config.onError) {
994
- config.onError(error as Error, ctx);
995
- }
996
- throw error;
997
- });
998
- };
999
- },
1000
- );
1001
- };
1002
- }
1003
-
1004
- // ============================================================================
1005
- // Consumer Helper
1006
- // ============================================================================
1007
-
1008
- /**
1009
- * Create a traced message consumer function
1010
- *
1011
- * Sets SpanKind.CONSUMER, OTel messaging semantic attributes,
1012
- * automatically extracts links from producer trace headers,
1013
- * and provides DLQ/retry recording helpers.
1014
- *
1015
- * @param config - Consumer configuration
1016
- * @returns Factory function that wraps your consumer logic
1017
- *
1018
- * @example Kafka consumer (single message)
1019
- * ```typescript
1020
- * export const processUserEvent = traceConsumer({
1021
- * system: 'kafka',
1022
- * destination: 'user-events',
1023
- * consumerGroup: 'event-processor',
1024
- * headersFrom: (msg) => msg.headers,
1025
- * })(ctx => async (message: KafkaMessage) => {
1026
- * // Link to producer span is automatically created
1027
- * const event = JSON.parse(message.value.toString());
1028
- * await processEvent(event);
1029
- * });
1030
- * ```
1031
- *
1032
- * @example Kafka consumer (batch mode)
1033
- * ```typescript
1034
- * export const processUserEventBatch = traceConsumer({
1035
- * system: 'kafka',
1036
- * destination: 'user-events',
1037
- * consumerGroup: 'event-processor',
1038
- * batchMode: true,
1039
- * headersFrom: (msg) => msg.headers,
1040
- * lagMetrics: {
1041
- * getCurrentOffset: (msg) => msg.offset,
1042
- * getEndOffset: () => consumer.getHighWatermark(),
1043
- * getPartition: (msg) => msg.partition,
1044
- * },
1045
- * })(ctx => async (messages: KafkaMessage[]) => {
1046
- * // Links to all producer spans are automatically created
1047
- * for (const msg of messages) {
1048
- * await processEvent(JSON.parse(msg.value.toString()));
1049
- * }
1050
- * });
1051
- * ```
1052
- *
1053
- * @example SQS consumer with DLQ handling
1054
- * ```typescript
1055
- * export const processSQSMessage = traceConsumer({
1056
- * system: 'sqs',
1057
- * destination: 'orders-queue',
1058
- * headersFrom: (msg) => sqsAttributesToHeaders(msg.MessageAttributes),
1059
- * onDLQ: (ctx, reason) => {
1060
- * ctx.recordDLQ(reason, 'orders-dlq');
1061
- * },
1062
- * })(ctx => async (message: SQSMessage) => {
1063
- * try {
1064
- * await processOrder(JSON.parse(message.Body));
1065
- * } catch (error) {
1066
- * if (message.ApproximateReceiveCount > 3) {
1067
- * ctx.recordDLQ(error.message);
1068
- * throw error;
1069
- * }
1070
- * ctx.recordRetry(message.ApproximateReceiveCount, 3);
1071
- * throw error;
1072
- * }
1073
- * });
1074
- * ```
1075
- */
1076
- export function traceConsumer<TArgs extends unknown[], TReturn>(
1077
- config: ConsumerConfig,
1078
- ) {
1079
- const operation = config.batchMode ? 'receive' : 'process';
1080
- const spanName = `${config.system}.${operation} ${config.destination}`;
1081
-
1082
- return (
1083
- fnFactory: (ctx: ConsumerContext) => (...args: TArgs) => Promise<TReturn>,
1084
- ): ((...args: TArgs) => Promise<TReturn>) => {
1085
- return trace<TArgs, TReturn>(
1086
- { name: spanName, spanKind: SpanKind.CONSUMER },
1087
- (baseCtx) => {
1088
- // Create mutable storage for producer links (populated during extractAndAddLinks)
1089
- const linkStorage: ProducerLinkStorage = { links: [] };
1090
-
1091
- // Create mutable ordering state (populated during extractOrdering)
1092
- const orderingState: OrderingState = {
1093
- sequenceNumber: null,
1094
- partitionKey: null,
1095
- messageId: null,
1096
- isDuplicate: false,
1097
- outOfOrderInfo: null,
1098
- };
1099
-
1100
- // Create consumer group state
1101
- const groupTracking = config.consumerGroupTracking;
1102
- const groupState: ConsumerGroupStateInternal = {
1103
- memberId:
1104
- typeof groupTracking?.memberId === 'function'
1105
- ? (groupTracking.memberId() ?? null)
1106
- : (groupTracking?.memberId ?? null),
1107
- groupInstanceId:
1108
- typeof groupTracking?.groupInstanceId === 'function'
1109
- ? (groupTracking.groupInstanceId() ?? null)
1110
- : (groupTracking?.groupInstanceId ?? null),
1111
- assignedPartitions: [],
1112
- generation: null,
1113
- isActive: true,
1114
- lastHeartbeat: null,
1115
- state: null,
1116
- };
1117
-
1118
- // Extend context with consumer-specific methods
1119
- const ctx = extendContextForConsumer(
1120
- baseCtx,
1121
- config,
1122
- linkStorage,
1123
- orderingState,
1124
- groupState,
1125
- );
1126
-
1127
- // Set semantic convention attributes
1128
- setConsumerAttributes(ctx, config);
1129
-
1130
- return async (...args: TArgs) => {
1131
- // Extract links from message headers (includes customContextExtractor if provided)
1132
- // This also populates linkStorage.links for getProducerLinks() and DLQ auto-linking
1133
- await extractAndAddLinks(ctx, config, args, linkStorage);
1134
-
1135
- // Extract and process ordering information
1136
- if (config.ordering) {
1137
- extractAndProcessOrdering(ctx, config, args, orderingState);
1138
- }
1139
-
1140
- // Extract lag metrics if configured
1141
- if (config.lagMetrics) {
1142
- await extractLagMetrics(ctx, config.lagMetrics, args);
1143
- }
1144
-
1145
- // Apply custom attributes hook if provided
1146
- if (config.customAttributes) {
1147
- // For batch mode, extract first message; for single mode, use args[0] directly
1148
- const batch = args[0];
1149
- const msg =
1150
- config.batchMode && Array.isArray(batch) && batch.length > 0
1151
- ? batch[0]
1152
- : batch;
1153
- // Only call hook if we have a message
1154
- if (msg !== undefined) {
1155
- const customAttrs = config.customAttributes(ctx, msg);
1156
- for (const [key, value] of Object.entries(customAttrs)) {
1157
- if (value !== undefined && value !== null) {
1158
- ctx.setAttribute(key, value as string | number | boolean);
1159
- }
1160
- }
1161
- }
1162
- }
1163
-
1164
- // Execute user's function
1165
- const userFn = fnFactory(ctx);
1166
- return userFn(...args).catch((error) => {
1167
- if (config.onError) {
1168
- config.onError(error as Error, ctx);
1169
- }
1170
- throw error;
1171
- });
1172
- };
1173
- },
1174
- );
1175
- };
1176
- }
1177
-
1178
- // ============================================================================
1179
- // Helper Functions
1180
- // ============================================================================
1181
-
1182
- /**
1183
- * Extend base context with producer-specific methods
1184
- */
1185
- function extendContextForProducer(
1186
- baseCtx: TraceContext,
1187
- config: ProducerConfig,
1188
- ): ProducerContext {
1189
- // Create a reference for `this` binding in getFullHeaders
1190
- const producerCtx: ProducerContext = {
1191
- ...baseCtx,
1192
-
1193
- getTraceHeaders(): { traceparent: string; tracestate?: string } {
1194
- const headers: Record<string, string> = {};
1195
- propagation.inject(context.active(), headers);
1196
-
1197
- const result: { traceparent: string; tracestate?: string } = {
1198
- traceparent: headers['traceparent'] || '',
1199
- };
1200
-
1201
- if (headers['tracestate']) {
1202
- result.tracestate = headers['tracestate'];
1203
- }
1204
-
1205
- return result;
1206
- },
1207
-
1208
- getAllPropagationHeaders(): Record<string, string> {
1209
- const headers: Record<string, string> = {};
1210
- propagation.inject(context.active(), headers);
1211
-
1212
- // Include baggage if configured
1213
- if (config.propagateBaggage) {
1214
- const baggage = propagation.getBaggage(context.active());
1215
- if (baggage) {
1216
- const entries: string[] = [];
1217
- for (const [key, value] of baggage.getAllEntries()) {
1218
- entries.push(
1219
- `${encodeURIComponent(key)}=${encodeURIComponent(value.value)}`,
1220
- );
1221
- }
1222
- if (entries.length > 0) {
1223
- headers['baggage'] = entries.join(',');
1224
- }
1225
- }
1226
- }
1227
-
1228
- return headers;
1229
- },
1230
-
1231
- getFullHeaders(): Record<string, string> {
1232
- // Start with all propagation headers (W3C + baggage)
1233
- const headers = producerCtx.getAllPropagationHeaders();
1234
-
1235
- // Add custom headers from hook if configured
1236
- if (config.customHeaders) {
1237
- const customHeaders = config.customHeaders(producerCtx);
1238
- Object.assign(headers, customHeaders);
1239
- }
1240
-
1241
- return headers;
1242
- },
1243
- };
1244
-
1245
- return producerCtx;
1246
- }
1247
-
1248
- /**
1249
- * Mutable storage for producer links (populated during extractAndAddLinks)
1250
- */
1251
- interface ProducerLinkStorage {
1252
- links: Link[];
1253
- }
1254
-
1255
- /**
1256
- * Ordering state for a single message
1257
- */
1258
- interface OrderingState {
1259
- sequenceNumber: number | null;
1260
- partitionKey: string | null;
1261
- messageId: string | null;
1262
- isDuplicate: boolean;
1263
- outOfOrderInfo: OutOfOrderInfo | null;
1264
- }
1265
-
1266
- /**
1267
- * Global sequence tracker for out-of-order detection (per partition)
1268
- */
1269
- const sequenceTrackers = new Map<string, number>();
1270
-
1271
- /**
1272
- * Global deduplication window (LRU-style using Map insertion order)
1273
- */
1274
- const deduplicationWindow = new Map<string, number>();
1275
- const DEFAULT_DEDUP_WINDOW_SIZE = 1000;
1276
-
1277
- /**
1278
- * Clean up old entries from deduplication window
1279
- */
1280
- function trimDeduplicationWindow(maxSize: number): void {
1281
- if (deduplicationWindow.size > maxSize) {
1282
- const excess = deduplicationWindow.size - maxSize;
1283
- const iterator = deduplicationWindow.keys();
1284
- for (let i = 0; i < excess; i++) {
1285
- const key = iterator.next().value;
1286
- if (key) deduplicationWindow.delete(key);
1287
- }
1288
- }
1289
- }
1290
-
1291
- /**
1292
- * Consumer group state tracking for a single consumer
1293
- */
1294
- interface ConsumerGroupStateInternal {
1295
- memberId: string | null;
1296
- groupInstanceId: string | null;
1297
- assignedPartitions: PartitionAssignment[];
1298
- generation: number | null;
1299
- isActive: boolean;
1300
- lastHeartbeat: number | null;
1301
- state: ConsumerGroupState['state'] | null;
1302
- }
1303
-
1304
- /**
1305
- * Extend base context with consumer-specific methods
1306
- */
1307
- function extendContextForConsumer(
1308
- baseCtx: TraceContext,
1309
- config: ConsumerConfig,
1310
- linkStorage: ProducerLinkStorage,
1311
- orderingState: OrderingState,
1312
- groupState: ConsumerGroupStateInternal,
1313
- ): ConsumerContext {
1314
- const consumerCtx: ConsumerContext = {
1315
- ...baseCtx,
1316
-
1317
- recordDLQ(
1318
- reason: string,
1319
- dlqNameOrOptions?: string | DLQOptions,
1320
- optionsParam?: DLQOptions,
1321
- ): void {
1322
- // Parse overloaded arguments
1323
- let dlqName: string | undefined;
1324
- let options: DLQOptions | undefined;
1325
-
1326
- if (typeof dlqNameOrOptions === 'string') {
1327
- dlqName = dlqNameOrOptions;
1328
- options = optionsParam;
1329
- } else if (typeof dlqNameOrOptions === 'object') {
1330
- options = dlqNameOrOptions;
1331
- }
1332
-
1333
- // Default linkToProducer to true
1334
- const linkToProducer = options?.linkToProducer ?? true;
1335
-
1336
- // Set basic DLQ attributes
1337
- baseCtx.setAttribute('messaging.dlq.reason', reason);
1338
- if (dlqName) {
1339
- baseCtx.setAttribute('messaging.dlq.name', dlqName);
1340
- }
1341
-
1342
- // Set enhanced DLQ attributes
1343
- if (options?.reasonCategory) {
1344
- baseCtx.setAttribute(
1345
- 'messaging.dlq.reason_category',
1346
- options.reasonCategory,
1347
- );
1348
- }
1349
- if (options?.attemptCount !== undefined) {
1350
- baseCtx.setAttribute(
1351
- 'messaging.dlq.attempt_count',
1352
- options.attemptCount,
1353
- );
1354
- }
1355
- if (options?.originalError) {
1356
- baseCtx.setAttribute(
1357
- 'messaging.dlq.error.type',
1358
- options.originalError.name,
1359
- );
1360
- baseCtx.setAttribute(
1361
- 'messaging.dlq.error.message',
1362
- options.originalError.message,
1363
- );
1364
- }
1365
-
1366
- // Set custom metadata
1367
- if (options?.metadata) {
1368
- for (const [key, value] of Object.entries(options.metadata)) {
1369
- baseCtx.setAttribute(`messaging.dlq.metadata.${key}`, value);
1370
- }
1371
- }
1372
-
1373
- // Auto-link to producer span if available and enabled
1374
- const producerLink = linkStorage.links[0];
1375
- if (linkToProducer && producerLink) {
1376
- baseCtx.setAttribute(
1377
- 'messaging.dlq.producer_trace_id',
1378
- producerLink.context.traceId,
1379
- );
1380
- baseCtx.setAttribute(
1381
- 'messaging.dlq.producer_span_id',
1382
- producerLink.context.spanId,
1383
- );
1384
- }
1385
-
1386
- // Record event with all attributes
1387
- const eventAttrs: Record<string, string | number | boolean> = {
1388
- 'messaging.dlq.reason': reason,
1389
- ...(dlqName && { 'messaging.dlq.name': dlqName }),
1390
- ...(options?.reasonCategory && {
1391
- 'messaging.dlq.reason_category': options.reasonCategory,
1392
- }),
1393
- ...(options?.attemptCount !== undefined && {
1394
- 'messaging.dlq.attempt_count': options.attemptCount,
1395
- }),
1396
- ...(options?.originalError && {
1397
- 'messaging.dlq.error.type': options.originalError.name,
1398
- 'messaging.dlq.error.message': options.originalError.message,
1399
- }),
1400
- };
1401
-
1402
- // Add producer link info to event if available
1403
- if (linkToProducer && producerLink) {
1404
- eventAttrs['messaging.dlq.producer_trace_id'] =
1405
- producerLink.context.traceId;
1406
- eventAttrs['messaging.dlq.producer_span_id'] =
1407
- producerLink.context.spanId;
1408
- }
1409
-
1410
- emitCorrelatedEvent(baseCtx, 'dlq_routed', eventAttrs);
1411
-
1412
- // Call user's onDLQ callback if provided
1413
- if (config.onDLQ) {
1414
- config.onDLQ(consumerCtx, reason);
1415
- }
1416
- },
1417
-
1418
- recordReplay(options?: DLQReplayOptions): void {
1419
- baseCtx.setAttribute('messaging.replay', true);
1420
-
1421
- if (options?.replayAttempt !== undefined) {
1422
- baseCtx.setAttribute('messaging.replay.attempt', options.replayAttempt);
1423
- }
1424
- if (options?.dlqDwellTimeMs !== undefined) {
1425
- baseCtx.setAttribute(
1426
- 'messaging.replay.dwell_time_ms',
1427
- options.dlqDwellTimeMs,
1428
- );
1429
- }
1430
-
1431
- // Create span link to original DLQ span if provided
1432
- if (options?.originalDLQSpanContext) {
1433
- baseCtx.addLinks([
1434
- {
1435
- context: options.originalDLQSpanContext,
1436
- attributes: { 'messaging.link.source': 'dlq_replay' },
1437
- },
1438
- ]);
1439
- }
1440
-
1441
- const eventAttrs: Record<string, string | number | boolean> = {
1442
- 'messaging.replay': true,
1443
- ...(options?.replayAttempt !== undefined && {
1444
- 'messaging.replay.attempt': options.replayAttempt,
1445
- }),
1446
- ...(options?.dlqDwellTimeMs !== undefined && {
1447
- 'messaging.replay.dwell_time_ms': options.dlqDwellTimeMs,
1448
- }),
1449
- };
1450
-
1451
- emitCorrelatedEvent(baseCtx, 'dlq_replay', eventAttrs);
1452
- },
1453
-
1454
- recordRetry(attemptNumber: number, maxAttempts?: number): void {
1455
- baseCtx.setAttribute('messaging.retry.count', attemptNumber);
1456
- if (maxAttempts !== undefined) {
1457
- baseCtx.setAttribute('messaging.retry.max_attempts', maxAttempts);
1458
- }
1459
- emitCorrelatedEvent(baseCtx, 'retry_attempt', {
1460
- 'messaging.retry.count': attemptNumber,
1461
- ...(maxAttempts !== undefined && {
1462
- 'messaging.retry.max_attempts': maxAttempts,
1463
- }),
1464
- });
1465
- },
1466
-
1467
- getProducerLinks(): Link[] {
1468
- return [...linkStorage.links];
1469
- },
1470
-
1471
- // ---- Ordering Methods ----
1472
-
1473
- isDuplicate(): boolean {
1474
- return orderingState.isDuplicate;
1475
- },
1476
-
1477
- getOutOfOrderInfo(): OutOfOrderInfo | null {
1478
- return orderingState.outOfOrderInfo;
1479
- },
1480
-
1481
- getSequenceNumber(): number | null {
1482
- return orderingState.sequenceNumber;
1483
- },
1484
-
1485
- getPartitionKey(): string | null {
1486
- return orderingState.partitionKey;
1487
- },
1488
-
1489
- // ---- Consumer Group Methods ----
1490
-
1491
- recordRebalance(event: RebalanceEvent): void {
1492
- // Update internal state including consumer group state
1493
- if (event.type === 'assigned') {
1494
- groupState.assignedPartitions = event.partitions;
1495
- groupState.isActive = true;
1496
- // After assignment completes, group is stable
1497
- groupState.state = 'stable';
1498
- } else if (event.type === 'revoked' || event.type === 'lost') {
1499
- // Remove revoked partitions from assignments
1500
- const revokedSet = new Set(
1501
- event.partitions.map((p) => `${p.topic}:${p.partition}`),
1502
- );
1503
- groupState.assignedPartitions = groupState.assignedPartitions.filter(
1504
- (p) => !revokedSet.has(`${p.topic}:${p.partition}`),
1505
- );
1506
- if (event.type === 'lost') {
1507
- groupState.isActive = false;
1508
- // Consumer lost connection, mark as dead
1509
- groupState.state = 'dead';
1510
- } else {
1511
- // Revoked means rebalance is starting
1512
- // If no partitions remain, consumer is empty; otherwise preparing for rebalance
1513
- groupState.state =
1514
- groupState.assignedPartitions.length === 0
1515
- ? 'empty'
1516
- : 'preparing_rebalance';
1517
- }
1518
- }
1519
-
1520
- if (event.generation !== undefined) {
1521
- groupState.generation = event.generation;
1522
- }
1523
- if (event.memberId) {
1524
- groupState.memberId = event.memberId;
1525
- }
1526
-
1527
- // Set span attributes
1528
- baseCtx.setAttribute(
1529
- 'messaging.consumer_group.rebalance.type',
1530
- event.type,
1531
- );
1532
- baseCtx.setAttribute(
1533
- 'messaging.consumer_group.rebalance.partition_count',
1534
- event.partitions.length,
1535
- );
1536
- if (event.generation !== undefined) {
1537
- baseCtx.setAttribute(
1538
- 'messaging.consumer_group.generation',
1539
- event.generation,
1540
- );
1541
- }
1542
- if (event.memberId) {
1543
- baseCtx.setAttribute(
1544
- 'messaging.consumer_group.member_id',
1545
- event.memberId,
1546
- );
1547
- }
1548
- if (event.reason) {
1549
- baseCtx.setAttribute(
1550
- 'messaging.consumer_group.rebalance.reason',
1551
- event.reason,
1552
- );
1553
- }
1554
-
1555
- // Set the new state on the span
1556
- if (groupState.state) {
1557
- baseCtx.setAttribute(
1558
- 'messaging.consumer_group.state',
1559
- groupState.state,
1560
- );
1561
- }
1562
-
1563
- // Record event
1564
- const eventAttrs: Record<string, string | number | boolean> = {
1565
- 'messaging.consumer_group.rebalance.type': event.type,
1566
- 'messaging.consumer_group.rebalance.partition_count':
1567
- event.partitions.length,
1568
- 'messaging.consumer_group.rebalance.timestamp': event.timestamp,
1569
- ...(event.generation !== undefined && {
1570
- 'messaging.consumer_group.generation': event.generation,
1571
- }),
1572
- ...(event.memberId && {
1573
- 'messaging.consumer_group.member_id': event.memberId,
1574
- }),
1575
- ...(event.reason && {
1576
- 'messaging.consumer_group.rebalance.reason': event.reason,
1577
- }),
1578
- ...(groupState.state && {
1579
- 'messaging.consumer_group.state': groupState.state,
1580
- }),
1581
- };
1582
-
1583
- // Add partition details if not too many
1584
- if (event.partitions.length <= 10) {
1585
- eventAttrs['messaging.consumer_group.rebalance.partitions'] =
1586
- event.partitions.map((p) => `${p.topic}:${p.partition}`).join(',');
1587
- }
1588
-
1589
- emitCorrelatedEvent(baseCtx, `consumer_group_${event.type}`, eventAttrs);
1590
-
1591
- // Call user's onRebalance callback if provided
1592
- if (config.consumerGroupTracking?.onRebalance) {
1593
- config.consumerGroupTracking.onRebalance(consumerCtx, event);
1594
- }
1595
-
1596
- // Call specific callbacks
1597
- if (
1598
- event.type === 'assigned' &&
1599
- config.consumerGroupTracking?.onPartitionsAssigned
1600
- ) {
1601
- config.consumerGroupTracking.onPartitionsAssigned(
1602
- consumerCtx,
1603
- event.partitions,
1604
- );
1605
- }
1606
- if (
1607
- event.type === 'revoked' &&
1608
- config.consumerGroupTracking?.onPartitionsRevoked
1609
- ) {
1610
- config.consumerGroupTracking.onPartitionsRevoked(
1611
- consumerCtx,
1612
- event.partitions,
1613
- );
1614
- }
1615
- },
1616
-
1617
- recordHeartbeat(healthy: boolean, latencyMs?: number): void {
1618
- groupState.lastHeartbeat = Date.now();
1619
-
1620
- baseCtx.setAttribute(
1621
- 'messaging.consumer_group.heartbeat.healthy',
1622
- healthy,
1623
- );
1624
- if (latencyMs !== undefined) {
1625
- baseCtx.setAttribute(
1626
- 'messaging.consumer_group.heartbeat.latency_ms',
1627
- latencyMs,
1628
- );
1629
- }
1630
-
1631
- emitCorrelatedEvent(baseCtx, 'consumer_group_heartbeat', {
1632
- 'messaging.consumer_group.heartbeat.healthy': healthy,
1633
- 'messaging.consumer_group.heartbeat.timestamp':
1634
- groupState.lastHeartbeat,
1635
- ...(latencyMs !== undefined && {
1636
- 'messaging.consumer_group.heartbeat.latency_ms': latencyMs,
1637
- }),
1638
- });
1639
- },
1640
-
1641
- recordPartitionLag(lag: PartitionLag): void {
1642
- const prefix = `messaging.consumer_group.lag.${lag.topic}.${lag.partition}`;
1643
-
1644
- baseCtx.setAttribute(`${prefix}.current_offset`, lag.currentOffset);
1645
- baseCtx.setAttribute(`${prefix}.end_offset`, lag.endOffset);
1646
- baseCtx.setAttribute(`${prefix}.lag`, lag.lag);
1647
-
1648
- emitCorrelatedEvent(baseCtx, 'partition_lag_recorded', {
1649
- 'messaging.consumer_group.lag.topic': lag.topic,
1650
- 'messaging.consumer_group.lag.partition': lag.partition,
1651
- 'messaging.consumer_group.lag.current_offset': lag.currentOffset,
1652
- 'messaging.consumer_group.lag.end_offset': lag.endOffset,
1653
- 'messaging.consumer_group.lag.lag': lag.lag,
1654
- 'messaging.consumer_group.lag.timestamp': lag.timestamp,
1655
- });
1656
- },
1657
-
1658
- getConsumerGroupState(): ConsumerGroupState | null {
1659
- if (!config.consumerGroup) {
1660
- return null;
1661
- }
1662
-
1663
- return {
1664
- groupId: config.consumerGroup,
1665
- memberId: groupState.memberId ?? undefined,
1666
- groupInstanceId: groupState.groupInstanceId ?? undefined,
1667
- assignedPartitions: [...groupState.assignedPartitions],
1668
- generation: groupState.generation ?? undefined,
1669
- isActive: groupState.isActive,
1670
- lastHeartbeat: groupState.lastHeartbeat ?? undefined,
1671
- state: groupState.state ?? undefined,
1672
- };
1673
- },
1674
-
1675
- getMemberId(): string | null {
1676
- return groupState.memberId;
1677
- },
1678
-
1679
- getAssignedPartitions(): PartitionAssignment[] {
1680
- return [...groupState.assignedPartitions];
1681
- },
1682
- };
1683
-
1684
- return consumerCtx;
1685
- }
1686
-
1687
- /**
1688
- * Set OTel semantic convention attributes for producer
1689
- */
1690
- function setProducerAttributes(
1691
- ctx: TraceContext,
1692
- config: ProducerConfig,
1693
- ): void {
1694
- ctx.setAttribute('messaging.system', config.system);
1695
- ctx.setAttribute('messaging.operation', 'publish');
1696
- ctx.setAttribute('messaging.destination.name', config.destination);
1697
-
1698
- // Set system-specific destination attribute
1699
- if (config.system === 'kafka') {
1700
- ctx.setAttribute('messaging.kafka.destination.topic', config.destination);
1701
- }
1702
-
1703
- // Set custom attributes
1704
- if (config.attributes) {
1705
- setCustomAttributes(ctx, config.attributes);
1706
- }
1707
- }
1708
-
1709
- /**
1710
- * Set dynamic producer attributes from arguments
1711
- */
1712
- function setDynamicProducerAttributes(
1713
- ctx: TraceContext,
1714
- config: ProducerConfig,
1715
- args: unknown[],
1716
- ): void {
1717
- // Message ID
1718
- if (config.messageIdFrom) {
1719
- const messageId = extractValue(config.messageIdFrom, args);
1720
- if (messageId !== undefined) {
1721
- ctx.setAttribute('messaging.message.id', String(messageId));
1722
- }
1723
- }
1724
-
1725
- // Partition (Kafka-specific)
1726
- if (config.partitionFrom) {
1727
- const partition = extractValue(config.partitionFrom, args);
1728
- if (partition !== undefined) {
1729
- ctx.setAttribute(
1730
- 'messaging.kafka.destination.partition',
1731
- Number(partition),
1732
- );
1733
- }
1734
- }
1735
-
1736
- // Key (Kafka-specific)
1737
- if (config.keyFrom) {
1738
- const key = extractValue(config.keyFrom, args);
1739
- if (key !== undefined) {
1740
- ctx.setAttribute('messaging.kafka.message.key', String(key));
1741
- }
1742
- }
1743
- }
1744
-
1745
- /**
1746
- * Set OTel semantic convention attributes for consumer
1747
- */
1748
- function setConsumerAttributes(
1749
- ctx: TraceContext,
1750
- config: ConsumerConfig,
1751
- ): void {
1752
- ctx.setAttribute('messaging.system', config.system);
1753
- ctx.setAttribute(
1754
- 'messaging.operation',
1755
- config.batchMode ? 'receive' : 'process',
1756
- );
1757
- ctx.setAttribute('messaging.destination.name', config.destination);
1758
-
1759
- // Consumer group
1760
- if (config.consumerGroup) {
1761
- ctx.setAttribute('messaging.consumer.group', config.consumerGroup);
1762
-
1763
- // System-specific consumer group attribute
1764
- if (config.system === 'kafka') {
1765
- ctx.setAttribute('messaging.kafka.consumer.group', config.consumerGroup);
1766
- }
1767
- }
1768
-
1769
- // Set system-specific destination attribute
1770
- if (config.system === 'kafka') {
1771
- ctx.setAttribute('messaging.kafka.destination.topic', config.destination);
1772
- }
1773
-
1774
- // Set custom attributes
1775
- if (config.attributes) {
1776
- setCustomAttributes(ctx, config.attributes);
1777
- }
1778
- }
1779
-
1780
- /**
1781
- * Extract links from message headers and add to span
1782
- *
1783
- * Uses W3C trace context by default, falls back to customContextExtractor if provided.
1784
- * Also populates linkStorage for getProducerLinks() and DLQ auto-linking.
1785
- */
1786
- async function extractAndAddLinks(
1787
- ctx: ConsumerContext,
1788
- config: ConsumerConfig,
1789
- args: unknown[],
1790
- linkStorage: ProducerLinkStorage,
1791
- ): Promise<void> {
1792
- if (!config.headersFrom && !config.customContextExtractor) {
1793
- return;
1794
- }
1795
-
1796
- const links: Link[] = [];
1797
-
1798
- if (config.batchMode && Array.isArray(args[0])) {
1799
- // Batch mode - extract links from all messages
1800
- const messages = args[0] as unknown[];
1801
-
1802
- if (config.headersFrom) {
1803
- const batchLinks = extractLinksFromBatch(
1804
- messages.map((msg) => {
1805
- const headers = extractHeaders(config.headersFrom!, msg);
1806
- return { headers };
1807
- }),
1808
- 'headers',
1809
- );
1810
- links.push(...batchLinks);
1811
- }
1812
-
1813
- // Try custom context extractor for messages without W3C links
1814
- if (config.customContextExtractor && config.headersFrom) {
1815
- for (const msg of messages) {
1816
- const headers = extractHeaders(config.headersFrom, msg);
1817
- if (headers) {
1818
- // Only use custom extractor if W3C headers weren't present
1819
- const w3cLink = createLinkFromHeaders(headers);
1820
- if (!w3cLink) {
1821
- const customContext = config.customContextExtractor(headers);
1822
- if (customContext) {
1823
- links.push({
1824
- context: customContext,
1825
- attributes: { 'messaging.link.source': 'custom_extractor' },
1826
- });
1827
- }
1828
- }
1829
- }
1830
- }
1831
- }
1832
-
1833
- // Set batch count
1834
- ctx.setAttribute('messaging.batch.message_count', messages.length);
1835
- } else {
1836
- // Single message mode
1837
- const msg = args[0];
1838
- const headers = config.headersFrom
1839
- ? extractHeaders(config.headersFrom, msg)
1840
- : undefined;
1841
-
1842
- if (headers) {
1843
- // Try W3C format first
1844
- const w3cLink = createLinkFromHeaders(headers);
1845
- if (w3cLink) {
1846
- links.push(w3cLink);
1847
- } else if (config.customContextExtractor) {
1848
- // Fall back to custom extractor
1849
- const customContext = config.customContextExtractor(headers);
1850
- if (customContext) {
1851
- links.push({
1852
- context: customContext,
1853
- attributes: { 'messaging.link.source': 'custom_extractor' },
1854
- });
1855
- }
1856
- }
1857
- }
1858
- }
1859
-
1860
- // Add all extracted links and store for getProducerLinks() / DLQ auto-linking
1861
- if (links.length > 0) {
1862
- ctx.addLinks(links);
1863
- linkStorage.links.push(...links);
1864
- }
1865
- }
1866
-
1867
- /**
1868
- * Extract lag metrics and set as span attributes
1869
- */
1870
- async function extractLagMetrics(
1871
- ctx: ConsumerContext,
1872
- lagConfig: LagMetricsConfig,
1873
- args: unknown[],
1874
- ): Promise<void> {
1875
- const msg = Array.isArray(args[0]) ? args[0][0] : args[0];
1876
-
1877
- // Current offset
1878
- let currentOffset: number | undefined;
1879
- if (lagConfig.getCurrentOffset && msg) {
1880
- currentOffset = lagConfig.getCurrentOffset(msg);
1881
- if (currentOffset !== undefined) {
1882
- ctx.setAttribute('messaging.kafka.message.offset', currentOffset);
1883
- }
1884
- }
1885
-
1886
- // Partition
1887
- if (lagConfig.getPartition && msg) {
1888
- const partition = lagConfig.getPartition(msg);
1889
- if (partition !== undefined) {
1890
- ctx.setAttribute('messaging.kafka.partition', partition);
1891
- }
1892
- }
1893
-
1894
- // End offset (high watermark) and lag calculation
1895
- if (lagConfig.getEndOffset) {
1896
- try {
1897
- const endOffset = await Promise.resolve(lagConfig.getEndOffset());
1898
- if (endOffset !== undefined && currentOffset !== undefined) {
1899
- const lag = endOffset - currentOffset;
1900
- ctx.setAttribute('messaging.kafka.consumer_lag', lag);
1901
-
1902
- // Add lag event
1903
- emitCorrelatedEvent(ctx, 'consumer_lag_measured', {
1904
- 'messaging.kafka.consumer_lag': lag,
1905
- 'messaging.kafka.message.offset': currentOffset,
1906
- 'messaging.kafka.high_watermark': endOffset,
1907
- });
1908
- }
1909
- } catch {
1910
- // Ignore lag calculation errors
1911
- }
1912
- }
1913
-
1914
- // Committed offset
1915
- if (lagConfig.getCommittedOffset) {
1916
- try {
1917
- const committedOffset = await Promise.resolve(
1918
- lagConfig.getCommittedOffset(),
1919
- );
1920
- if (committedOffset !== undefined) {
1921
- ctx.setAttribute('messaging.kafka.committed_offset', committedOffset);
1922
- }
1923
- } catch {
1924
- // Ignore committed offset errors
1925
- }
1926
- }
1927
-
1928
- // Batch-specific metrics
1929
- if (Array.isArray(args[0]) && args[0].length > 0) {
1930
- const messages = args[0] as unknown[];
1931
- if (lagConfig.getCurrentOffset) {
1932
- const firstOffset = lagConfig.getCurrentOffset(messages[0]);
1933
- const lastMessage = messages.at(-1);
1934
- const lastOffset =
1935
- lastMessage === undefined
1936
- ? undefined
1937
- : lagConfig.getCurrentOffset(lastMessage);
1938
-
1939
- if (firstOffset !== undefined) {
1940
- ctx.setAttribute('messaging.batch.first_offset', firstOffset);
1941
- }
1942
- if (lastOffset !== undefined) {
1943
- ctx.setAttribute('messaging.batch.last_offset', lastOffset);
1944
- }
1945
- }
1946
- }
1947
- }
1948
-
1949
- /**
1950
- * Extract headers from message using config
1951
- */
1952
- function extractHeaders(
1953
- headersFrom: string | ((msg: unknown) => Record<string, string> | undefined),
1954
- msg: unknown,
1955
- ): Record<string, string> | undefined {
1956
- if (typeof headersFrom === 'function') {
1957
- return headersFrom(msg);
1958
- }
1959
-
1960
- // String path - extract from message property
1961
- if (typeof msg === 'object' && msg !== null) {
1962
- const value = (msg as Record<string, unknown>)[headersFrom];
1963
- if (typeof value === 'object' && value !== null) {
1964
- return value as Record<string, string>;
1965
- }
1966
- }
1967
-
1968
- return undefined;
1969
- }
1970
-
1971
- /**
1972
- * Extract value from arguments using config
1973
- */
1974
- function extractValue(
1975
- extractor: string | ((args: unknown[]) => unknown),
1976
- args: unknown[],
1977
- ): unknown {
1978
- if (typeof extractor === 'function') {
1979
- return extractor(args);
1980
- }
1981
-
1982
- // String path - extract from first argument
1983
- const firstArg = args[0];
1984
- if (typeof firstArg === 'object' && firstArg !== null) {
1985
- return (firstArg as Record<string, unknown>)[extractor];
1986
- }
1987
-
1988
- return undefined;
1989
- }
1990
-
1991
- /**
1992
- * Set custom attributes on context, handling non-primitive values
1993
- */
1994
- function setCustomAttributes(ctx: TraceContext, attributes: Attributes): void {
1995
- for (const [key, value] of Object.entries(attributes)) {
1996
- if (value !== undefined && value !== null) {
1997
- // setAttribute accepts primitives and arrays of primitives
1998
- if (
1999
- typeof value === 'string' ||
2000
- typeof value === 'number' ||
2001
- typeof value === 'boolean'
2002
- ) {
2003
- ctx.setAttribute(key, value);
2004
- } else if (Array.isArray(value)) {
2005
- // Filter out null/undefined from arrays and ensure proper typing
2006
- const cleanArray = value.filter(
2007
- (v): v is string | number | boolean =>
2008
- v !== null &&
2009
- v !== undefined &&
2010
- (typeof v === 'string' ||
2011
- typeof v === 'number' ||
2012
- typeof v === 'boolean'),
2013
- );
2014
- if (cleanArray.length > 0) {
2015
- ctx.setAttribute(key, cleanArray as string[] | number[] | boolean[]);
2016
- }
2017
- } else {
2018
- ctx.setAttribute(key, JSON.stringify(value));
2019
- }
2020
- }
2021
- }
2022
- }
2023
-
2024
- /**
2025
- * Extract and process ordering information from message
2026
- *
2027
- * Handles:
2028
- * - Sequence number extraction and tracking
2029
- * - Out-of-order detection
2030
- * - Duplicate detection
2031
- * - Span attribute setting
2032
- * - Callback invocation
2033
- */
2034
- function extractAndProcessOrdering(
2035
- ctx: ConsumerContext,
2036
- config: ConsumerConfig,
2037
- args: unknown[],
2038
- orderingState: OrderingState,
2039
- ): void {
2040
- const ordering = config.ordering;
2041
- if (!ordering) return;
2042
-
2043
- // Get messages to process - all messages in batch mode, single message otherwise
2044
- const messages: unknown[] =
2045
- config.batchMode && Array.isArray(args[0]) ? args[0] : [args[0]];
2046
-
2047
- if (messages.length === 0) return;
2048
-
2049
- // Track aggregate stats for batch reporting
2050
- let outOfOrderCount = 0;
2051
- let duplicateCount = 0;
2052
- let lastSequence: number | null = null;
2053
- let lastPartitionKey: string | null = null;
2054
- let lastMessageId: string | null = null;
2055
-
2056
- for (const [i, msg] of messages.entries()) {
2057
- if (!msg) continue;
2058
-
2059
- // Per-message state for this iteration
2060
- let msgSequence: number | null = null;
2061
- let msgPartitionKey: string | null = null;
2062
- let msgId: string | null = null;
2063
-
2064
- // Extract sequence number
2065
- if (ordering.sequenceFrom) {
2066
- const seq = ordering.sequenceFrom(msg);
2067
- if (seq !== undefined) {
2068
- msgSequence = seq;
2069
- lastSequence = seq;
2070
- }
2071
- }
2072
-
2073
- // Extract partition key
2074
- if (ordering.partitionKeyFrom) {
2075
- const key = ordering.partitionKeyFrom(msg);
2076
- if (key !== undefined) {
2077
- msgPartitionKey = key;
2078
- lastPartitionKey = key;
2079
- }
2080
- }
2081
-
2082
- // Extract message ID for deduplication
2083
- if (ordering.messageIdFrom) {
2084
- const id = ordering.messageIdFrom(msg);
2085
- if (id !== undefined) {
2086
- msgId = id;
2087
- lastMessageId = id;
2088
- }
2089
- }
2090
-
2091
- // Out-of-order detection for this message
2092
- if (ordering.detectOutOfOrder && msgSequence !== null) {
2093
- // Build tracker key using per-message partition key
2094
- const msgOrderingState: OrderingState = {
2095
- sequenceNumber: msgSequence,
2096
- partitionKey: msgPartitionKey,
2097
- messageId: msgId,
2098
- isDuplicate: false,
2099
- outOfOrderInfo: null,
2100
- };
2101
- const trackerKey = buildTrackerKey(config, msgOrderingState);
2102
- const prevSequence = sequenceTrackers.get(trackerKey);
2103
-
2104
- if (prevSequence !== undefined) {
2105
- const expectedSequence = prevSequence + 1;
2106
-
2107
- if (msgSequence !== expectedSequence) {
2108
- outOfOrderCount++;
2109
- const gap = msgSequence - expectedSequence;
2110
- const outOfOrderInfo: OutOfOrderInfo = {
2111
- currentSequence: msgSequence,
2112
- expectedSequence,
2113
- partitionKey: msgPartitionKey ?? undefined,
2114
- gap,
2115
- };
2116
-
2117
- // Store the first out-of-order info for backward compatibility
2118
- if (!orderingState.outOfOrderInfo) {
2119
- orderingState.outOfOrderInfo = outOfOrderInfo;
2120
- }
2121
-
2122
- // Add event for each out-of-order message
2123
- emitCorrelatedEvent(ctx, 'message_out_of_order', {
2124
- 'messaging.ordering.batch_index': i,
2125
- 'messaging.ordering.current_sequence': msgSequence,
2126
- 'messaging.ordering.expected_sequence': expectedSequence,
2127
- 'messaging.ordering.gap': gap,
2128
- ...(msgPartitionKey && {
2129
- 'messaging.ordering.partition_key': msgPartitionKey,
2130
- }),
2131
- });
2132
-
2133
- // Call user callback if provided
2134
- if (ordering.onOutOfOrder) {
2135
- ordering.onOutOfOrder(ctx, outOfOrderInfo);
2136
- }
2137
- }
2138
- }
2139
-
2140
- // Update tracker with this message's sequence
2141
- sequenceTrackers.set(trackerKey, msgSequence);
2142
- }
2143
-
2144
- // Duplicate detection for this message
2145
- if (ordering.detectDuplicates && msgId !== null) {
2146
- const msgOrderingState: OrderingState = {
2147
- sequenceNumber: msgSequence,
2148
- partitionKey: msgPartitionKey,
2149
- messageId: msgId,
2150
- isDuplicate: false,
2151
- outOfOrderInfo: null,
2152
- };
2153
- const dedupKey = buildDedupKey(config, msgOrderingState);
2154
-
2155
- if (deduplicationWindow.has(dedupKey)) {
2156
- duplicateCount++;
2157
-
2158
- // Add event for each duplicate
2159
- emitCorrelatedEvent(ctx, 'message_duplicate', {
2160
- 'messaging.ordering.batch_index': i,
2161
- 'messaging.message.id': msgId,
2162
- });
2163
-
2164
- // Call user callback if provided
2165
- if (ordering.onDuplicate) {
2166
- ordering.onDuplicate(ctx, msgId);
2167
- }
2168
- } else {
2169
- // Add to deduplication window
2170
- deduplicationWindow.set(dedupKey, Date.now());
2171
-
2172
- // Trim window if needed
2173
- const windowSize =
2174
- ordering.deduplicationWindowSize ?? DEFAULT_DEDUP_WINDOW_SIZE;
2175
- trimDeduplicationWindow(windowSize);
2176
- }
2177
- }
2178
- }
2179
-
2180
- // Update orderingState with final values from the batch
2181
- orderingState.sequenceNumber = lastSequence;
2182
- orderingState.partitionKey = lastPartitionKey;
2183
- orderingState.messageId = lastMessageId;
2184
- orderingState.isDuplicate = duplicateCount > 0;
2185
-
2186
- // Set aggregate span attributes
2187
- if (lastSequence !== null) {
2188
- ctx.setAttribute('messaging.message.sequence_number', lastSequence);
2189
- }
2190
- if (lastPartitionKey !== null) {
2191
- ctx.setAttribute('messaging.message.partition_key', lastPartitionKey);
2192
- }
2193
- if (lastMessageId !== null) {
2194
- ctx.setAttribute('messaging.message.id', lastMessageId);
2195
- }
2196
-
2197
- // Report batch-level ordering statistics
2198
- if (outOfOrderCount > 0) {
2199
- ctx.setAttribute('messaging.ordering.out_of_order', true);
2200
- ctx.setAttribute('messaging.ordering.out_of_order_count', outOfOrderCount);
2201
- }
2202
- if (duplicateCount > 0) {
2203
- ctx.setAttribute('messaging.ordering.duplicate', true);
2204
- ctx.setAttribute('messaging.ordering.duplicate_count', duplicateCount);
2205
- }
2206
- }
2207
-
2208
- /**
2209
- * Build a unique key for sequence tracking based on system, destination, and partition
2210
- */
2211
- function buildTrackerKey(
2212
- config: ConsumerConfig,
2213
- orderingState: OrderingState,
2214
- ): string {
2215
- const parts = [config.system, config.destination];
2216
- if (orderingState.partitionKey) {
2217
- parts.push(orderingState.partitionKey);
2218
- }
2219
- if (config.consumerGroup) {
2220
- parts.push(config.consumerGroup);
2221
- }
2222
- return parts.join(':');
2223
- }
2224
-
2225
- /**
2226
- * Build a unique key for deduplication based on system, destination, and message ID
2227
- */
2228
- function buildDedupKey(
2229
- config: ConsumerConfig,
2230
- orderingState: OrderingState,
2231
- ): string {
2232
- const parts = [config.system, config.destination];
2233
- if (orderingState.messageId) {
2234
- parts.push(orderingState.messageId);
2235
- }
2236
- return parts.join(':');
2237
- }
2238
-
2239
- /**
2240
- * Clear sequence tracking state (useful for testing)
2241
- */
2242
- export function clearOrderingState(): void {
2243
- sequenceTrackers.clear();
2244
- deduplicationWindow.clear();
2245
- }