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/functional.ts DELETED
@@ -1,2539 +0,0 @@
1
- /**
2
- * Functional API for non-class code
3
- *
4
- * Three approaches for different use cases:
5
- * 1. trace() - Zero-ceremony HOF for single functions
6
- * 2. withTracing() - Middleware-style composable wrapper
7
- * 3. instrument() - Batch auto-instrumentation for modules
8
- *
9
- * @example trace() - Single function
10
- * ```typescript
11
- * export const createUser = trace(ctx => async (data) => {
12
- * ctx.setAttribute('user.id', data.id)
13
- * return await db.users.create(data)
14
- * })
15
- * ```
16
- *
17
- * @example withTracing() - Composable middleware
18
- * ```typescript
19
- * export const createUser = withTracing({
20
- * name: 'user.create'
21
- * })(ctx => async (data) => {
22
- * ctx.setAttribute('user.id', data.id)
23
- * return await db.users.create(data)
24
- * })
25
- * ```
26
- *
27
- * @example instrument() - Batch instrumentation
28
- * ```typescript
29
- * export default instrument({
30
- * createUser: async (data) => { },
31
- * updateUser: async (id, data) => { }
32
- * }, { serviceName: 'user' })
33
- * ```
34
- */
35
-
36
- import {
37
- SpanStatusCode,
38
- trace as otelTrace,
39
- context,
40
- propagation,
41
- type Span,
42
- } from '@opentelemetry/api';
43
- import { getConfig } from './config';
44
- import { getConfig as getInitConfig, getSdk } from './init';
45
- import {
46
- type Sampler,
47
- type SamplingContext,
48
- AlwaysSampler,
49
- AUTOTEL_SAMPLING_TAIL_KEEP,
50
- AUTOTEL_SAMPLING_TAIL_EVALUATED,
51
- } from './sampling';
52
- import { getEventQueue } from './track';
53
- import type { TraceContext } from './trace-context';
54
- import {
55
- createTraceContext,
56
- enterOrRun,
57
- getActiveContextWithBaggage,
58
- getContextStorage,
59
- } from './trace-context';
60
- import { setSpanName } from './trace-helpers';
61
- import { runInOperationContext } from './operation-context';
62
- import { inferVariableNameFromCallStack } from './variable-name-inference';
63
-
64
- /**
65
- * Complete trace context containing trace identifiers and span methods
66
- *
67
- * The ctx parameter in trace() functions provides:
68
- * - traceId, spanId, correlationId from the active span
69
- * - Span manipulation methods (setAttribute, setAttributes, setStatus, recordException)
70
- *
71
- * For custom context, access it directly in your functions (standard OpenTelemetry pattern).
72
- *
73
- * @example
74
- * ```typescript
75
- * import { trace } from 'autotel'
76
- *
77
- * export const createUser = trace(ctx => async (data: CreateUserData) => {
78
- * // Get custom context directly (standard OTel approach)
79
- * const userId = getCurrentUserId()
80
- * const tenantId = getCurrentTenant()
81
- *
82
- * // Use ctx for span operations and trace IDs
83
- * ctx.setAttribute('user.id', data.id)
84
- * ctx.setAttribute('user.tenant', tenantId)
85
- * console.log(ctx.traceId) // Trace IDs available
86
- * })
87
- * ```
88
- */
89
- export type { TraceContext } from './trace-context';
90
-
91
- /**
92
- * Helper type to extract function signature from factory pattern
93
- * This helps TypeScript infer types correctly for factory functions
94
- */
95
- type ExtractFunctionSignature<T> = T extends (ctx: TraceContext) => infer F
96
- ? F extends (...args: infer Args) => infer Return
97
- ? (...args: Args) => Return
98
- : never
99
- : never;
100
-
101
- /**
102
- * Helper type to exclude functions that return functions from immediate execution overloads
103
- */
104
- type ExcludeFactoryReturn<T> = T extends (ctx: TraceContext) => infer F
105
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
- F extends (...args: any[]) => any
107
- ? never
108
- : T
109
- : T;
110
-
111
- type GenericFunction = (...args: unknown[]) => unknown;
112
-
113
- const FACTORY_NAME_HINTS = new Set([
114
- 'ctx',
115
- '_ctx',
116
- 'context',
117
- 'tracecontext',
118
- 'tracectx',
119
- ]);
120
- const TRACE_FACTORY_SET = new WeakSet<object>();
121
-
122
- const SINGLE_LINE_COMMENT_REGEX = /\/\/.*$/gm;
123
- const MULTI_LINE_COMMENT_REGEX = /\/\*[\s\S]*?\*\//gm;
124
- const PARAM_TOKEN_SANITIZE_REGEX = new RegExp(String.raw`[{}\[\]\s]`, 'g');
125
-
126
- function markAsTraceFactory(fn: object): void {
127
- TRACE_FACTORY_SET.add(fn);
128
- }
129
-
130
- function hasFactoryMark(fn: object): boolean {
131
- return TRACE_FACTORY_SET.has(fn);
132
- }
133
-
134
- function sanitizeParameterToken(token: string): string {
135
- const [firstToken] = token.split('=');
136
- return (firstToken ?? '').replaceAll(PARAM_TOKEN_SANITIZE_REGEX, '').trim();
137
- }
138
-
139
- function getFirstParameterToken(fn: GenericFunction): string | null {
140
- let source = Function.prototype.toString.call(fn);
141
- source = source
142
- .replaceAll(MULTI_LINE_COMMENT_REGEX, '')
143
- .replaceAll(SINGLE_LINE_COMMENT_REGEX, '')
144
- .trim();
145
-
146
- // Arrow functions
147
- const arrowMatch = source.match(
148
- /^(?:async\s*)?(?:\(([^)]*)\)|([^=()]+))\s*=>/,
149
- );
150
- if (arrowMatch) {
151
- const params = (arrowMatch[1] ?? arrowMatch[2] ?? '').split(',');
152
- const first = params[0]?.trim();
153
- if (first) {
154
- return sanitizeParameterToken(first);
155
- }
156
- return null;
157
- }
158
-
159
- // Function declarations/expressions
160
- const functionMatch = source.match(/^[^(]*\(([^)]*)\)/);
161
- if (functionMatch) {
162
- const params = functionMatch[1]?.split(',');
163
- const first = params?.[0]?.trim();
164
- if (first) {
165
- return sanitizeParameterToken(first);
166
- }
167
- }
168
-
169
- return null;
170
- }
171
-
172
- /**
173
- * Symbol that explicitly marks a function as immediate-execution-with-ctx
174
- * (`(ctx) => result`), bypassing parameter-name introspection. Library
175
- * authors who wrap user handlers — like `autotel-aws/lambda`'s `wrapHandler`
176
- * — should mark their inner trace function with this so dispatch survives
177
- * downstream bundlers that minify parameter names.
178
- */
179
- const IMMEDIATE_EXECUTION_SYMBOL = Symbol.for('autotel.immediate-execution');
180
-
181
- type ImmediateExecutionFlag = {
182
- [IMMEDIATE_EXECUTION_SYMBOL]?: true;
183
- };
184
-
185
- function hasImmediateExecutionMark(fn: unknown): boolean {
186
- return (
187
- typeof fn === 'function' &&
188
- (fn as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] === true
189
- );
190
- }
191
-
192
- /**
193
- * Mark a function as immediate-execution-with-ctx so `trace(name, fn)`
194
- * dispatch doesn't depend on the first parameter being named `ctx`.
195
- *
196
- * Necessary when the function will be bundled by a minifier (esbuild,
197
- * terser, etc.) that renames identifiers. The name-allowlist heuristic in
198
- * `looksLikeTraceFactory` cannot recover from that; the marker can.
199
- *
200
- * @example
201
- * ```ts
202
- * import { markAsImmediate, trace } from 'autotel';
203
- *
204
- * const inner = markAsImmediate(async (ctx) => {
205
- * ctx.setAttribute('user.id', '123');
206
- * return { ok: true };
207
- * });
208
- * const result = await trace('user.read', inner);
209
- * ```
210
- */
211
- export function markAsImmediate<F>(fn: F): F {
212
- if (typeof fn === 'function') {
213
- (fn as unknown as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] =
214
- true;
215
- }
216
- return fn;
217
- }
218
-
219
- function looksLikeTraceFactory(fn: GenericFunction): boolean {
220
- if (hasFactoryMark(fn)) {
221
- return true;
222
- }
223
- if (hasImmediateExecutionMark(fn)) {
224
- return true;
225
- }
226
-
227
- if (fn.length === 0) {
228
- if (!isAsyncFunction(fn)) {
229
- try {
230
- const result = fn();
231
- return typeof result === 'function';
232
- } catch {
233
- return false;
234
- }
235
- }
236
- return false;
237
- }
238
-
239
- const firstParam = getFirstParameterToken(fn);
240
- if (!firstParam) {
241
- return false;
242
- }
243
-
244
- const normalized = firstParam.toLowerCase();
245
- if (
246
- FACTORY_NAME_HINTS.has(normalized) ||
247
- normalized.startsWith('ctx') ||
248
- normalized.startsWith('_ctx') ||
249
- normalized.startsWith('trace') ||
250
- normalized.endsWith('ctx') || // Match baseCtx, spanCtx, etc.
251
- normalized.includes('context') // Match traceContext, spanContext, etc.
252
- ) {
253
- return true;
254
- }
255
-
256
- return false;
257
- }
258
-
259
- /**
260
- * Check if a function that takes ctx returns another function (factory pattern)
261
- * vs returning a value directly (immediate execution pattern)
262
- *
263
- * IMPORTANT: For async functions, we skip probing entirely and assume immediate execution.
264
- * This is because:
265
- * - Factory pattern: `(ctx) => async (...args) => result` - outer function is SYNC
266
- * - Immediate execution: `async (ctx) => result` - function itself is ASYNC
267
- *
268
- * Probing async functions by executing them causes side effects (like creating orphan spans)
269
- * because the async function starts executing synchronously until the first await.
270
- */
271
- function isFactoryReturningFunction(
272
- fnWithCtx: (ctx: TraceContext) => unknown,
273
- ): boolean {
274
- // Async functions with ctx parameter are always immediate execution
275
- // because factory patterns have a sync outer function that returns the async inner
276
- if (isAsyncFunction(fnWithCtx)) {
277
- return false;
278
- }
279
-
280
- try {
281
- const result = fnWithCtx(createDummyCtx());
282
- return typeof result === 'function';
283
- } catch {
284
- // If the function throws when called with dummy ctx, assume it's immediate execution
285
- // since factory functions typically just return a function and don't execute logic
286
- return false;
287
- }
288
- }
289
-
290
- function isTraceFactoryFunction<TArgs extends unknown[], TReturn>(
291
- fn:
292
- | ((...args: TArgs) => TReturn)
293
- | ((ctx: TraceContext) => (...args: TArgs) => TReturn),
294
- ): fn is (ctx: TraceContext) => (...args: TArgs) => TReturn {
295
- if (typeof fn !== 'function') {
296
- return false;
297
- }
298
-
299
- if (hasFactoryMark(fn)) {
300
- return true;
301
- }
302
-
303
- if (looksLikeTraceFactory(fn as GenericFunction)) {
304
- markAsTraceFactory(fn);
305
- return true;
306
- }
307
-
308
- return false;
309
- }
310
-
311
- function ensureTraceFactory<TArgs extends unknown[], TReturn>(
312
- fnOrFactory:
313
- | ((...args: TArgs) => TReturn | Promise<TReturn>)
314
- | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),
315
- ): (ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn> {
316
- if (isTraceFactoryFunction(fnOrFactory)) {
317
- return fnOrFactory;
318
- }
319
-
320
- const plainFn = fnOrFactory as (...args: TArgs) => TReturn | Promise<TReturn>;
321
- const factory = (ctx: TraceContext) => {
322
- void ctx;
323
- return plainFn;
324
- };
325
- markAsTraceFactory(factory);
326
- return factory;
327
- }
328
-
329
- type WrappedFunction<TArgs extends unknown[], TReturn> = (
330
- ...args: TArgs
331
- ) => TReturn | Promise<TReturn>;
332
-
333
- function wrapFactoryWithTracing<TArgs extends unknown[], TReturn>(
334
- fnOrFactory:
335
- | ((...args: TArgs) => TReturn | Promise<TReturn>)
336
- | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),
337
- options: TracingOptions<TArgs, TReturn>,
338
- variableName?: string,
339
- ): WrappedFunction<TArgs, TReturn> {
340
- const factory = ensureTraceFactory(fnOrFactory);
341
-
342
- // Get the inner function (the actual function being traced)
343
- const sampleFn = factory(createDummyCtx());
344
-
345
- // Infer function name with priority:
346
- // 1. Explicit variable name (from instrument() or explicit name parameter)
347
- // 2. Inner function name (named function expressions - e.g., "async function createUser")
348
- // 3. Variable name from call stack (inferred from const assignment, for arrow functions)
349
- // 4. Factory function name (for cases where factory itself is named)
350
- const innerFunctionName = inferFunctionName(
351
- sampleFn as InstrumentableFunction,
352
- );
353
- const callStackVariableName = innerFunctionName
354
- ? undefined
355
- : inferVariableNameFromCallStack(); // Only infer from call stack if no inner function name
356
- const factoryName = inferFunctionName(factory as InstrumentableFunction);
357
- const effectiveVariableName =
358
- variableName || innerFunctionName || callStackVariableName || factoryName;
359
-
360
- const useAsyncWrapper = isAsyncFunction(sampleFn);
361
-
362
- if (useAsyncWrapper) {
363
- return wrapWithTracing(
364
- factory as (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
365
- options,
366
- effectiveVariableName,
367
- ) as WrappedFunction<TArgs, TReturn>;
368
- }
369
-
370
- return wrapWithTracingSync(
371
- factory as (ctx: TraceContext) => (...args: TArgs) => TReturn,
372
- options,
373
- effectiveVariableName,
374
- ) as WrappedFunction<TArgs, TReturn>;
375
- }
376
-
377
- /**
378
- * Common options for functional tracing
379
- */
380
- export interface TracingOptions<
381
- TArgs extends unknown[] = unknown[],
382
- TReturn = unknown,
383
- > {
384
- /**
385
- * Span name (highest priority)
386
- * If provided, this is used as the span name
387
- */
388
- name?: string;
389
-
390
- /**
391
- * Service name (used to compose final span name)
392
- * If name not provided, span name becomes: ${serviceName}.${functionName}
393
- */
394
- serviceName?: string;
395
-
396
- /**
397
- * Sampling strategy
398
- * @default AlwaysSampler
399
- */
400
- sampler?: Sampler;
401
-
402
- /**
403
- * Enable metrics collection (counter, histogram)
404
- * @default false
405
- */
406
- withMetrics?: boolean;
407
-
408
- /**
409
- * Extract attributes from function arguments
410
- */
411
- attributesFromArgs?: (args: TArgs) => Record<string, unknown>;
412
-
413
- /**
414
- * Extract attributes from function result
415
- */
416
- attributesFromResult?: (result: TReturn) => Record<string, unknown>;
417
-
418
- /**
419
- * Capture the function arguments onto the span as `autotel.input`
420
- * (JSON, truncated). One arg is captured directly; multiple are captured as
421
- * an array. Off by default — opt in per call. Tools (visualizers, devtools)
422
- * read this alongside `ai.toolCall.args` to show function I/O uniformly.
423
- * Avoid on args with secrets/PII, or pair with a redacting processor.
424
- */
425
- captureInput?: boolean;
426
-
427
- /**
428
- * Capture the function return value onto the span as `autotel.output`
429
- * (JSON, truncated). Off by default. Same caveats as {@link captureInput}.
430
- */
431
- captureOutput?: boolean;
432
-
433
- /**
434
- * Start a new root span instead of creating a child
435
- * Useful for serverless entry points
436
- * @default false
437
- */
438
- startNewRoot?: boolean;
439
-
440
- /**
441
- * Flush events queue when span ends
442
- * Only flushes on root spans (to avoid excessive flushing)
443
- * @default true
444
- */
445
- flushOnRootSpanEnd?: boolean;
446
-
447
- /**
448
- * Span kind for semantic convention compliance
449
- * Used for messaging (PRODUCER/CONSUMER), HTTP (CLIENT/SERVER), etc.
450
- * @default SpanKind.INTERNAL
451
- */
452
- spanKind?: import('@opentelemetry/api').SpanKind;
453
- }
454
-
455
- /**
456
- * Options for instrument() batch instrumentation
457
- */
458
- export interface InstrumentOptions<
459
- T extends Record<string, InstrumentableFunction> = Record<
460
- string,
461
- InstrumentableFunction
462
- >,
463
- > extends TracingOptions {
464
- /** Functions to instrument */
465
- functions: T;
466
- /**
467
- * Per-function configuration overrides
468
- */
469
- overrides?: Record<string, Partial<TracingOptions>>;
470
-
471
- /**
472
- * Functions to skip (won't be instrumented)
473
- * Supports:
474
- * - String keys: 'functionName'
475
- * - RegExp: /^_internal/
476
- * - Predicate: (key, fn) => boolean
477
- *
478
- * By default, functions starting with _ are skipped
479
- */
480
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
481
- skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[];
482
- }
483
-
484
- // Maximum error message length to prevent span bloat
485
- const MAX_ERROR_MESSAGE_LENGTH = 500;
486
-
487
- function createDummyCtx<
488
- TBaggage extends Record<string, unknown> | undefined = undefined,
489
- >(): TraceContext<TBaggage> {
490
- // `recordException` / `addEvent` are no-op shims kept for the same
491
- // compatibility window as `createTraceContext` (see trace-context.ts).
492
- return {
493
- traceId: '',
494
- spanId: '',
495
- correlationId: '',
496
- setAttribute: () => {},
497
- setAttributes: () => {},
498
- setStatus: () => {},
499
- recordException: () => {},
500
- addEvent: () => {},
501
- addLink: () => {},
502
- addLinks: () => {},
503
- updateName: () => {},
504
- isRecording: () => false,
505
- getBaggage: () => {},
506
- setBaggage: () => '',
507
- deleteBaggage: () => {},
508
- getAllBaggage: () => new Map(),
509
- } as unknown as TraceContext<TBaggage>;
510
- }
511
-
512
- /** Attribute keys for opt-in function I/O capture (see TracingOptions). */
513
- const AUTOTEL_INPUT_ATTR = 'autotel.input';
514
- const AUTOTEL_OUTPUT_ATTR = 'autotel.output';
515
- const CAPTURE_MAX_CHARS = 4096;
516
-
517
- /** JSON-serialize a captured value, defensively (truncate, swallow cycles). */
518
- function serializeCapture(value: unknown): string | undefined {
519
- if (value === undefined) return undefined;
520
- try {
521
- const json = typeof value === 'string' ? value : JSON.stringify(value);
522
- if (json === undefined) return undefined;
523
- return json.length > CAPTURE_MAX_CHARS
524
- ? `${json.slice(0, CAPTURE_MAX_CHARS)}…[truncated]`
525
- : json;
526
- } catch {
527
- return undefined;
528
- }
529
- }
530
-
531
- /** `autotel.input` from args (single arg captured directly, else the array). */
532
- function captureInputAttrs(
533
- args: unknown[],
534
- enabled?: boolean,
535
- ): Record<string, unknown> {
536
- if (!enabled) return {};
537
- const s = serializeCapture(args.length === 1 ? args[0] : args);
538
- return s === undefined ? {} : { [AUTOTEL_INPUT_ATTR]: s };
539
- }
540
-
541
- /** `autotel.output` from the return value. */
542
- function captureOutputAttrs(
543
- result: unknown,
544
- enabled?: boolean,
545
- ): Record<string, unknown> {
546
- if (!enabled) return {};
547
- const s = serializeCapture(result);
548
- return s === undefined ? {} : { [AUTOTEL_OUTPUT_ATTR]: s };
549
- }
550
-
551
- function isAsyncFunction(fn: unknown): boolean {
552
- return typeof fn === 'function' && fn.constructor?.name === 'AsyncFunction';
553
- }
554
-
555
- // Symbol to prevent double-instrumentation (idempotency flag)
556
- const INSTRUMENTED_SYMBOL = Symbol.for('autotel.functional.instrumented');
557
-
558
- type InstrumentedFlag = {
559
- [INSTRUMENTED_SYMBOL]?: true;
560
- };
561
-
562
- function hasInstrumentationFlag(value: unknown): value is InstrumentedFlag {
563
- return (
564
- (typeof value === 'function' || typeof value === 'object') &&
565
- value !== null &&
566
- Boolean((value as InstrumentedFlag)[INSTRUMENTED_SYMBOL])
567
- );
568
- }
569
-
570
- /**
571
- * Truncate error message to prevent span bloat
572
- */
573
- function truncateErrorMessage(message: string): string {
574
- if (message.length <= MAX_ERROR_MESSAGE_LENGTH) {
575
- return message;
576
- }
577
- return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}... (truncated)`;
578
- }
579
-
580
- type InstrumentableFunction<
581
- TArgs extends unknown[] = unknown[],
582
- TReturn = unknown,
583
- > = ((...args: TArgs) => TReturn | Promise<TReturn>) & {
584
- displayName?: string;
585
- name?: string;
586
- };
587
-
588
- /**
589
- * Try to infer function name from function properties
590
- * Checks for displayName, name, or other metadata that might be set
591
- */
592
- function inferFunctionName<
593
- TArgs extends unknown[] = unknown[],
594
- TReturn = unknown,
595
- >(fn: InstrumentableFunction<TArgs, TReturn>): string | undefined {
596
- // Check for displayName property (sometimes set by bundlers)
597
- const displayName = (fn as { displayName?: string }).displayName;
598
- if (displayName) {
599
- return displayName;
600
- }
601
-
602
- // Check function.name (works for named functions and modern arrow function assignment)
603
- // Note: Empty string is falsy, so this handles both undefined and ''
604
- if (fn.name && fn.name !== 'anonymous' && fn.name !== '') {
605
- return fn.name;
606
- }
607
-
608
- // Try to extract name from function source (for function declarations)
609
- const source = Function.prototype.toString.call(fn);
610
- const match = source.match(/function\s+([^(\s]+)/);
611
- if (match && match[1] && match[1] !== 'anonymous') {
612
- return match[1];
613
- }
614
-
615
- return undefined;
616
- }
617
-
618
- /**
619
- * Determine span name using priority:
620
- * 1. Explicit name option
621
- * 2. serviceName + functionName
622
- * 3. Inferred from function/variable name (including stack trace fallback)
623
- * 4. Fallback to 'unknown'
624
- */
625
- function getSpanName<TArgs extends unknown[], TReturn>(
626
- options: TracingOptions<TArgs, TReturn>,
627
- fn: InstrumentableFunction<TArgs, TReturn>,
628
- variableName?: string,
629
- ): string {
630
- // 1. Explicit name
631
- if (options.name) {
632
- return options.name;
633
- }
634
-
635
- // 2. Try variable name, function name, or function properties
636
- let fnName = variableName || inferFunctionName(fn);
637
-
638
- // Default to 'anonymous' if still no name
639
- fnName = fnName || 'anonymous';
640
-
641
- // 2. serviceName + functionName
642
- if (options.serviceName) {
643
- return `${options.serviceName}.${fnName}`;
644
- }
645
-
646
- // 3. Inferred from function name
647
- if (fnName && fnName !== 'anonymous') {
648
- return fnName;
649
- }
650
-
651
- // 4. Fallback
652
- return 'unknown';
653
- }
654
-
655
- /**
656
- * Check if function should be skipped
657
- */
658
- function shouldSkip(
659
- key: string,
660
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
661
- fn: Function,
662
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
663
- skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[],
664
- ): boolean {
665
- // Default: skip functions starting with _
666
- if (key.startsWith('_')) {
667
- return true;
668
- }
669
-
670
- if (!skip || skip.length === 0) {
671
- return false;
672
- }
673
-
674
- for (const rule of skip) {
675
- if (typeof rule === 'string' && key === rule) {
676
- return true;
677
- } else if (rule instanceof RegExp && rule.test(key)) {
678
- return true;
679
- } else if (typeof rule === 'function' && rule(key, fn)) {
680
- return true;
681
- }
682
- }
683
-
684
- return false;
685
- }
686
-
687
- /**
688
- * Get current trace context value (internal helper)
689
- *
690
- * Returns base context (trace IDs) + span methods from the active span.
691
- */
692
- function getCtxValue<
693
- TBaggage extends Record<string, unknown> | undefined = undefined,
694
- >(): TraceContext<TBaggage> | null {
695
- const activeSpan = otelTrace.getActiveSpan();
696
- if (!activeSpan) return null;
697
-
698
- // Use shared utility to create trace context
699
- return createTraceContext<TBaggage>(activeSpan);
700
- }
701
-
702
- /**
703
- * Context object that lazily evaluates the active span on property access
704
- *
705
- * Access trace context directly without function call syntax.
706
- *
707
- * @example
708
- * ```typescript
709
- * import { trace, ctx } from 'autotel'
710
- *
711
- * export const createUser = trace(async (data) => {
712
- * // Direct property access - no function call!
713
- * if (ctx.traceId) {
714
- * ctx.setAttribute('user.id', data.id)
715
- * console.log('Trace:', ctx.traceId)
716
- * }
717
- * })
718
- * ```
719
- */
720
- export const ctx = new Proxy(
721
- {},
722
- {
723
- get(_target, prop) {
724
- const ctxValue = getCtxValue();
725
- if (!ctxValue) {
726
- return;
727
- }
728
- return ctxValue[prop as keyof typeof ctxValue];
729
- },
730
-
731
- has(_target, prop) {
732
- const ctxValue = getCtxValue();
733
- if (!ctxValue) {
734
- return false;
735
- }
736
- return prop in ctxValue;
737
- },
738
-
739
- ownKeys() {
740
- const ctxValue = getCtxValue();
741
- if (!ctxValue) {
742
- return [];
743
- }
744
- return Object.keys(ctxValue);
745
- },
746
-
747
- getOwnPropertyDescriptor(_target, prop) {
748
- const ctxValue = getCtxValue();
749
- if (!ctxValue) {
750
- return;
751
- }
752
- return Object.getOwnPropertyDescriptor(ctxValue, prop);
753
- },
754
- },
755
- );
756
-
757
- /**
758
- * Core tracing wrapper for async functions (internal implementation)
759
- */
760
- function wrapWithTracing<TArgs extends unknown[], TReturn>(
761
- fnFactory: (
762
- ctx: TraceContext,
763
- ) => (...args: TArgs) => TReturn | Promise<TReturn>,
764
- options: TracingOptions<TArgs, TReturn>,
765
- variableName?: string,
766
- ): (...args: TArgs) => Promise<TReturn> {
767
- // Idempotency check: if already instrumented, return as-is
768
- if (hasInstrumentationFlag(fnFactory)) {
769
- // Already instrumented - proceed
770
- }
771
-
772
- const config = getConfig();
773
- const tracer = config.tracer;
774
- const meter = config.meter;
775
- const sampler = options.sampler || new AlwaysSampler();
776
-
777
- const tempFn = fnFactory(createDummyCtx());
778
- const spanName = getSpanName(options, tempFn, variableName);
779
-
780
- const callCounter = options.withMetrics
781
- ? meter.createCounter(`${spanName}.calls`, {
782
- description: `Call count for ${spanName}`,
783
- unit: '1',
784
- })
785
- : undefined;
786
-
787
- const durationHistogram = options.withMetrics
788
- ? meter.createHistogram(`${spanName}.duration`, {
789
- description: `Duration for ${spanName}`,
790
- unit: 'ms',
791
- })
792
- : undefined;
793
-
794
- const wrappedFunction = async function wrappedFunction(
795
- this: unknown,
796
- ...args: TArgs
797
- ): Promise<TReturn> {
798
- const samplingContext: SamplingContext = {
799
- operationName: spanName,
800
- args,
801
- metadata: {},
802
- };
803
-
804
- const shouldSample = sampler.shouldSample(samplingContext);
805
- const needsTailSampling =
806
- 'needsTailSampling' in sampler &&
807
- typeof sampler.needsTailSampling === 'function'
808
- ? sampler.needsTailSampling()
809
- : false;
810
-
811
- if (!shouldSample && !needsTailSampling) {
812
- const fn = fnFactory(createDummyCtx());
813
- return await fn.call(this, ...args);
814
- }
815
-
816
- const startTime = performance.now();
817
- const isRootSpan =
818
- options.startNewRoot || otelTrace.getActiveSpan() === undefined;
819
- const shouldAutoFlush =
820
- options.flushOnRootSpanEnd ?? getInitConfig()?.flushOnRootSpanEnd ?? true;
821
- const shouldAutoFlushSpans = getInitConfig()?.forceFlushOnShutdown ?? false;
822
-
823
- const flushIfNeeded = async () => {
824
- if (!shouldAutoFlush || !isRootSpan) return;
825
-
826
- try {
827
- // Flush events queue
828
- const queue = getEventQueue();
829
- if (queue && queue.size() > 0) {
830
- await queue.flush();
831
- }
832
-
833
- // Flush OpenTelemetry spans if enabled
834
- if (shouldAutoFlushSpans) {
835
- const sdk = getSdk();
836
- if (sdk) {
837
- try {
838
- // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
839
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
840
- const sdkAny = sdk as any;
841
- if (typeof sdkAny.getTracerProvider === 'function') {
842
- const tracerProvider = sdkAny.getTracerProvider();
843
- if (
844
- tracerProvider &&
845
- typeof tracerProvider.forceFlush === 'function'
846
- ) {
847
- await tracerProvider.forceFlush();
848
- }
849
- }
850
- } catch {
851
- // Ignore errors when accessing tracer provider (may not be available in test mocks)
852
- }
853
- }
854
- }
855
- } catch (error) {
856
- const initConfig = getInitConfig();
857
- const logger = initConfig?.logger;
858
- if (logger?.error) {
859
- logger.error(
860
- {
861
- err: error instanceof Error ? error : undefined,
862
- },
863
- `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,
864
- );
865
- }
866
- }
867
- };
868
-
869
- // Build span options including root and kind
870
- const spanOptions: import('@opentelemetry/api').SpanOptions = {};
871
- if (options.startNewRoot) {
872
- spanOptions.root = true;
873
- }
874
- if (options.spanKind !== undefined) {
875
- spanOptions.kind = options.spanKind;
876
- }
877
-
878
- const parentContext = getActiveContextWithBaggage();
879
- return tracer.startActiveSpan(
880
- spanName,
881
- spanOptions,
882
- parentContext,
883
- async (span) => {
884
- // Run within operation context so events can auto-capture operation.name
885
- return runInOperationContext(spanName, async () => {
886
- let shouldKeepSpan = true;
887
-
888
- setSpanName(span, spanName);
889
-
890
- // Initialize context storage with the active context BEFORE creating trace context
891
- const initialContext = context.active();
892
- const contextStorage = getContextStorage();
893
- if (!contextStorage.getStore()) {
894
- enterOrRun(contextStorage, initialContext);
895
- }
896
-
897
- const ctxValue = createTraceContext(span);
898
- const fn = fnFactory(ctxValue);
899
- const argsAttributes = {
900
- ...captureInputAttrs(args, options.captureInput),
901
- ...(options.attributesFromArgs
902
- ? options.attributesFromArgs(args)
903
- : {}),
904
- };
905
-
906
- const handleTailSampling = (
907
- success: boolean,
908
- duration: number,
909
- error?: unknown,
910
- ) => {
911
- if (
912
- needsTailSampling &&
913
- 'shouldKeepTrace' in sampler &&
914
- typeof sampler.shouldKeepTrace === 'function'
915
- ) {
916
- shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {
917
- success,
918
- duration,
919
- error,
920
- });
921
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
922
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
923
- }
924
- };
925
-
926
- const onSuccess = async (result: TReturn) => {
927
- const duration = performance.now() - startTime;
928
-
929
- callCounter?.add(1, {
930
- operation: spanName,
931
- status: 'success',
932
- });
933
-
934
- durationHistogram?.record(duration, {
935
- operation: spanName,
936
- status: 'success',
937
- });
938
-
939
- const resultAttributes = {
940
- ...captureOutputAttrs(result, options.captureOutput),
941
- ...(options.attributesFromResult
942
- ? options.attributesFromResult(result)
943
- : {}),
944
- };
945
-
946
- span.setStatus({ code: SpanStatusCode.OK });
947
- span.setAttributes({
948
- ...argsAttributes,
949
- ...resultAttributes,
950
- 'operation.name': spanName,
951
- 'code.function': spanName,
952
- 'operation.duration': duration,
953
- 'operation.success': true,
954
- });
955
-
956
- handleTailSampling(true, duration);
957
-
958
- span.end();
959
- await flushIfNeeded();
960
- return result;
961
- };
962
-
963
- const onError = async (error: unknown): Promise<never> => {
964
- const duration = performance.now() - startTime;
965
-
966
- callCounter?.add(1, {
967
- operation: spanName,
968
- status: 'error',
969
- });
970
-
971
- durationHistogram?.record(duration, {
972
- operation: spanName,
973
- status: 'error',
974
- });
975
-
976
- const errorMessage =
977
- error instanceof Error ? error.message : 'Unknown error';
978
- const truncatedMessage = truncateErrorMessage(errorMessage);
979
-
980
- span.setStatus({
981
- code: SpanStatusCode.ERROR,
982
- message: truncatedMessage,
983
- });
984
-
985
- span.setAttributes({
986
- ...argsAttributes,
987
- 'operation.name': spanName,
988
- 'code.function': spanName,
989
- 'operation.duration': duration,
990
- 'operation.success': false,
991
- error: true,
992
- 'exception.type':
993
- error instanceof Error ? error.constructor.name : 'Error',
994
- 'exception.message': truncatedMessage,
995
- });
996
-
997
- if (error instanceof Error && error.stack) {
998
- span.setAttribute(
999
- 'exception.stack',
1000
- error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH),
1001
- );
1002
- }
1003
-
1004
- span.recordException(
1005
- error instanceof Error ? error : new Error(String(error)),
1006
- );
1007
-
1008
- handleTailSampling(false, duration, error);
1009
-
1010
- span.end();
1011
- await flushIfNeeded();
1012
- throw error;
1013
- };
1014
-
1015
- try {
1016
- callCounter?.add(1, {
1017
- operation: spanName,
1018
- status: 'started',
1019
- });
1020
-
1021
- // Execute the user's function with the updated context
1022
- // This ensures ctx.setBaggage() changes are visible to OpenTelemetry operations
1023
- // (like BaggageSpanProcessor, child spans, etc.)
1024
- // We use getActiveContextWithBaggage() which checks the stored context,
1025
- // so if baggage is set during execution, it will be picked up
1026
- const executeWithContext = async () => {
1027
- // Get the current context (may have been updated by ctx.setBaggage())
1028
- const currentContext = getActiveContextWithBaggage();
1029
- // Establish the context in OpenTelemetry's context manager
1030
- return context.with(currentContext, async () => {
1031
- return fn.call(this, ...args);
1032
- });
1033
- };
1034
- const result = await executeWithContext();
1035
-
1036
- return await onSuccess(result);
1037
- } catch (error) {
1038
- await onError(error);
1039
- throw error;
1040
- }
1041
- });
1042
- },
1043
- );
1044
- };
1045
-
1046
- // Mark as instrumented to prevent double-wrapping
1047
- (wrappedFunction as InstrumentedFlag)[INSTRUMENTED_SYMBOL] = true;
1048
-
1049
- Object.defineProperty(wrappedFunction, 'name', {
1050
- value: tempFn.name || 'trace',
1051
- configurable: true,
1052
- });
1053
-
1054
- return wrappedFunction;
1055
- }
1056
-
1057
- /**
1058
- * Core tracing wrapper for sync functions (internal implementation)
1059
- */
1060
- function wrapWithTracingSync<TArgs extends unknown[], TReturn>(
1061
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1062
- options: TracingOptions<TArgs, TReturn>,
1063
- variableName?: string,
1064
- ): (...args: TArgs) => TReturn {
1065
- // Idempotency check: if already instrumented, return as-is
1066
- if (hasInstrumentationFlag(fnFactory)) {
1067
- // If already instrumented, we need to extract the original factory
1068
- // For now, we'll just proceed - this edge case is handled by the wrapped function check
1069
- }
1070
-
1071
- const config = getConfig();
1072
- const tracer = config.tracer;
1073
- const meter = config.meter;
1074
- const sampler = options.sampler || new AlwaysSampler();
1075
-
1076
- // We need to get a reference function name for span naming
1077
- // Create a minimal dummy context just for extracting the function name
1078
- // This won't affect actual tracing - we use the real context inside the span
1079
- const tempFn = fnFactory(createDummyCtx());
1080
- const spanName = getSpanName(options, tempFn, variableName);
1081
-
1082
- // Metrics setup (if enabled)
1083
- const callCounter = options.withMetrics
1084
- ? meter.createCounter(`${spanName}.calls`, {
1085
- description: `Call count for ${spanName}`,
1086
- unit: '1',
1087
- })
1088
- : undefined;
1089
-
1090
- const durationHistogram = options.withMetrics
1091
- ? meter.createHistogram(`${spanName}.duration`, {
1092
- description: `Duration for ${spanName}`,
1093
- unit: 'ms',
1094
- })
1095
- : undefined;
1096
-
1097
- // Return wrapped function
1098
- function wrappedFunction(
1099
- this: unknown,
1100
- ...args: TArgs
1101
- ): TReturn | Promise<TReturn> {
1102
- const samplingContext: SamplingContext = {
1103
- operationName: spanName,
1104
- args,
1105
- metadata: {},
1106
- };
1107
-
1108
- const shouldSample = sampler.shouldSample(samplingContext);
1109
- const needsTailSampling =
1110
- 'needsTailSampling' in sampler &&
1111
- typeof sampler.needsTailSampling === 'function'
1112
- ? sampler.needsTailSampling()
1113
- : false;
1114
-
1115
- // If not sampling and no tail sampling, execute without tracing
1116
- if (!shouldSample && !needsTailSampling) {
1117
- const fn = fnFactory(createDummyCtx());
1118
- return fn.call(this, ...args);
1119
- }
1120
-
1121
- const startTime = performance.now();
1122
-
1123
- // Track if this is a root span for auto-flush
1124
- const isRootSpan =
1125
- options.startNewRoot || otelTrace.getActiveSpan() === undefined;
1126
- const shouldAutoFlush =
1127
- options.flushOnRootSpanEnd ?? getInitConfig()?.flushOnRootSpanEnd ?? true;
1128
- const shouldAutoFlushSpans = getInitConfig()?.forceFlushOnShutdown ?? false;
1129
-
1130
- // Note: This is intentionally fire-and-forget (void) for synchronous functions.
1131
- // Synchronous functions cannot await flush completion without blocking execution.
1132
- // The forceFlushOnShutdown guarantee only applies to async functions.
1133
- const flushIfNeeded = () => {
1134
- if (!shouldAutoFlush || !isRootSpan) return;
1135
-
1136
- // Flush events queue
1137
- const queue = getEventQueue();
1138
- if (queue && queue.size() > 0) {
1139
- void queue.flush().catch((error) => {
1140
- const initConfig = getInitConfig();
1141
- const logger = initConfig?.logger;
1142
- if (logger?.error) {
1143
- logger.error(
1144
- {
1145
- err: error instanceof Error ? error : undefined,
1146
- },
1147
- `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,
1148
- );
1149
- }
1150
- });
1151
- }
1152
-
1153
- // Flush OpenTelemetry spans if enabled
1154
- if (shouldAutoFlushSpans) {
1155
- const sdk = getSdk();
1156
- if (sdk) {
1157
- try {
1158
- // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
1159
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1160
- const sdkAny = sdk as any;
1161
- if (typeof sdkAny.getTracerProvider === 'function') {
1162
- const tracerProvider = sdkAny.getTracerProvider();
1163
- if (
1164
- tracerProvider &&
1165
- typeof tracerProvider.forceFlush === 'function'
1166
- ) {
1167
- void tracerProvider.forceFlush().catch((error: unknown) => {
1168
- const initConfig = getInitConfig();
1169
- const logger = initConfig?.logger;
1170
- if (logger?.error) {
1171
- logger.error(
1172
- {
1173
- err: error instanceof Error ? error : undefined,
1174
- },
1175
- `[autotel] Span flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,
1176
- );
1177
- }
1178
- });
1179
- }
1180
- }
1181
- } catch {
1182
- // Ignore errors when accessing tracer provider (may not be available in test mocks)
1183
- }
1184
- }
1185
- }
1186
- };
1187
-
1188
- // Build span options including root and kind
1189
- const spanOptions: import('@opentelemetry/api').SpanOptions = {};
1190
- if (options.startNewRoot) {
1191
- spanOptions.root = true;
1192
- }
1193
- if (options.spanKind !== undefined) {
1194
- spanOptions.kind = options.spanKind;
1195
- }
1196
-
1197
- const parentContext = getActiveContextWithBaggage();
1198
- return tracer.startActiveSpan(
1199
- spanName,
1200
- spanOptions,
1201
- parentContext,
1202
- (span) => {
1203
- // Run within operation context so events can auto-capture operation.name
1204
- return runInOperationContext(spanName, () => {
1205
- let shouldKeepSpan = true;
1206
-
1207
- // Store span name for trace context helpers
1208
- setSpanName(span, spanName);
1209
-
1210
- // Create trace context for this span using shared utility
1211
- const ctxValue = createTraceContext(span);
1212
-
1213
- // Get the actual function from the factory
1214
- const fn = fnFactory(ctxValue);
1215
-
1216
- // Extract attributes only when actually tracing
1217
- // This avoids expensive preprocessing when sampling rejects the trace
1218
- const argsAttributes = {
1219
- ...captureInputAttrs(args, options.captureInput),
1220
- ...(options.attributesFromArgs
1221
- ? options.attributesFromArgs(args)
1222
- : {}),
1223
- };
1224
-
1225
- const handleTailSampling = (
1226
- success: boolean,
1227
- duration: number,
1228
- error?: unknown,
1229
- ) => {
1230
- if (
1231
- needsTailSampling &&
1232
- 'shouldKeepTrace' in sampler &&
1233
- typeof sampler.shouldKeepTrace === 'function'
1234
- ) {
1235
- shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {
1236
- success,
1237
- duration,
1238
- error,
1239
- });
1240
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
1241
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
1242
- }
1243
- };
1244
-
1245
- const onSuccess = (result: TReturn) => {
1246
- const duration = performance.now() - startTime;
1247
-
1248
- callCounter?.add(1, {
1249
- operation: spanName,
1250
- status: 'success',
1251
- });
1252
-
1253
- durationHistogram?.record(duration, {
1254
- operation: spanName,
1255
- status: 'success',
1256
- });
1257
-
1258
- const resultAttributes = {
1259
- ...captureOutputAttrs(result, options.captureOutput),
1260
- ...(options.attributesFromResult
1261
- ? options.attributesFromResult(result)
1262
- : {}),
1263
- };
1264
-
1265
- span.setStatus({ code: SpanStatusCode.OK });
1266
- span.setAttributes({
1267
- ...argsAttributes,
1268
- ...resultAttributes,
1269
- 'operation.name': spanName,
1270
- 'code.function': spanName,
1271
- 'operation.duration': duration,
1272
- 'operation.success': true,
1273
- });
1274
-
1275
- handleTailSampling(true, duration);
1276
-
1277
- span.end();
1278
- void flushIfNeeded();
1279
- return result;
1280
- };
1281
-
1282
- const onError = (error: unknown): never => {
1283
- const duration = performance.now() - startTime;
1284
-
1285
- callCounter?.add(1, {
1286
- operation: spanName,
1287
- status: 'error',
1288
- });
1289
-
1290
- durationHistogram?.record(duration, {
1291
- operation: spanName,
1292
- status: 'error',
1293
- });
1294
-
1295
- const errorMessage =
1296
- error instanceof Error ? error.message : 'Unknown error';
1297
- const truncatedMessage = truncateErrorMessage(errorMessage);
1298
-
1299
- span.setStatus({
1300
- code: SpanStatusCode.ERROR,
1301
- message: truncatedMessage,
1302
- });
1303
-
1304
- span.setAttributes({
1305
- ...argsAttributes,
1306
- 'operation.name': spanName,
1307
- 'code.function': spanName,
1308
- 'operation.duration': duration,
1309
- 'operation.success': false,
1310
- error: true,
1311
- 'exception.type':
1312
- error instanceof Error ? error.constructor.name : 'Error',
1313
- 'exception.message': truncatedMessage,
1314
- });
1315
-
1316
- span.recordException(
1317
- error instanceof Error ? error : new Error(String(error)),
1318
- );
1319
-
1320
- handleTailSampling(false, duration, error);
1321
-
1322
- span.end();
1323
- void flushIfNeeded();
1324
- throw error;
1325
- };
1326
-
1327
- try {
1328
- callCounter?.add(1, {
1329
- operation: spanName,
1330
- status: 'started',
1331
- });
1332
-
1333
- const result = fn.call(this, ...args);
1334
-
1335
- if (result instanceof Promise) {
1336
- return result.then(onSuccess, onError);
1337
- }
1338
-
1339
- return onSuccess(result);
1340
- } catch (error) {
1341
- return onError(error);
1342
- }
1343
- });
1344
- },
1345
- );
1346
- }
1347
-
1348
- // Mark as instrumented to prevent double-wrapping
1349
- (wrappedFunction as InstrumentedFlag)[INSTRUMENTED_SYMBOL] = true;
1350
-
1351
- // Preserve function name for better debugging
1352
- // Use the same tempFn we created earlier for span naming
1353
- Object.defineProperty(wrappedFunction, 'name', {
1354
- value: tempFn.name || 'trace',
1355
- configurable: true,
1356
- });
1357
-
1358
- return wrappedFunction as unknown as (...args: TArgs) => TReturn;
1359
- }
1360
-
1361
- /**
1362
- * Execute a function immediately within a trace span
1363
- * Used for the immediate execution pattern: trace((ctx) => result)
1364
- */
1365
- function executeImmediately<TReturn = unknown>(
1366
- fn: (ctx: TraceContext) => TReturn | Promise<TReturn>,
1367
- options: TracingOptions<unknown[], unknown>,
1368
- ): TReturn | Promise<TReturn> {
1369
- const config = getConfig();
1370
- const tracer = config.tracer;
1371
- const meter = config.meter;
1372
- const sampler = options.sampler || new AlwaysSampler();
1373
-
1374
- // Get span name from options or use 'anonymous'
1375
- const spanName = options.name || 'anonymous';
1376
-
1377
- const samplingContext: SamplingContext = {
1378
- operationName: spanName,
1379
- args: [],
1380
- metadata: {},
1381
- };
1382
-
1383
- const shouldSample = sampler.shouldSample(samplingContext);
1384
- const needsTailSampling =
1385
- 'needsTailSampling' in sampler &&
1386
- typeof sampler.needsTailSampling === 'function'
1387
- ? sampler.needsTailSampling()
1388
- : false;
1389
-
1390
- if (!shouldSample && !needsTailSampling) {
1391
- return fn(createDummyCtx());
1392
- }
1393
-
1394
- const startTime = performance.now();
1395
- const isRootSpan =
1396
- options.startNewRoot || otelTrace.getActiveSpan() === undefined;
1397
- const shouldAutoFlush =
1398
- options.flushOnRootSpanEnd ?? getInitConfig()?.flushOnRootSpanEnd ?? true;
1399
- const shouldAutoFlushSpans = getInitConfig()?.forceFlushOnShutdown ?? false;
1400
-
1401
- const callCounter = options.withMetrics
1402
- ? meter.createCounter(`${spanName}.calls`, {
1403
- description: `Call count for ${spanName}`,
1404
- unit: '1',
1405
- })
1406
- : undefined;
1407
-
1408
- const durationHistogram = options.withMetrics
1409
- ? meter.createHistogram(`${spanName}.duration`, {
1410
- description: `Duration for ${spanName}`,
1411
- unit: 'ms',
1412
- })
1413
- : undefined;
1414
-
1415
- const flushIfNeeded = async () => {
1416
- if (!shouldAutoFlush || !isRootSpan) return;
1417
-
1418
- try {
1419
- // Flush events queue
1420
- const queue = getEventQueue();
1421
- if (queue && queue.size() > 0) {
1422
- await queue.flush();
1423
- }
1424
-
1425
- // Flush OpenTelemetry spans if enabled
1426
- if (shouldAutoFlushSpans) {
1427
- const sdk = getSdk();
1428
- if (sdk) {
1429
- try {
1430
- // Type assertion needed as getTracerProvider is not in the public NodeSDK interface
1431
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1432
- const sdkAny = sdk as any;
1433
- if (typeof sdkAny.getTracerProvider === 'function') {
1434
- const tracerProvider = sdkAny.getTracerProvider();
1435
- if (
1436
- tracerProvider &&
1437
- typeof tracerProvider.forceFlush === 'function'
1438
- ) {
1439
- await tracerProvider.forceFlush();
1440
- }
1441
- }
1442
- } catch {
1443
- // Ignore errors when accessing tracer provider (may not be available in test mocks)
1444
- }
1445
- }
1446
- }
1447
- } catch (error) {
1448
- const initConfig = getInitConfig();
1449
- const logger = initConfig?.logger;
1450
- if (logger?.error) {
1451
- logger.error(
1452
- {
1453
- err: error instanceof Error ? error : undefined,
1454
- },
1455
- `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,
1456
- );
1457
- }
1458
- }
1459
- };
1460
-
1461
- // Build span options including root and kind
1462
- const spanOptions: import('@opentelemetry/api').SpanOptions = {};
1463
- if (options.startNewRoot) {
1464
- spanOptions.root = true;
1465
- }
1466
- if (options.spanKind !== undefined) {
1467
- spanOptions.kind = options.spanKind;
1468
- }
1469
-
1470
- const parentContext = getActiveContextWithBaggage();
1471
- return tracer.startActiveSpan(
1472
- spanName,
1473
- spanOptions,
1474
- parentContext,
1475
- (span) => {
1476
- return runInOperationContext(spanName, () => {
1477
- let shouldKeepSpan = true;
1478
-
1479
- setSpanName(span, spanName);
1480
- const ctxValue = createTraceContext(span);
1481
-
1482
- const handleTailSampling = (
1483
- success: boolean,
1484
- duration: number,
1485
- error?: unknown,
1486
- ) => {
1487
- if (
1488
- needsTailSampling &&
1489
- 'shouldKeepTrace' in sampler &&
1490
- typeof sampler.shouldKeepTrace === 'function'
1491
- ) {
1492
- shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {
1493
- success,
1494
- duration,
1495
- error,
1496
- });
1497
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
1498
- span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
1499
- }
1500
- };
1501
-
1502
- // Sync handlers for synchronous results (can't await)
1503
- // NOTE: forceFlushOnShutdown will NOT block for synchronous trace() calls
1504
- // Flush is fire-and-forget, so spans may be dropped if process exits immediately
1505
- const onSuccessSync = (result: TReturn) => {
1506
- const duration = performance.now() - startTime;
1507
-
1508
- callCounter?.add(1, {
1509
- operation: spanName,
1510
- status: 'success',
1511
- });
1512
-
1513
- durationHistogram?.record(duration, {
1514
- operation: spanName,
1515
- status: 'success',
1516
- });
1517
-
1518
- span.setStatus({ code: SpanStatusCode.OK });
1519
- span.setAttributes({
1520
- 'operation.name': spanName,
1521
- 'code.function': spanName,
1522
- 'operation.duration': duration,
1523
- 'operation.success': true,
1524
- });
1525
-
1526
- handleTailSampling(true, duration);
1527
-
1528
- span.end();
1529
- void flushIfNeeded();
1530
- return result;
1531
- };
1532
-
1533
- const onErrorSync = (error: unknown): never => {
1534
- const duration = performance.now() - startTime;
1535
-
1536
- callCounter?.add(1, {
1537
- operation: spanName,
1538
- status: 'error',
1539
- });
1540
-
1541
- durationHistogram?.record(duration, {
1542
- operation: spanName,
1543
- status: 'error',
1544
- });
1545
-
1546
- const errorMessage =
1547
- error instanceof Error ? error.message : 'Unknown error';
1548
- const truncatedMessage = truncateErrorMessage(errorMessage);
1549
-
1550
- span.setStatus({
1551
- code: SpanStatusCode.ERROR,
1552
- message: truncatedMessage,
1553
- });
1554
-
1555
- span.setAttributes({
1556
- 'operation.name': spanName,
1557
- 'code.function': spanName,
1558
- 'operation.duration': duration,
1559
- 'operation.success': false,
1560
- error: true,
1561
- 'exception.type':
1562
- error instanceof Error ? error.constructor.name : 'Error',
1563
- 'exception.message': truncatedMessage,
1564
- });
1565
-
1566
- if (error instanceof Error && error.stack) {
1567
- span.setAttribute(
1568
- 'exception.stack',
1569
- error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH),
1570
- );
1571
- }
1572
-
1573
- span.recordException(
1574
- error instanceof Error ? error : new Error(String(error)),
1575
- );
1576
-
1577
- handleTailSampling(false, duration, error);
1578
-
1579
- span.end();
1580
- void flushIfNeeded();
1581
- throw error;
1582
- };
1583
-
1584
- // Async handlers for Promise results (await flush)
1585
- const onSuccessAsync = async (result: TReturn) => {
1586
- const duration = performance.now() - startTime;
1587
-
1588
- callCounter?.add(1, {
1589
- operation: spanName,
1590
- status: 'success',
1591
- });
1592
-
1593
- durationHistogram?.record(duration, {
1594
- operation: spanName,
1595
- status: 'success',
1596
- });
1597
-
1598
- span.setStatus({ code: SpanStatusCode.OK });
1599
- span.setAttributes({
1600
- 'operation.name': spanName,
1601
- 'code.function': spanName,
1602
- 'operation.duration': duration,
1603
- 'operation.success': true,
1604
- });
1605
-
1606
- handleTailSampling(true, duration);
1607
-
1608
- span.end();
1609
- await flushIfNeeded();
1610
- return result;
1611
- };
1612
-
1613
- const onErrorAsync = async (error: unknown): Promise<never> => {
1614
- const duration = performance.now() - startTime;
1615
-
1616
- callCounter?.add(1, {
1617
- operation: spanName,
1618
- status: 'error',
1619
- });
1620
-
1621
- durationHistogram?.record(duration, {
1622
- operation: spanName,
1623
- status: 'error',
1624
- });
1625
-
1626
- const errorMessage =
1627
- error instanceof Error ? error.message : 'Unknown error';
1628
- const truncatedMessage = truncateErrorMessage(errorMessage);
1629
-
1630
- span.setStatus({
1631
- code: SpanStatusCode.ERROR,
1632
- message: truncatedMessage,
1633
- });
1634
-
1635
- span.setAttributes({
1636
- 'operation.name': spanName,
1637
- 'code.function': spanName,
1638
- 'operation.duration': duration,
1639
- 'operation.success': false,
1640
- error: true,
1641
- 'exception.type':
1642
- error instanceof Error ? error.constructor.name : 'Error',
1643
- 'exception.message': truncatedMessage,
1644
- });
1645
-
1646
- if (error instanceof Error && error.stack) {
1647
- span.setAttribute(
1648
- 'exception.stack',
1649
- error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH),
1650
- );
1651
- }
1652
-
1653
- span.recordException(
1654
- error instanceof Error ? error : new Error(String(error)),
1655
- );
1656
-
1657
- handleTailSampling(false, duration, error);
1658
-
1659
- span.end();
1660
- await flushIfNeeded();
1661
- throw error;
1662
- };
1663
-
1664
- try {
1665
- callCounter?.add(1, {
1666
- operation: spanName,
1667
- status: 'started',
1668
- });
1669
-
1670
- const result = fn(ctxValue);
1671
-
1672
- // Check if result is a Promise - use async handlers to await flush
1673
- if (result instanceof Promise) {
1674
- return result.then(onSuccessAsync, onErrorAsync);
1675
- }
1676
-
1677
- // Synchronous result - use sync handlers
1678
- return onSuccessSync(result);
1679
- } catch (error) {
1680
- return onErrorSync(error);
1681
- }
1682
- });
1683
- },
1684
- );
1685
- }
1686
-
1687
- /**
1688
- * Approach 1: trace() - Zero-ceremony HOF
1689
- *
1690
- * Wrap a single function with automatic tracing.
1691
- * The function receives a context object as the first parameter.
1692
- *
1693
- * Supports two patterns:
1694
- * 1. **Factory pattern** - Returns a traced function: `trace(ctx => (...args) => result)`
1695
- * 2. **Immediate execution** - Executes immediately with tracing: `trace(ctx => result)`
1696
- *
1697
- * @example Auto-inferred name - Plain function
1698
- * ```typescript
1699
- * export const createUser = trace(async (data) => {
1700
- * return await db.users.create(data)
1701
- * })
1702
- * // → Traced as "createUser"
1703
- * ```
1704
- *
1705
- * @example Auto-inferred name - Factory pattern (with ctx access)
1706
- * ```typescript
1707
- * export const createUser = trace(ctx => async (data) => {
1708
- * ctx.setAttribute('user.id', data.id)
1709
- * return await db.users.create(data)
1710
- * })
1711
- * // → Traced as "createUser", returns wrapped function
1712
- * ```
1713
- *
1714
- * @example Immediate execution - Execute once with tracing
1715
- * ```typescript
1716
- * // Wraps an existing function and executes immediately
1717
- * function timed<T>(fn: () => Promise<T>): Promise<T> {
1718
- * return trace(async (ctx) => {
1719
- * ctx.setAttribute('operation', 'timed')
1720
- * return await fn()
1721
- * })
1722
- * }
1723
- * // → Executes immediately, returns result directly
1724
- * ```
1725
- *
1726
- * @example Custom name - Plain function
1727
- * ```typescript
1728
- * export const createUser = trace('user.create', async (data) => {
1729
- * return await db.users.create(data)
1730
- * })
1731
- * // → Traced as "user.create"
1732
- * ```
1733
- *
1734
- * @example Custom name - Factory pattern
1735
- * ```typescript
1736
- * export const createUser = trace('user.create', ctx => async (data) => {
1737
- * ctx.setAttribute('user.id', data.id)
1738
- * return await db.users.create(data)
1739
- * })
1740
- * // → Traced as "user.create"
1741
- * ```
1742
- *
1743
- * @example Custom name - Immediate execution
1744
- * ```typescript
1745
- * const result = trace('fetch.user', async (ctx) => {
1746
- * ctx.setAttribute('userId', '123')
1747
- * return await fetchUser('123')
1748
- * })
1749
- * // → Executes immediately with span name "fetch.user"
1750
- * ```
1751
- *
1752
- * @example Full options - Plain function
1753
- * ```typescript
1754
- * export const createUser = trace({
1755
- * name: 'user.create',
1756
- * sampler: new AdaptiveSampler(),
1757
- * withMetrics: true
1758
- * }, async (data) => {
1759
- * return await db.users.create(data)
1760
- * })
1761
- * ```
1762
- *
1763
- * @example Full options - Factory pattern
1764
- * ```typescript
1765
- * export const createUser = trace({
1766
- * name: 'user.create',
1767
- * sampler: new AdaptiveSampler(),
1768
- * withMetrics: true
1769
- * }, ctx => async (data) => {
1770
- * ctx.setAttribute('user.id', data.id)
1771
- * return await db.users.create(data)
1772
- * })
1773
- * ```
1774
- */
1775
- // Sync overloads - Ordered from most specific to most generic for better type inference
1776
-
1777
- // Single argument - Specific overloads with TraceContext first
1778
- // Overload 1a: Immediate execution - sync function with context
1779
- export function trace<
1780
- TBaggage extends Record<string, unknown> | undefined = undefined,
1781
- TReturn = unknown,
1782
- >(fn: (ctx: TraceContext<TBaggage>) => TReturn): TReturn;
1783
- // Overload 1b: Factory sync function with no args - non-generic for type inference
1784
- export function trace<
1785
- TBaggage extends Record<string, unknown> | undefined = undefined,
1786
- >(fnFactory: (ctx: TraceContext<TBaggage>) => () => unknown): () => unknown;
1787
- // Overload 1c: Factory sync function - non-generic for type inference
1788
- export function trace<
1789
- TBaggage extends Record<string, unknown> | undefined = undefined,
1790
- TArgs extends unknown[] = unknown[],
1791
- TReturn = unknown,
1792
- >(
1793
- fnFactory: (ctx: TraceContext<TBaggage>) => (...args: TArgs) => TReturn,
1794
- ): (...args: TArgs) => TReturn;
1795
- // Overload 1d: Factory sync function with no args returning explicit type (typed generic)
1796
- export function trace<TReturn = unknown>(
1797
- fnFactory: (ctx: TraceContext) => () => TReturn,
1798
- ): () => TReturn;
1799
- // Overload 1e: Factory sync function - use conditional type to extract signature
1800
- // This overload is more specific and helps TypeScript infer types from factory functions
1801
- export function trace<
1802
- TFactory extends (ctx: TraceContext) => (...args: unknown[]) => unknown,
1803
- >(fnFactory: TFactory): ExtractFunctionSignature<TFactory>;
1804
- // Overload 1f: Generic factory sync function (fallback)
1805
- export function trace<TArgs extends unknown[], TReturn = unknown>(
1806
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1807
- ): (...args: TArgs) => TReturn;
1808
-
1809
- // Single argument - Plain function overloads (no ctx parameter)
1810
- // Overload 1g: Plain sync function with no args
1811
- export function trace<TReturn = unknown>(fn: () => TReturn): () => TReturn;
1812
- // Overload 1h: Plain sync function (generic fallback)
1813
- export function trace<TArgs extends unknown[], TReturn = unknown>(
1814
- fn: (...args: TArgs) => TReturn,
1815
- ): (...args: TArgs) => TReturn;
1816
-
1817
- // Two arguments - name + function - Specific overloads with TraceContext first
1818
- // Overload 2a: Name + immediate execution sync with context
1819
- // This overload only matches functions that DON'T return functions (factories)
1820
- export function trace<TReturn = unknown>(
1821
- name: string,
1822
- fn: ExcludeFactoryReturn<(ctx: TraceContext) => TReturn>,
1823
- ): TReturn;
1824
- // Overload 2b: Name + factory sync function with no args
1825
- export function trace<TReturn = unknown>(
1826
- name: string,
1827
- fnFactory: (ctx: TraceContext) => () => TReturn,
1828
- ): () => TReturn;
1829
- // Overload 2c: Name + factory sync function - non-generic for type inference
1830
- export function trace<TArgs extends unknown[], TReturn>(
1831
- name: string,
1832
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1833
- ): (...args: TArgs) => TReturn;
1834
- // Overload 2d: Name + factory sync function - use conditional type to extract signature
1835
- // This overload allows TypeScript to infer types from the factory function parameter
1836
- export function trace<
1837
- TFactory extends (ctx: TraceContext) => (...args: unknown[]) => unknown,
1838
- >(name: string, fnFactory: TFactory): ExtractFunctionSignature<TFactory>;
1839
- // Overload 2e: Name + factory sync function (fallback)
1840
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1841
- name: string,
1842
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1843
- ): (...args: TArgs) => TReturn;
1844
-
1845
- // Two arguments - name + function - Plain function overloads
1846
- // Overload 2f: Name + plain sync function
1847
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1848
- name: string,
1849
- fn: (...args: TArgs) => TReturn,
1850
- ): (...args: TArgs) => TReturn;
1851
-
1852
- // Two arguments - options + function - Specific overloads with TraceContext first
1853
- // Overload 3a: Options + immediate execution sync with context
1854
- export function trace<TReturn = unknown>(
1855
- options: TracingOptions<[], TReturn>,
1856
- fn: (ctx: TraceContext) => TReturn,
1857
- ): TReturn;
1858
- // Overload 3b: Options + factory sync function with no args
1859
- export function trace<TReturn = unknown>(
1860
- options: TracingOptions<[], TReturn>,
1861
- fnFactory: (ctx: TraceContext) => () => TReturn,
1862
- ): () => TReturn;
1863
- // Overload 3c: Options + factory sync function - non-generic for type inference
1864
- export function trace<TArgs extends unknown[], TReturn>(
1865
- options: TracingOptions<TArgs, TReturn>,
1866
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1867
- ): (...args: TArgs) => TReturn;
1868
- // Overload 3d: Options + factory sync function (fallback)
1869
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1870
- options: TracingOptions<TArgs, TReturn>,
1871
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
1872
- ): (...args: TArgs) => TReturn;
1873
-
1874
- // Two arguments - options + function - Plain function overloads
1875
- // Overload 3e: Options + plain sync function
1876
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1877
- options: TracingOptions<TArgs, TReturn>,
1878
- fn: (...args: TArgs) => TReturn,
1879
- ): (...args: TArgs) => TReturn;
1880
-
1881
- // Async overloads - Ordered from most specific to most generic
1882
-
1883
- // Single argument - Specific async overloads with TraceContext first
1884
- // Overload 4a: Immediate execution - async function with context
1885
- export function trace<TReturn = unknown>(
1886
- fn: (ctx: TraceContext) => Promise<TReturn>,
1887
- ): Promise<TReturn>;
1888
- // Overload 4b: Factory async function with no args - non-generic for type inference
1889
- export function trace(
1890
- fnFactory: (ctx: TraceContext) => () => Promise<unknown>,
1891
- ): () => Promise<unknown>;
1892
- // Overload 4c: Factory async function - non-generic for type inference
1893
- export function trace<TArgs extends unknown[], TReturn>(
1894
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1895
- ): (...args: TArgs) => Promise<TReturn>;
1896
- // Overload 4d: Factory async function with no args (typed generic)
1897
- export function trace<TReturn = unknown>(
1898
- fnFactory: (ctx: TraceContext) => () => Promise<TReturn>,
1899
- ): () => Promise<TReturn>;
1900
- // Overload 4e: Factory async function - use conditional type to extract signature
1901
- export function trace<
1902
- TFactory extends (
1903
- ctx: TraceContext,
1904
- ) => (...args: unknown[]) => Promise<unknown>,
1905
- >(fnFactory: TFactory): ExtractFunctionSignature<TFactory>;
1906
- // Overload 4f: Generic factory async function (fallback)
1907
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1908
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1909
- ): (...args: TArgs) => Promise<TReturn>;
1910
-
1911
- // Single argument - Plain async function overloads (no ctx parameter)
1912
- // Overload 4g: Plain async function with no args
1913
- export function trace<TReturn = unknown>(
1914
- fn: () => Promise<TReturn>,
1915
- ): () => Promise<TReturn>;
1916
- // Overload 4h: Plain async function (generic fallback)
1917
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1918
- fn: (...args: TArgs) => Promise<TReturn>,
1919
- ): (...args: TArgs) => Promise<TReturn>;
1920
-
1921
- // Two arguments - name + async function - Specific overloads with TraceContext first
1922
- // Overload 5a: Name + immediate execution async with context
1923
- // This overload only matches functions that DON'T return functions (factories)
1924
- export function trace<TReturn = unknown>(
1925
- name: string,
1926
- fn: ExcludeFactoryReturn<(ctx: TraceContext) => Promise<TReturn>>,
1927
- ): Promise<TReturn>;
1928
- // Overload 5b: Name + factory async function with no args
1929
- export function trace<TReturn = unknown>(
1930
- name: string,
1931
- fnFactory: (ctx: TraceContext) => () => Promise<TReturn>,
1932
- ): () => Promise<TReturn>;
1933
- // Overload 5c: Name + factory async function - non-generic for type inference
1934
- export function trace<TArgs extends unknown[], TReturn>(
1935
- name: string,
1936
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1937
- ): (...args: TArgs) => Promise<TReturn>;
1938
- // Overload 5d: Name + factory async function - use conditional type to extract signature
1939
- // This overload allows TypeScript to infer types from the factory function parameter
1940
- export function trace<
1941
- TFactory extends (
1942
- ctx: TraceContext,
1943
- ) => (...args: unknown[]) => Promise<unknown>,
1944
- >(name: string, fnFactory: TFactory): ExtractFunctionSignature<TFactory>;
1945
- // Overload 5e: Name + factory async function (fallback)
1946
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1947
- name: string,
1948
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1949
- ): (...args: TArgs) => Promise<TReturn>;
1950
-
1951
- // Two arguments - name + async function - Plain function overloads
1952
- // Overload 5f: Name + plain async function
1953
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1954
- name: string,
1955
- fn: (...args: TArgs) => Promise<TReturn>,
1956
- ): (...args: TArgs) => Promise<TReturn>;
1957
-
1958
- // Two arguments - options + async function - Specific overloads with TraceContext first
1959
- // Overload 6a: Options + immediate execution async with context
1960
- export function trace<TReturn = unknown>(
1961
- options: TracingOptions<[], TReturn>,
1962
- fn: (ctx: TraceContext) => Promise<TReturn>,
1963
- ): Promise<TReturn>;
1964
- // Overload 6b: Options + factory async function with no args
1965
- export function trace<TReturn = unknown>(
1966
- options: TracingOptions<[], TReturn>,
1967
- fnFactory: (ctx: TraceContext) => () => Promise<TReturn>,
1968
- ): () => Promise<TReturn>;
1969
- // Overload 6c: Options + factory async function - non-generic for type inference
1970
- export function trace<TArgs extends unknown[], TReturn>(
1971
- options: TracingOptions<TArgs, TReturn>,
1972
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1973
- ): (...args: TArgs) => Promise<TReturn>;
1974
- // Overload 6d: Options + factory async function (fallback)
1975
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1976
- options: TracingOptions<TArgs, TReturn>,
1977
- fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
1978
- ): (...args: TArgs) => Promise<TReturn>;
1979
-
1980
- // Two arguments - options + async function - Plain function overloads
1981
- // Overload 6e: Options + plain async function
1982
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1983
- options: TracingOptions<TArgs, TReturn>,
1984
- fn: (...args: TArgs) => Promise<TReturn>,
1985
- ): (...args: TArgs) => Promise<TReturn>;
1986
-
1987
- // Implementation
1988
- export function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(
1989
- fnOrNameOrOptions:
1990
- | ((...args: TArgs) => TReturn)
1991
- | ((...args: TArgs) => Promise<TReturn>)
1992
- | ((ctx: TraceContext) => (...args: TArgs) => TReturn)
1993
- | ((ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>)
1994
- | ((ctx: TraceContext) => TReturn)
1995
- | ((ctx: TraceContext) => Promise<TReturn>)
1996
- | string
1997
- | TracingOptions<TArgs, TReturn>,
1998
- maybeFn?:
1999
- | ((...args: TArgs) => TReturn)
2000
- | ((...args: TArgs) => Promise<TReturn>)
2001
- | ((ctx: TraceContext) => (...args: TArgs) => TReturn)
2002
- | ((ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>)
2003
- | ((ctx: TraceContext) => TReturn)
2004
- | ((ctx: TraceContext) => Promise<TReturn>),
2005
- ): WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn> {
2006
- // Handle: trace(fn) - single argument
2007
- if (typeof fnOrNameOrOptions === 'function') {
2008
- // Check if it's immediate execution pattern: (ctx) => result
2009
- if (
2010
- looksLikeTraceFactory(fnOrNameOrOptions as GenericFunction) &&
2011
- !isFactoryReturningFunction(
2012
- fnOrNameOrOptions as (ctx: TraceContext) => unknown,
2013
- )
2014
- ) {
2015
- // Immediate execution pattern
2016
- return executeImmediately(
2017
- fnOrNameOrOptions as (ctx: TraceContext) => TReturn | Promise<TReturn>,
2018
- {},
2019
- ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
2020
- }
2021
- // Factory pattern or plain function
2022
- return wrapFactoryWithTracing(
2023
- fnOrNameOrOptions as (...args: TArgs) => TReturn,
2024
- {} as TracingOptions<TArgs, TReturn>,
2025
- );
2026
- }
2027
-
2028
- // Handle: trace(name, fn) or trace(options, fn) - two arguments
2029
- if (typeof fnOrNameOrOptions === 'string') {
2030
- if (!maybeFn) {
2031
- throw new Error('trace(name, fn): fn is required');
2032
- }
2033
- // Check if it's immediate execution pattern
2034
- if (
2035
- looksLikeTraceFactory(maybeFn as GenericFunction) &&
2036
- !isFactoryReturningFunction(maybeFn as (ctx: TraceContext) => unknown)
2037
- ) {
2038
- // Immediate execution pattern with name
2039
- return executeImmediately(
2040
- maybeFn as (ctx: TraceContext) => TReturn | Promise<TReturn>,
2041
- { name: fnOrNameOrOptions },
2042
- ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
2043
- }
2044
- return wrapFactoryWithTracing(
2045
- maybeFn as (...args: TArgs) => TReturn,
2046
- { name: fnOrNameOrOptions } as TracingOptions<TArgs, TReturn>,
2047
- );
2048
- }
2049
-
2050
- // Handle: trace(options, fn)
2051
- if (!maybeFn) {
2052
- throw new Error('trace(options, fn): fn is required');
2053
- }
2054
-
2055
- // Check if it's immediate execution pattern
2056
- if (
2057
- looksLikeTraceFactory(maybeFn as GenericFunction) &&
2058
- !isFactoryReturningFunction(maybeFn as (ctx: TraceContext) => unknown)
2059
- ) {
2060
- // Immediate execution pattern with options
2061
- return executeImmediately(
2062
- maybeFn as (ctx: TraceContext) => TReturn | Promise<TReturn>,
2063
- fnOrNameOrOptions as TracingOptions<unknown[], unknown>,
2064
- ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
2065
- }
2066
-
2067
- return wrapFactoryWithTracing(
2068
- maybeFn as (...args: TArgs) => TReturn,
2069
- fnOrNameOrOptions as TracingOptions<TArgs, TReturn>,
2070
- );
2071
- }
2072
-
2073
- /**
2074
- * Approach 2: withTracing() - Middleware-style composable wrapper
2075
- *
2076
- * Returns a HOF that wraps functions with tracing.
2077
- * Perfect for composition and reusable configuration.
2078
- *
2079
- * @example Standard usage
2080
- * ```typescript
2081
- * export const createUser = withTracing({
2082
- * name: 'user.create'
2083
- * })(ctx => async (data) => {
2084
- * ctx.setAttribute('user.id', data.id)
2085
- * return await db.users.create(data)
2086
- * })
2087
- * ```
2088
- *
2089
- * @example Composable
2090
- * ```typescript
2091
- * const trace = withTracing({ serviceName: 'user' })
2092
- *
2093
- * export const createUser = trace(ctx => async (data) => { })
2094
- * export const updateUser = trace(ctx => async (id, data) => { })
2095
- * ```
2096
- *
2097
- * @example With other middleware
2098
- * ```typescript
2099
- * export const createUser = compose(
2100
- * withAuth({ role: 'admin' }),
2101
- * withTracing({ name: 'user.create' }),
2102
- * withRateLimit({ max: 100 })
2103
- * )(ctx => async (data) => { })
2104
- * ```
2105
- */
2106
- export function withTracing<
2107
- TArgs extends unknown[] = unknown[],
2108
- TReturn = unknown,
2109
- >(
2110
- options: TracingOptions<TArgs, TReturn> = {},
2111
- ): (
2112
- fnFactory: (
2113
- ctx: TraceContext,
2114
- ) => (...args: TArgs) => TReturn | Promise<TReturn>,
2115
- ) => (...args: TArgs) => TReturn | Promise<TReturn> {
2116
- return (
2117
- fnFactory: (
2118
- ctx: TraceContext,
2119
- ) => (...args: TArgs) => TReturn | Promise<TReturn>,
2120
- ): WrappedFunction<TArgs, TReturn> =>
2121
- wrapFactoryWithTracing<TArgs, TReturn>(fnFactory, options);
2122
- }
2123
-
2124
- /**
2125
- * Approach 3: instrument() - Batch auto-instrumentation
2126
- *
2127
- * Instrument an entire module/object at once.
2128
- * Closest to @Instrumented decorator pattern.
2129
- *
2130
- * @example Basic usage
2131
- * ```typescript
2132
- * export default instrument({
2133
- * functions: {
2134
- * createUser: async (data) => { },
2135
- * updateUser: async (id, data) => { },
2136
- * deleteUser: async (id) => { }
2137
- * },
2138
- * serviceName: 'user',
2139
- * sampler: new AdaptiveSampler()
2140
- * })
2141
- * // → Traced as "user.createUser", "user.updateUser", "user.deleteUser"
2142
- * ```
2143
- *
2144
- * @example Per-function overrides
2145
- * ```typescript
2146
- * export default instrument({
2147
- * functions: {
2148
- * createUser: async (data) => { },
2149
- * deleteUser: async (id) => { }
2150
- * },
2151
- * serviceName: 'user',
2152
- * overrides: {
2153
- * deleteUser: {
2154
- * sampler: new AlwaysSampler(),
2155
- * withMetrics: true
2156
- * }
2157
- * }
2158
- * })
2159
- * ```
2160
- *
2161
- * @example Skip functions
2162
- * ```typescript
2163
- * export default instrument({
2164
- * functions: {
2165
- * createUser: async (data) => { },
2166
- * _internal: async () => { }, // Auto-skipped (_-prefix)
2167
- * deleteUser: async (id) => { }
2168
- * },
2169
- * serviceName: 'user',
2170
- * skip: [/^test/, (key) => key.includes('debug')]
2171
- * })
2172
- * ```
2173
- */
2174
- export function instrument<T extends Record<string, InstrumentableFunction>>(
2175
- options: InstrumentOptions<T>,
2176
- ): T {
2177
- const { functions, ...tracingOptions } = options;
2178
- const instrumented: Partial<T> = {};
2179
-
2180
- for (const key of Object.keys(functions)) {
2181
- const typedKey = key as keyof T;
2182
- const fn = functions[typedKey];
2183
-
2184
- // Skip if not a function or undefined - just pass through the value
2185
- if (!fn || typeof fn !== 'function') {
2186
- instrumented[typedKey] = fn as T[typeof typedKey];
2187
- continue;
2188
- }
2189
-
2190
- // Only instrument own enumerable async functions
2191
- // Check if should skip
2192
- if (shouldSkip(key, fn, tracingOptions.skip)) {
2193
- instrumented[typedKey] = fn as T[typeof typedKey];
2194
- continue;
2195
- }
2196
-
2197
- // Merge base options with per-function overrides
2198
- const fnOptions: TracingOptions = {
2199
- ...tracingOptions,
2200
- ...tracingOptions.overrides?.[key],
2201
- // If no explicit name, use key as function name
2202
- name: tracingOptions.overrides?.[key]?.name,
2203
- };
2204
-
2205
- // Bind function to original object to preserve 'this' context
2206
- // This ensures methods can access state on the original object
2207
- const boundFn = fn.bind(functions);
2208
-
2209
- // Convert plain function to factory pattern for trace()
2210
- // For instrument(), we create a factory that ignores ctx and returns the original function
2211
- const fnFactory = (ctx: TraceContext) => {
2212
- void ctx;
2213
- return boundFn;
2214
- };
2215
-
2216
- // Wrap with tracing (sync or async based on implementation)
2217
- instrumented[typedKey] = wrapFactoryWithTracing(
2218
- fnFactory,
2219
- fnOptions,
2220
- key,
2221
- ) as T[typeof typedKey];
2222
- }
2223
-
2224
- return instrumented as T;
2225
- }
2226
-
2227
- /**
2228
- * Options for span() function
2229
- */
2230
- export interface SpanOptions {
2231
- /** Span name */
2232
- name: string;
2233
- /** Attributes to set on the span */
2234
- attributes?: Record<string, string | number | boolean>;
2235
- }
2236
-
2237
- /**
2238
- * Execute a function within a named span
2239
- *
2240
- * Useful for adding tracing to specific code blocks without wrapping
2241
- * the entire function. Supports both synchronous and asynchronous functions.
2242
- *
2243
- * Mirrors `trace()`: pass a span name as the first argument for the common
2244
- * case, or full `SpanOptions` when you need to attach attributes.
2245
- *
2246
- * @example
2247
- * ```typescript
2248
- * // Name shorthand
2249
- * await span('payment.charge', async (span) => {
2250
- * await chargeCustomer(order);
2251
- * })
2252
- *
2253
- * // Full options when attributes are needed
2254
- * await span(
2255
- * { name: 'payment.charge', attributes: { amount: order.total } },
2256
- * async (span) => {
2257
- * await chargeCustomer(order);
2258
- * },
2259
- * )
2260
- *
2261
- * // Sync
2262
- * const total = span('calculateTotal', (span) => {
2263
- * return items.reduce((sum, item) => sum + item.price, 0);
2264
- * })
2265
- * ```
2266
- */
2267
- // Overloads — sync first (more specific match), then async.
2268
- // Each shape is offered with a string name OR a full SpanOptions object so
2269
- // span() aligns with trace()'s argument flexibility.
2270
- export function span<T = unknown>(name: string, fn: (span: Span) => T): T;
2271
- export function span<T = unknown>(
2272
- name: string,
2273
- fn: (span: Span) => Promise<T>,
2274
- ): Promise<T>;
2275
- export function span<T = unknown>(
2276
- options: SpanOptions,
2277
- fn: (span: Span) => T,
2278
- ): T;
2279
- export function span<T = unknown>(
2280
- options: SpanOptions,
2281
- fn: (span: Span) => Promise<T>,
2282
- ): Promise<T>;
2283
- // Implementation
2284
- export function span<T = unknown>(
2285
- nameOrOptions: string | SpanOptions,
2286
- fn: (span: Span) => T | Promise<T>,
2287
- ): T | Promise<T> {
2288
- const options: SpanOptions =
2289
- typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;
2290
- const config = getConfig();
2291
- const tracer = config.tracer;
2292
- const { name, attributes } = options;
2293
-
2294
- const executeSpan = (span: Span) => {
2295
- // Run within operation context so events can auto-capture operation.name
2296
- return runInOperationContext(name, () => {
2297
- try {
2298
- // Set attributes
2299
- if (attributes) {
2300
- for (const [key, value] of Object.entries(attributes)) {
2301
- span.setAttribute(key, value);
2302
- }
2303
- }
2304
-
2305
- const result = fn(span);
2306
-
2307
- // Check if result is a Promise
2308
- if (result instanceof Promise) {
2309
- return result
2310
- .then((resolved) => {
2311
- span.setStatus({ code: SpanStatusCode.OK });
2312
- span.end();
2313
- return resolved;
2314
- })
2315
- .catch((error) => {
2316
- const errorMessage =
2317
- error instanceof Error
2318
- ? error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)
2319
- : String(error).slice(0, MAX_ERROR_MESSAGE_LENGTH);
2320
-
2321
- span.setAttribute('error.message', errorMessage);
2322
- span.setStatus({
2323
- code: SpanStatusCode.ERROR,
2324
- message: errorMessage,
2325
- });
2326
-
2327
- span.recordException(
2328
- error instanceof Error ? error : new Error(String(error)),
2329
- );
2330
- span.end();
2331
- throw error;
2332
- });
2333
- } else {
2334
- // Synchronous function
2335
- span.setStatus({ code: SpanStatusCode.OK });
2336
- span.end();
2337
- return result;
2338
- }
2339
- } catch (error) {
2340
- // Synchronous error handling
2341
- const errorMessage =
2342
- error instanceof Error
2343
- ? error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)
2344
- : String(error).slice(0, MAX_ERROR_MESSAGE_LENGTH);
2345
-
2346
- span.setAttribute('error.message', errorMessage);
2347
- span.setStatus({
2348
- code: SpanStatusCode.ERROR,
2349
- message: errorMessage,
2350
- });
2351
-
2352
- span.recordException(
2353
- error instanceof Error ? error : new Error(String(error)),
2354
- );
2355
- span.end();
2356
- throw error;
2357
- }
2358
- });
2359
- };
2360
-
2361
- const parentContext = getActiveContextWithBaggage();
2362
- const result = tracer.startActiveSpan(name, {}, parentContext, executeSpan);
2363
-
2364
- // tracer.startActiveSpan might return a Promise even for sync callbacks
2365
- // Check if it's a Promise and handle accordingly
2366
- if (result instanceof Promise) {
2367
- return result;
2368
- }
2369
-
2370
- return result as T;
2371
- }
2372
-
2373
- /**
2374
- * Options for withNewContext() function
2375
- */
2376
- export interface WithNewContextOptions<T = unknown> {
2377
- /** Function to execute in new root context */
2378
- fn: () => Promise<T>;
2379
- }
2380
-
2381
- /**
2382
- * Execute a function in a new root context (prevents span propagation)
2383
- *
2384
- * Useful when you want to start a completely new trace without
2385
- * parent-child relationships.
2386
- *
2387
- * @example
2388
- * ```typescript
2389
- * async function handleWebhook(payload: WebhookPayload) {
2390
- * // This creates a new root trace, not connected to the HTTP request trace
2391
- * await withNewContext({
2392
- * fn: async () => {
2393
- * await trace(ctx => async () => {
2394
- * await processWebhookPayload(payload)
2395
- * })()
2396
- * }
2397
- * })
2398
- * }
2399
- * ```
2400
- */
2401
- export async function withNewContext<T = unknown>(
2402
- options: WithNewContextOptions<T>,
2403
- ): Promise<T> {
2404
- const { fn } = options;
2405
- const config = getConfig();
2406
- const tracer = config.tracer;
2407
-
2408
- // Start a new root span (breaks trace propagation)
2409
- return tracer.startActiveSpan('root', { root: true }, async (span) => {
2410
- try {
2411
- const result = await fn();
2412
- span.setStatus({ code: SpanStatusCode.OK });
2413
- return result;
2414
- } catch (error) {
2415
- span.recordException(
2416
- error instanceof Error ? error : new Error(String(error)),
2417
- );
2418
- span.setStatus({ code: SpanStatusCode.ERROR });
2419
- throw error;
2420
- } finally {
2421
- span.end();
2422
- }
2423
- });
2424
- }
2425
-
2426
- /**
2427
- * Options for withBaggage() function
2428
- */
2429
- export interface WithBaggageOptions<T = unknown> {
2430
- /** Baggage entries to set (key-value pairs) */
2431
- baggage: Record<string, string>;
2432
- /** Function to execute with the updated baggage */
2433
- fn: () => T | Promise<T>;
2434
- }
2435
-
2436
- /**
2437
- * Execute a function with updated baggage entries
2438
- *
2439
- * Baggage is immutable in OpenTelemetry, so this helper creates a new context
2440
- * with the specified baggage entries and runs the function within that context.
2441
- * All child spans created within the function will inherit the baggage.
2442
- *
2443
- * @example Setting baggage for downstream services
2444
- * ```typescript
2445
- * import { trace, withBaggage } from 'autotel';
2446
- *
2447
- * export const createOrder = trace((ctx) => async (order: Order) => {
2448
- * // Set baggage that will be propagated to downstream HTTP calls
2449
- * return await withBaggage({
2450
- * baggage: {
2451
- * 'tenant.id': order.tenantId,
2452
- * 'user.id': order.userId,
2453
- * },
2454
- * fn: async () => {
2455
- * // This HTTP call will include the baggage in headers
2456
- * await fetch('/api/charge', {
2457
- * method: 'POST',
2458
- * body: JSON.stringify(order),
2459
- * });
2460
- * },
2461
- * });
2462
- * });
2463
- * ```
2464
- *
2465
- * @example Using with existing baggage
2466
- * ```typescript
2467
- * export const processOrder = trace((ctx) => async (order: Order) => {
2468
- * // Read existing baggage
2469
- * const tenantId = ctx.getBaggage('tenant.id');
2470
- *
2471
- * // Add additional baggage entries
2472
- * return await withBaggage({
2473
- * baggage: {
2474
- * 'order.id': order.id,
2475
- * 'order.amount': String(order.amount),
2476
- * },
2477
- * fn: async () => {
2478
- * await charge(order);
2479
- * },
2480
- * });
2481
- * });
2482
- * ```
2483
- */
2484
- export function withBaggage<T = unknown>(
2485
- options: WithBaggageOptions<T>,
2486
- ): T | Promise<T> {
2487
- const { baggage: baggageEntries, fn } = options;
2488
- const currentContext = context.active();
2489
-
2490
- // Get existing baggage or create new
2491
- let updatedBaggage =
2492
- propagation.getBaggage(currentContext) ?? propagation.createBaggage();
2493
-
2494
- // Set all baggage entries
2495
- for (const [key, value] of Object.entries(baggageEntries)) {
2496
- updatedBaggage = updatedBaggage.setEntry(key, { value });
2497
- }
2498
-
2499
- // Create new context with updated baggage
2500
- const newContext = propagation.setBaggage(currentContext, updatedBaggage);
2501
-
2502
- // Sync contextStorage so nested traces (via getActiveContextWithBaggage) see the baggage.
2503
- // Use run() instead of enterWith() to properly scope the context changes.
2504
- const ctxStorage = getContextStorage();
2505
- const previousStored = ctxStorage.getStore();
2506
- const baggageEnrichedStored = previousStored
2507
- ? { value: propagation.setBaggage(previousStored.value, updatedBaggage) }
2508
- : { value: newContext };
2509
-
2510
- // Run the function within the new context, scoped properly
2511
- const result = previousStored
2512
- ? ctxStorage.run(baggageEnrichedStored, () => context.with(newContext, fn))
2513
- : context.with(newContext, fn);
2514
-
2515
- if (result instanceof Promise) {
2516
- // For async operations, ensure context is restored after the promise settles
2517
- return result.then(
2518
- (value) => {
2519
- // Restore original context before resolving
2520
- if (previousStored) {
2521
- return ctxStorage.run(previousStored, () => value);
2522
- }
2523
- return value;
2524
- },
2525
- (error) => {
2526
- // Restore original context before rejecting
2527
- if (previousStored) {
2528
- return ctxStorage.run(previousStored, () => {
2529
- throw error;
2530
- });
2531
- }
2532
- throw error;
2533
- },
2534
- );
2535
- }
2536
-
2537
- // Sync function - context automatically restored when scope exits
2538
- return result;
2539
- }