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
@@ -1,916 +0,0 @@
1
- /**
2
- * Distributed workflow tracing with cross-service correlation
3
- *
4
- * Enables tracking workflows that span multiple microservices by propagating
5
- * workflow identity (workflowId, stepName, stepIndex) via baggage in message headers.
6
- *
7
- * Unlike local workflow.ts (which uses AsyncLocalStorage), distributed workflows
8
- * propagate context across network boundaries using W3C baggage.
9
- *
10
- * @example Order fulfillment saga across services
11
- * ```typescript
12
- * // Service A: Order Service
13
- * import { traceDistributedWorkflow, WorkflowBaggage } from 'autotel/workflow-distributed';
14
- * import { traceProducer } from 'autotel/messaging';
15
- *
16
- * export const createOrder = traceDistributedWorkflow({
17
- * name: 'OrderFulfillment',
18
- * workflowIdFrom: (order) => order.id,
19
- * version: '1.0.0',
20
- * })(ctx => async (order: Order) => {
21
- * // Workflow baggage is auto-set
22
- * await publishToInventory(order);
23
- * });
24
- *
25
- * const publishToInventory = traceProducer({
26
- * system: 'kafka',
27
- * destination: 'inventory-requests',
28
- * propagateBaggage: true, // Includes workflow.* baggage
29
- * })(ctx => async (order) => {
30
- * await producer.send({ topic: 'inventory-requests', value: order });
31
- * });
32
- *
33
- * // Service B: Inventory Service
34
- * import { traceDistributedStep, WorkflowBaggage } from 'autotel/workflow-distributed';
35
- *
36
- * export const processInventory = traceDistributedStep({
37
- * name: 'ReserveInventory',
38
- * extractBaggage: true, // Extracts workflow.* from headers
39
- * })(ctx => async (message) => {
40
- * const workflow = WorkflowBaggage.get(ctx);
41
- * // workflow.workflowId === order.id (propagated from Service A)
42
- * console.log(`Processing step for workflow ${workflow.workflowId}`);
43
- * await reserveItems(message.items);
44
- * });
45
- * ```
46
- *
47
- * @module
48
- */
49
-
50
- import { context, propagation, SpanKind } from '@opentelemetry/api';
51
- import { createSafeBaggageSchema } from './business-baggage';
52
- import { emitCorrelatedEvent } from './correlated-events';
53
- import { trace } from './functional';
54
- import type { TraceContext } from './trace-context';
55
-
56
- // ============================================================================
57
- // Workflow Baggage Schema
58
- // ============================================================================
59
-
60
- /**
61
- * Workflow baggage field definitions
62
- */
63
- const workflowBaggageFields = {
64
- /** Unique identifier for the workflow instance */
65
- workflowId: { type: 'string' as const, maxLength: 128, required: true },
66
-
67
- /** Name/type of the workflow (e.g., "OrderFulfillment") */
68
- workflowName: { type: 'string' as const, maxLength: 64, required: true },
69
-
70
- /** Version of the workflow definition */
71
- workflowVersion: { type: 'string' as const, maxLength: 32 },
72
-
73
- /** Current step name */
74
- stepName: { type: 'string' as const, maxLength: 64 },
75
-
76
- /** Current step index (0-based) */
77
- stepIndex: { type: 'number' as const },
78
-
79
- /** Total number of steps (if known) */
80
- totalSteps: { type: 'number' as const },
81
-
82
- /** Parent workflow ID (for sub-workflows) */
83
- parentWorkflowId: { type: 'string' as const, maxLength: 128 },
84
-
85
- /** Correlation ID for external systems */
86
- correlationId: { type: 'string' as const, maxLength: 128 },
87
-
88
- /** Workflow priority */
89
- priority: {
90
- type: 'enum' as const,
91
- values: ['low', 'normal', 'high', 'critical'] as const,
92
- },
93
-
94
- /** Initiating user/system */
95
- initiatedBy: { type: 'string' as const, maxLength: 64 },
96
-
97
- /** Workflow start timestamp (ISO) */
98
- startedAt: { type: 'string' as const, maxLength: 30 },
99
- } as const;
100
-
101
- /**
102
- * Pre-built baggage schema for distributed workflows
103
- *
104
- * Use this to read/write workflow context that propagates across services.
105
- *
106
- * @example Setting workflow baggage
107
- * ```typescript
108
- * WorkflowBaggage.set(ctx, {
109
- * workflowId: 'order-12345',
110
- * workflowName: 'OrderFulfillment',
111
- * stepName: 'ReserveInventory',
112
- * stepIndex: 1,
113
- * });
114
- * ```
115
- *
116
- * @example Reading workflow baggage in downstream service
117
- * ```typescript
118
- * const { workflowId, workflowName, stepIndex } = WorkflowBaggage.get(ctx);
119
- * console.log(`Processing ${workflowName} step ${stepIndex}`);
120
- * ```
121
- */
122
- export const WorkflowBaggage = createSafeBaggageSchema(workflowBaggageFields, {
123
- prefix: 'workflow',
124
- hashHighCardinality: false, // Workflow IDs should be traceable
125
- redactPII: false, // Workflow fields are internal identifiers
126
- });
127
-
128
- /**
129
- * Type for workflow baggage values
130
- */
131
- export type WorkflowBaggageValues = {
132
- workflowId: string;
133
- workflowName: string;
134
- workflowVersion?: string;
135
- stepName?: string;
136
- stepIndex?: number;
137
- totalSteps?: number;
138
- parentWorkflowId?: string;
139
- correlationId?: string;
140
- priority?: 'low' | 'normal' | 'high' | 'critical';
141
- initiatedBy?: string;
142
- startedAt?: string;
143
- };
144
-
145
- // ============================================================================
146
- // Types
147
- // ============================================================================
148
-
149
- /**
150
- * Configuration for distributed workflow tracing
151
- */
152
- export interface DistributedWorkflowConfig {
153
- /** Workflow name/type (e.g., "OrderFulfillment", "UserOnboarding") */
154
- name: string;
155
-
156
- /**
157
- * Extract workflow ID from function arguments
158
- *
159
- * Receives all arguments passed to the workflow function, allowing
160
- * multi-parameter handlers to derive workflow IDs from any argument.
161
- *
162
- * @example Single argument
163
- * ```typescript
164
- * workflowIdFrom: (order) => order.id
165
- * ```
166
- *
167
- * @example Multiple arguments (payload + metadata)
168
- * ```typescript
169
- * workflowIdFrom: (payload, metadata) => metadata.correlationId ?? payload.id
170
- * ```
171
- */
172
- workflowIdFrom: (...args: unknown[]) => string;
173
-
174
- /** Workflow version (e.g., "1.0.0", "2023-01-15") */
175
- version?: string;
176
-
177
- /** Total number of steps if known */
178
- totalSteps?: number;
179
-
180
- /** Parent workflow ID (for sub-workflows) */
181
- parentWorkflowId?: string;
182
-
183
- /** Correlation ID for external systems */
184
- correlationId?: string;
185
-
186
- /** Workflow priority */
187
- priority?: 'low' | 'normal' | 'high' | 'critical';
188
-
189
- /** User/system that initiated the workflow */
190
- initiatedBy?: string;
191
-
192
- /** Additional span attributes */
193
- attributes?: Record<string, string | number | boolean>;
194
-
195
- /** Callback on workflow start */
196
- onStart?: (ctx: DistributedWorkflowContext) => void;
197
-
198
- /** Callback on workflow completion */
199
- onComplete?: (ctx: DistributedWorkflowContext, result: unknown) => void;
200
-
201
- /** Callback on workflow error */
202
- onError?: (ctx: DistributedWorkflowContext, error: Error) => void;
203
- }
204
-
205
- /**
206
- * Configuration for distributed workflow step
207
- */
208
- export interface DistributedStepConfig {
209
- /** Step name (e.g., "ReserveInventory", "ChargePayment") */
210
- name: string;
211
-
212
- /**
213
- * Extract baggage from incoming message/request
214
- *
215
- * If true, reads workflow baggage from current context (assumes already extracted).
216
- * If function, extracts from arguments.
217
- *
218
- * @default true
219
- */
220
- extractBaggage?:
221
- | boolean
222
- | ((args: unknown[]) => WorkflowBaggageValues | null);
223
-
224
- /** Override step index (otherwise uses baggage or auto-increments) */
225
- stepIndex?: number;
226
-
227
- /** Additional span attributes */
228
- attributes?: Record<string, string | number | boolean>;
229
-
230
- /** Whether this step is idempotent (safe to retry) */
231
- idempotent?: boolean;
232
-
233
- /** Whether this step is a compensation/rollback step */
234
- isCompensation?: boolean;
235
-
236
- /** Callback on step start */
237
- onStart?: (ctx: DistributedStepContext) => void;
238
-
239
- /** Callback on step completion */
240
- onComplete?: (ctx: DistributedStepContext, result: unknown) => void;
241
-
242
- /** Callback on step error */
243
- onError?: (ctx: DistributedStepContext, error: Error) => void;
244
- }
245
-
246
- /**
247
- * Extended context for distributed workflow root
248
- */
249
- export interface DistributedWorkflowContext extends TraceContext {
250
- /** The workflow ID */
251
- workflowId: string;
252
-
253
- /** The workflow name */
254
- workflowName: string;
255
-
256
- /** The workflow version */
257
- workflowVersion?: string;
258
-
259
- /** Get workflow baggage for propagation to other services */
260
- getWorkflowBaggage(): WorkflowBaggageValues;
261
-
262
- /** Set additional workflow baggage fields */
263
- setWorkflowBaggage(values: Partial<WorkflowBaggageValues>): void;
264
-
265
- /** Get headers with workflow baggage for outgoing requests */
266
- getWorkflowHeaders(): Record<string, string>;
267
-
268
- /** Record workflow step completion (for progress tracking) */
269
- recordStepProgress(stepName: string, stepIndex: number): void;
270
- }
271
-
272
- /**
273
- * Extended context for distributed workflow step
274
- */
275
- export interface DistributedStepContext extends TraceContext {
276
- /** The workflow ID (from baggage) */
277
- workflowId: string | null;
278
-
279
- /** The workflow name (from baggage) */
280
- workflowName: string | null;
281
-
282
- /** The current step name */
283
- stepName: string;
284
-
285
- /** The current step index */
286
- stepIndex: number | null;
287
-
288
- /** Whether this step is a compensation */
289
- isCompensation: boolean;
290
-
291
- /** Get the full workflow baggage */
292
- getWorkflowBaggage(): WorkflowBaggageValues | null;
293
-
294
- /** Update workflow baggage (e.g., increment step index) */
295
- updateWorkflowBaggage(values: Partial<WorkflowBaggageValues>): void;
296
-
297
- /** Get headers with updated workflow baggage for downstream calls */
298
- getWorkflowHeaders(): Record<string, string>;
299
-
300
- /** Mark step as requiring compensation on failure */
301
- requiresCompensation(compensationData?: Record<string, unknown>): void;
302
- }
303
-
304
- // ============================================================================
305
- // Distributed Workflow Tracer
306
- // ============================================================================
307
-
308
- /**
309
- * Create a traced distributed workflow function
310
- *
311
- * Wraps a function as the entry point for a distributed workflow. Automatically:
312
- * - Generates or extracts workflow ID
313
- * - Sets workflow baggage for downstream propagation
314
- * - Creates root span with workflow attributes
315
- *
316
- * @param config - Workflow configuration
317
- * @returns Factory function for the workflow handler
318
- *
319
- * @example Basic usage
320
- * ```typescript
321
- * export const createOrder = traceDistributedWorkflow({
322
- * name: 'OrderFulfillment',
323
- * workflowIdFrom: (order) => order.id,
324
- * version: '1.0.0',
325
- * })(ctx => async (order: Order) => {
326
- * ctx.recordStepProgress('ValidateOrder', 0);
327
- * await validateOrder(order);
328
- *
329
- * ctx.recordStepProgress('ReserveInventory', 1);
330
- * await publishToInventoryService(order);
331
- *
332
- * return { workflowId: ctx.workflowId, status: 'started' };
333
- * });
334
- * ```
335
- */
336
- export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
337
- config: DistributedWorkflowConfig,
338
- ) {
339
- const spanName = `workflow.${config.name}`;
340
-
341
- return (
342
- fnFactory: (
343
- ctx: DistributedWorkflowContext,
344
- ) => (...args: TArgs) => Promise<TReturn>,
345
- ): ((...args: TArgs) => Promise<TReturn>) => {
346
- return trace<TArgs, TReturn>(
347
- { name: spanName, spanKind: SpanKind.INTERNAL },
348
- (baseCtx) => {
349
- return async (...args: TArgs) => {
350
- // Extract workflow ID from arguments (spread to allow multi-arg access)
351
- const workflowId = config.workflowIdFrom(...args);
352
- const startedAt = new Date().toISOString();
353
-
354
- // Initialize workflow baggage
355
- const baggageValues: WorkflowBaggageValues = {
356
- workflowId,
357
- workflowName: config.name,
358
- workflowVersion: config.version,
359
- stepIndex: 0,
360
- totalSteps: config.totalSteps,
361
- parentWorkflowId: config.parentWorkflowId,
362
- correlationId: config.correlationId,
363
- priority: config.priority,
364
- initiatedBy: config.initiatedBy,
365
- startedAt,
366
- };
367
-
368
- // Set baggage
369
- WorkflowBaggage.set(baseCtx, baggageValues);
370
-
371
- // Set span attributes
372
- baseCtx.setAttribute('workflow.id', workflowId);
373
- baseCtx.setAttribute('workflow.name', config.name);
374
- if (config.version) {
375
- baseCtx.setAttribute('workflow.version', config.version);
376
- }
377
- if (config.totalSteps) {
378
- baseCtx.setAttribute('workflow.total_steps', config.totalSteps);
379
- }
380
- if (config.parentWorkflowId) {
381
- baseCtx.setAttribute('workflow.parent_id', config.parentWorkflowId);
382
- }
383
- if (config.priority) {
384
- baseCtx.setAttribute('workflow.priority', config.priority);
385
- }
386
- if (config.initiatedBy) {
387
- baseCtx.setAttribute('workflow.initiated_by', config.initiatedBy);
388
- }
389
- baseCtx.setAttribute('workflow.started_at', startedAt);
390
-
391
- // Apply custom attributes
392
- if (config.attributes) {
393
- for (const [key, value] of Object.entries(config.attributes)) {
394
- baseCtx.setAttribute(key, value);
395
- }
396
- }
397
-
398
- // Create extended context
399
- const workflowCtx: DistributedWorkflowContext = {
400
- ...baseCtx,
401
- workflowId,
402
- workflowName: config.name,
403
- workflowVersion: config.version,
404
-
405
- getWorkflowBaggage(): WorkflowBaggageValues {
406
- return { ...baggageValues };
407
- },
408
-
409
- setWorkflowBaggage(values: Partial<WorkflowBaggageValues>): void {
410
- Object.assign(baggageValues, values);
411
- WorkflowBaggage.set(baseCtx, baggageValues);
412
- },
413
-
414
- getWorkflowHeaders(): Record<string, string> {
415
- const headers: Record<string, string> = {};
416
- const ctx = context.active();
417
- propagation.inject(ctx, headers);
418
- return headers;
419
- },
420
-
421
- recordStepProgress(stepName: string, stepIndex: number): void {
422
- baggageValues.stepName = stepName;
423
- baggageValues.stepIndex = stepIndex;
424
- WorkflowBaggage.set(baseCtx, baggageValues);
425
-
426
- emitCorrelatedEvent(baseCtx, 'workflow.step_progress', {
427
- 'workflow.step.name': stepName,
428
- 'workflow.step.index': stepIndex,
429
- });
430
- },
431
- };
432
-
433
- // Call onStart callback
434
- config.onStart?.(workflowCtx);
435
-
436
- // Add start event
437
- emitCorrelatedEvent(baseCtx, 'workflow.started', {
438
- 'workflow.id': workflowId,
439
- 'workflow.name': config.name,
440
- });
441
-
442
- try {
443
- const userFn = fnFactory(workflowCtx);
444
- const result = await userFn(...args);
445
-
446
- // Call onComplete callback
447
- config.onComplete?.(workflowCtx, result);
448
-
449
- // Add completion event
450
- emitCorrelatedEvent(baseCtx, 'workflow.completed', {
451
- 'workflow.id': workflowId,
452
- });
453
-
454
- return result;
455
- } catch (error) {
456
- // Call onError callback
457
- config.onError?.(workflowCtx, error as Error);
458
-
459
- // Add error event
460
- emitCorrelatedEvent(baseCtx, 'workflow.failed', {
461
- 'workflow.id': workflowId,
462
- 'workflow.error': (error as Error).message,
463
- });
464
-
465
- throw error;
466
- }
467
- };
468
- },
469
- );
470
- };
471
- }
472
-
473
- // ============================================================================
474
- // Distributed Step Tracer
475
- // ============================================================================
476
-
477
- /**
478
- * Create a traced distributed workflow step
479
- *
480
- * Use in downstream services to trace steps that are part of a distributed workflow.
481
- * Automatically extracts workflow baggage from the current context.
482
- *
483
- * @param config - Step configuration
484
- * @returns Factory function for the step handler
485
- *
486
- * @example Consumer in downstream service
487
- * ```typescript
488
- * export const processInventory = traceConsumer({
489
- * system: 'kafka',
490
- * destination: 'inventory-requests',
491
- * extractBaggage: true, // Extracts workflow.* from headers
492
- * })(ctx => {
493
- * // Wrap inner logic with traceDistributedStep
494
- * return traceDistributedStep({
495
- * name: 'ReserveInventory',
496
- * })(stepCtx => async (message) => {
497
- * console.log(`Processing workflow ${stepCtx.workflowId}`);
498
- * await reserveItems(message.items);
499
- * })(message);
500
- * });
501
- * ```
502
- *
503
- * @example Standalone step handler
504
- * ```typescript
505
- * export const reserveInventory = traceDistributedStep({
506
- * name: 'ReserveInventory',
507
- * idempotent: true,
508
- * })(ctx => async (request: InventoryRequest) => {
509
- * if (ctx.workflowId) {
510
- * console.log(`Part of workflow ${ctx.workflowId}, step ${ctx.stepIndex}`);
511
- * }
512
- * return await inventoryService.reserve(request.items);
513
- * });
514
- * ```
515
- */
516
- export function traceDistributedStep<TArgs extends unknown[], TReturn>(
517
- config: DistributedStepConfig,
518
- ) {
519
- const spanName = `workflow.step.${config.name}`;
520
-
521
- return (
522
- fnFactory: (
523
- ctx: DistributedStepContext,
524
- ) => (...args: TArgs) => Promise<TReturn>,
525
- ): ((...args: TArgs) => Promise<TReturn>) => {
526
- return trace<TArgs, TReturn>(
527
- { name: spanName, spanKind: SpanKind.INTERNAL },
528
- (baseCtx) => {
529
- return async (...args: TArgs) => {
530
- // Extract workflow baggage
531
- let baggageValues: WorkflowBaggageValues | null = null;
532
-
533
- const extractBaggage = config.extractBaggage ?? true;
534
- if (typeof extractBaggage === 'function') {
535
- baggageValues = extractBaggage(args);
536
- } else if (extractBaggage) {
537
- // Read from current context
538
- const extracted = WorkflowBaggage.get(baseCtx);
539
- if (extracted.workflowId && extracted.workflowName) {
540
- baggageValues = extracted as WorkflowBaggageValues;
541
- }
542
- }
543
-
544
- // Determine step index
545
- // If explicit stepIndex provided in config, use it
546
- // Otherwise, auto-increment from baggage if available
547
- let stepIndex: number | null;
548
- if (config.stepIndex !== undefined) {
549
- stepIndex = config.stepIndex;
550
- } else if (baggageValues?.stepIndex === undefined) {
551
- stepIndex = null;
552
- } else {
553
- // Auto-increment from previous step
554
- stepIndex = baggageValues.stepIndex + 1;
555
- }
556
-
557
- // Update baggage with current step
558
- if (baggageValues) {
559
- baggageValues.stepName = config.name;
560
- if (stepIndex !== null) {
561
- baggageValues.stepIndex = stepIndex;
562
- }
563
- WorkflowBaggage.set(baseCtx, baggageValues);
564
- }
565
-
566
- // Set span attributes
567
- baseCtx.setAttribute('workflow.step.name', config.name);
568
- if (stepIndex !== null) {
569
- baseCtx.setAttribute('workflow.step.index', stepIndex);
570
- }
571
- if (config.idempotent !== undefined) {
572
- baseCtx.setAttribute('workflow.step.idempotent', config.idempotent);
573
- }
574
- if (config.isCompensation) {
575
- baseCtx.setAttribute('workflow.step.is_compensation', true);
576
- }
577
-
578
- // Add workflow context attributes if available
579
- if (baggageValues) {
580
- baseCtx.setAttribute('workflow.id', baggageValues.workflowId);
581
- baseCtx.setAttribute('workflow.name', baggageValues.workflowName);
582
- if (baggageValues.workflowVersion) {
583
- baseCtx.setAttribute(
584
- 'workflow.version',
585
- baggageValues.workflowVersion,
586
- );
587
- }
588
- if (baggageValues.totalSteps) {
589
- baseCtx.setAttribute(
590
- 'workflow.total_steps',
591
- baggageValues.totalSteps,
592
- );
593
- }
594
- }
595
-
596
- // Apply custom attributes
597
- if (config.attributes) {
598
- for (const [key, value] of Object.entries(config.attributes)) {
599
- baseCtx.setAttribute(key, value);
600
- }
601
- }
602
-
603
- // Compensation data storage
604
- let compensationData: Record<string, unknown> | undefined;
605
-
606
- // Create extended context
607
- const stepCtx: DistributedStepContext = {
608
- ...baseCtx,
609
- workflowId: baggageValues?.workflowId ?? null,
610
- workflowName: baggageValues?.workflowName ?? null,
611
- stepName: config.name,
612
- stepIndex,
613
- isCompensation: config.isCompensation ?? false,
614
-
615
- getWorkflowBaggage(): WorkflowBaggageValues | null {
616
- return baggageValues ? { ...baggageValues } : null;
617
- },
618
-
619
- updateWorkflowBaggage(
620
- values: Partial<WorkflowBaggageValues>,
621
- ): void {
622
- if (baggageValues) {
623
- Object.assign(baggageValues, values);
624
- WorkflowBaggage.set(baseCtx, baggageValues);
625
- }
626
- },
627
-
628
- getWorkflowHeaders(): Record<string, string> {
629
- const headers: Record<string, string> = {};
630
- const ctx = context.active();
631
- propagation.inject(ctx, headers);
632
- return headers;
633
- },
634
-
635
- requiresCompensation(data?: Record<string, unknown>): void {
636
- compensationData = data;
637
- baseCtx.setAttribute('workflow.step.requires_compensation', true);
638
- emitCorrelatedEvent(
639
- baseCtx,
640
- 'workflow.step.compensation_registered',
641
- {
642
- 'workflow.step.name': config.name,
643
- ...(data && {
644
- 'workflow.step.compensation_data': JSON.stringify(data),
645
- }),
646
- },
647
- );
648
- },
649
- };
650
-
651
- // Call onStart callback
652
- config.onStart?.(stepCtx);
653
-
654
- // Add start event
655
- emitCorrelatedEvent(baseCtx, 'workflow.step.started', {
656
- 'workflow.step.name': config.name,
657
- ...(baggageValues && { 'workflow.id': baggageValues.workflowId }),
658
- });
659
-
660
- try {
661
- const userFn = fnFactory(stepCtx);
662
- const result = await userFn(...args);
663
-
664
- // Call onComplete callback
665
- config.onComplete?.(stepCtx, result);
666
-
667
- // Add completion event
668
- emitCorrelatedEvent(baseCtx, 'workflow.step.completed', {
669
- 'workflow.step.name': config.name,
670
- });
671
-
672
- return result;
673
- } catch (error) {
674
- // Call onError callback
675
- config.onError?.(stepCtx, error as Error);
676
-
677
- // Add error event with compensation info if registered
678
- emitCorrelatedEvent(baseCtx, 'workflow.step.failed', {
679
- 'workflow.step.name': config.name,
680
- 'workflow.step.error': (error as Error).message,
681
- ...(compensationData && {
682
- 'workflow.step.requires_compensation': true,
683
- }),
684
- });
685
-
686
- throw error;
687
- }
688
- };
689
- },
690
- );
691
- };
692
- }
693
-
694
- // ============================================================================
695
- // Utility Functions
696
- // ============================================================================
697
-
698
- /**
699
- * Generate a unique workflow ID
700
- *
701
- * @param prefix - Optional prefix for the ID
702
- * @returns A unique workflow ID
703
- *
704
- * @example
705
- * ```typescript
706
- * const workflowId = generateWorkflowId('order'); // "order-abc123def456"
707
- * ```
708
- */
709
- export function generateWorkflowId(prefix?: string): string {
710
- const random = Math.random().toString(36).slice(2, 15);
711
- const timestamp = Date.now().toString(36);
712
- const id = `${timestamp}-${random}`;
713
- return prefix ? `${prefix}-${id}` : id;
714
- }
715
-
716
- /**
717
- * Check if the current context is part of a distributed workflow
718
- *
719
- * @param ctx - The trace context
720
- * @returns True if workflow baggage is present
721
- */
722
- export function isInDistributedWorkflow(ctx: TraceContext): boolean {
723
- const baggage = WorkflowBaggage.get(ctx);
724
- return !!(baggage.workflowId && baggage.workflowName);
725
- }
726
-
727
- /**
728
- * Get workflow progress information
729
- *
730
- * @param ctx - The trace context
731
- * @returns Progress info or null if not in a workflow
732
- */
733
- export function getWorkflowProgress(ctx: TraceContext): {
734
- workflowId: string;
735
- workflowName: string;
736
- currentStep: string | null;
737
- currentStepIndex: number | null;
738
- totalSteps: number | null;
739
- percentComplete: number | null;
740
- } | null {
741
- const baggage = WorkflowBaggage.get(ctx);
742
- if (!baggage.workflowId || !baggage.workflowName) {
743
- return null;
744
- }
745
-
746
- const percentComplete =
747
- baggage.totalSteps && baggage.stepIndex !== undefined
748
- ? Math.round(((baggage.stepIndex + 1) / baggage.totalSteps) * 100)
749
- : null;
750
-
751
- return {
752
- workflowId: baggage.workflowId,
753
- workflowName: baggage.workflowName,
754
- currentStep: baggage.stepName ?? null,
755
- currentStepIndex: baggage.stepIndex ?? null,
756
- totalSteps: baggage.totalSteps ?? null,
757
- percentComplete,
758
- };
759
- }
760
-
761
- /**
762
- * Create workflow correlation headers for manual propagation
763
- *
764
- * Use when you need to manually add workflow context to outgoing requests.
765
- *
766
- * @param values - Workflow baggage values
767
- * @returns Headers object with workflow baggage
768
- *
769
- * @example
770
- * ```typescript
771
- * const headers = createWorkflowHeaders({
772
- * workflowId: 'order-123',
773
- * workflowName: 'OrderFulfillment',
774
- * stepIndex: 2,
775
- * });
776
- *
777
- * await fetch('/api/inventory', { headers });
778
- * ```
779
- */
780
- export function createWorkflowHeaders(
781
- values: Partial<WorkflowBaggageValues>,
782
- ): Record<string, string> {
783
- const headers: Record<string, string> = {};
784
-
785
- // Build baggage string
786
- const baggageEntries: string[] = [];
787
-
788
- if (values.workflowId) {
789
- baggageEntries.push(
790
- `workflow.workflowId=${encodeURIComponent(values.workflowId)}`,
791
- );
792
- }
793
- if (values.workflowName) {
794
- baggageEntries.push(
795
- `workflow.workflowName=${encodeURIComponent(values.workflowName)}`,
796
- );
797
- }
798
- if (values.workflowVersion) {
799
- baggageEntries.push(
800
- `workflow.workflowVersion=${encodeURIComponent(values.workflowVersion)}`,
801
- );
802
- }
803
- if (values.stepName) {
804
- baggageEntries.push(
805
- `workflow.stepName=${encodeURIComponent(values.stepName)}`,
806
- );
807
- }
808
- if (values.stepIndex !== undefined) {
809
- baggageEntries.push(`workflow.stepIndex=${values.stepIndex}`);
810
- }
811
- if (values.totalSteps !== undefined) {
812
- baggageEntries.push(`workflow.totalSteps=${values.totalSteps}`);
813
- }
814
- if (values.priority) {
815
- baggageEntries.push(`workflow.priority=${values.priority}`);
816
- }
817
- if (values.correlationId) {
818
- baggageEntries.push(
819
- `workflow.correlationId=${encodeURIComponent(values.correlationId)}`,
820
- );
821
- }
822
- if (values.parentWorkflowId) {
823
- baggageEntries.push(
824
- `workflow.parentWorkflowId=${encodeURIComponent(values.parentWorkflowId)}`,
825
- );
826
- }
827
- if (values.initiatedBy) {
828
- baggageEntries.push(
829
- `workflow.initiatedBy=${encodeURIComponent(values.initiatedBy)}`,
830
- );
831
- }
832
- if (values.startedAt) {
833
- baggageEntries.push(
834
- `workflow.startedAt=${encodeURIComponent(values.startedAt)}`,
835
- );
836
- }
837
-
838
- if (baggageEntries.length > 0) {
839
- headers['baggage'] = baggageEntries.join(',');
840
- }
841
-
842
- return headers;
843
- }
844
-
845
- /**
846
- * Parse workflow context from baggage header
847
- *
848
- * @param baggageHeader - The baggage header value
849
- * @returns Parsed workflow values or null
850
- */
851
- export function parseWorkflowFromBaggage(
852
- baggageHeader: string,
853
- ): Partial<WorkflowBaggageValues> | null {
854
- if (!baggageHeader) {
855
- return null;
856
- }
857
-
858
- const values: Partial<WorkflowBaggageValues> = {};
859
- const entries = baggageHeader.split(',');
860
-
861
- for (const entry of entries) {
862
- const [key, value] = entry.trim().split('=');
863
- if (!key || !value) continue;
864
-
865
- const decodedValue = decodeURIComponent(value);
866
-
867
- switch (key) {
868
- case 'workflow.workflowId': {
869
- values.workflowId = decodedValue;
870
- break;
871
- }
872
- case 'workflow.workflowName': {
873
- values.workflowName = decodedValue;
874
- break;
875
- }
876
- case 'workflow.workflowVersion': {
877
- values.workflowVersion = decodedValue;
878
- break;
879
- }
880
- case 'workflow.stepName': {
881
- values.stepName = decodedValue;
882
- break;
883
- }
884
- case 'workflow.stepIndex': {
885
- values.stepIndex = Number.parseInt(decodedValue, 10);
886
- break;
887
- }
888
- case 'workflow.totalSteps': {
889
- values.totalSteps = Number.parseInt(decodedValue, 10);
890
- break;
891
- }
892
- case 'workflow.priority': {
893
- values.priority = decodedValue as WorkflowBaggageValues['priority'];
894
- break;
895
- }
896
- case 'workflow.correlationId': {
897
- values.correlationId = decodedValue;
898
- break;
899
- }
900
- case 'workflow.parentWorkflowId': {
901
- values.parentWorkflowId = decodedValue;
902
- break;
903
- }
904
- case 'workflow.initiatedBy': {
905
- values.initiatedBy = decodedValue;
906
- break;
907
- }
908
- case 'workflow.startedAt': {
909
- values.startedAt = decodedValue;
910
- break;
911
- }
912
- }
913
- }
914
-
915
- return Object.keys(values).length > 0 ? values : null;
916
- }