autotel 4.1.0 → 4.2.1

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 (253) hide show
  1. package/dist/auto.cjs +5 -3
  2. package/dist/auto.cjs.map +1 -1
  3. package/dist/auto.js +3 -3
  4. package/dist/auto.js.map +1 -1
  5. package/dist/chunk-C_NdSu1c.cjs +34 -0
  6. package/dist/correlation-id.cjs +1 -1
  7. package/dist/correlation-id.d.cts.map +1 -1
  8. package/dist/correlation-id.d.ts.map +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
  13. package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
  14. package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
  15. package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
  16. package/dist/event.cjs +1 -1
  17. package/dist/event.js +1 -1
  18. package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
  19. package/dist/functional-C8B0Qa7o.cjs.map +1 -0
  20. package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
  21. package/dist/functional-r-AUIRy_.js.map +1 -0
  22. package/dist/functional.cjs +1 -1
  23. package/dist/functional.js +1 -1
  24. package/dist/http.cjs +1 -1
  25. package/dist/http.js +1 -1
  26. package/dist/index.cjs +15 -13
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
  33. package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
  34. package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
  35. package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
  36. package/dist/instrumentation.cjs +2 -2
  37. package/dist/instrumentation.js +2 -2
  38. package/dist/logger.cjs +236 -8
  39. package/dist/logger.cjs.map +1 -0
  40. package/dist/messaging.cjs +1 -1
  41. package/dist/messaging.js +1 -1
  42. package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
  43. package/dist/node-require-CZ_PU448.cjs.map +1 -0
  44. package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
  45. package/dist/node-require-vROmTeJ8.js.map +1 -0
  46. package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
  47. package/dist/operation-context-CKBoA4Qy.js.map +1 -0
  48. package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
  49. package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
  50. package/dist/register.cjs +3 -1
  51. package/dist/register.cjs.map +1 -1
  52. package/dist/register.js +2 -2
  53. package/dist/register.js.map +1 -1
  54. package/dist/semantic-helpers.cjs +1 -1
  55. package/dist/semantic-helpers.js +1 -1
  56. package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
  57. package/dist/stable-hash-ChFBIhNt.js.map +1 -0
  58. package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
  59. package/dist/stable-hash-brKISGf1.cjs.map +1 -0
  60. package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
  61. package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
  62. package/dist/trace-helpers.cjs +1 -1
  63. package/dist/trace-helpers.js +1 -1
  64. package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
  65. package/dist/track-COUuU48p.js.map +1 -0
  66. package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
  67. package/dist/track-Cb3Q4QmS.cjs.map +1 -0
  68. package/dist/validate.cjs +1 -1
  69. package/dist/validate.js +1 -1
  70. package/dist/webhook.cjs +1 -1
  71. package/dist/webhook.js +1 -1
  72. package/dist/workflow-distributed.cjs +1 -1
  73. package/dist/workflow-distributed.js +1 -1
  74. package/dist/workflow.cjs +3 -1
  75. package/dist/workflow.cjs.map +1 -1
  76. package/dist/workflow.d.cts.map +1 -1
  77. package/dist/workflow.d.ts.map +1 -1
  78. package/dist/workflow.js +3 -3
  79. package/dist/workflow.js.map +1 -1
  80. package/dist/yaml-config.cjs +233 -4
  81. package/dist/yaml-config.cjs.map +1 -0
  82. package/dist/yaml-config.d.cts.map +1 -1
  83. package/dist/yaml-config.d.ts.map +1 -1
  84. package/dist/yaml-config.js +8 -7
  85. package/dist/yaml-config.js.map +1 -1
  86. package/package.json +1 -2
  87. package/dist/functional-DtI0u4vx.js.map +0 -1
  88. package/dist/functional-zpzNLhky.cjs.map +0 -1
  89. package/dist/logger-thMPLpOG.cjs +0 -487
  90. package/dist/logger-thMPLpOG.cjs.map +0 -1
  91. package/dist/node-require-DF5QBX6z.cjs.map +0 -1
  92. package/dist/node-require-Db1oDpLj.js.map +0 -1
  93. package/dist/operation-context-C-2hmmtP.js.map +0 -1
  94. package/dist/operation-context-n4_obUwq.cjs.map +0 -1
  95. package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
  96. package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
  97. package/dist/track-D59FfpL0.cjs.map +0 -1
  98. package/dist/track-wc0HafS_.js.map +0 -1
  99. package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
  100. package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
  101. package/src/attribute-redacting-processor.test.ts +0 -763
  102. package/src/attribute-redacting-processor.ts +0 -621
  103. package/src/attributes/attachers.ts +0 -161
  104. package/src/attributes/builders.ts +0 -529
  105. package/src/attributes/domains.ts +0 -42
  106. package/src/attributes/index.ts +0 -81
  107. package/src/attributes/registry.ts +0 -323
  108. package/src/attributes/types.ts +0 -211
  109. package/src/attributes/utils.ts +0 -64
  110. package/src/attributes/validators.ts +0 -266
  111. package/src/attributes.test.ts +0 -292
  112. package/src/auto.ts +0 -67
  113. package/src/autotel-logger.test.ts +0 -548
  114. package/src/autotel-logger.ts +0 -364
  115. package/src/baggage-span-processor.test.ts +0 -202
  116. package/src/baggage-span-processor.ts +0 -100
  117. package/src/business-baggage.test.ts +0 -500
  118. package/src/business-baggage.ts +0 -669
  119. package/src/circuit-breaker.test.ts +0 -341
  120. package/src/circuit-breaker.ts +0 -184
  121. package/src/config.test.ts +0 -94
  122. package/src/config.ts +0 -172
  123. package/src/correlated-events.test.ts +0 -151
  124. package/src/correlated-events.ts +0 -47
  125. package/src/correlation-id.test.ts +0 -163
  126. package/src/correlation-id.ts +0 -206
  127. package/src/db.test.ts +0 -252
  128. package/src/db.ts +0 -447
  129. package/src/decorators.test.ts +0 -153
  130. package/src/decorators.ts +0 -188
  131. package/src/define-event.test.ts +0 -41
  132. package/src/define-event.ts +0 -58
  133. package/src/devtools.ts +0 -60
  134. package/src/drain-pipeline.test.ts +0 -68
  135. package/src/drain-pipeline.ts +0 -199
  136. package/src/drain-toolkit.test.ts +0 -113
  137. package/src/drain-toolkit.ts +0 -129
  138. package/src/enricher-toolkit.test.ts +0 -67
  139. package/src/enricher-toolkit.ts +0 -79
  140. package/src/enrichers.test.ts +0 -150
  141. package/src/enrichers.ts +0 -145
  142. package/src/env-config.test.ts +0 -323
  143. package/src/env-config.ts +0 -309
  144. package/src/error-catalog.test.ts +0 -133
  145. package/src/error-catalog.ts +0 -262
  146. package/src/event-queue.test.ts +0 -864
  147. package/src/event-queue.ts +0 -699
  148. package/src/event-subscriber.ts +0 -262
  149. package/src/event-testing.ts +0 -197
  150. package/src/event.test.ts +0 -1104
  151. package/src/event.ts +0 -988
  152. package/src/events-config.ts +0 -235
  153. package/src/exporters.ts +0 -165
  154. package/src/filtering-span-processor.test.ts +0 -281
  155. package/src/filtering-span-processor.ts +0 -111
  156. package/src/flatten-attributes.test.ts +0 -76
  157. package/src/flatten-attributes.ts +0 -80
  158. package/src/functional.strict-types.typecheck.ts +0 -53
  159. package/src/functional.test.ts +0 -1464
  160. package/src/functional.ts +0 -2539
  161. package/src/functional.types.test.ts +0 -135
  162. package/src/hook.mjs +0 -15
  163. package/src/http.test.ts +0 -485
  164. package/src/http.ts +0 -424
  165. package/src/index.ts +0 -433
  166. package/src/init-auto-redactor.test.ts +0 -53
  167. package/src/init-redactor.test.ts +0 -8
  168. package/src/init.customization.test.ts +0 -665
  169. package/src/init.integrations.test.ts +0 -399
  170. package/src/init.openllmetry.test.ts +0 -194
  171. package/src/init.protocol.test.ts +0 -215
  172. package/src/init.ts +0 -2439
  173. package/src/instrumentation.test.ts +0 -108
  174. package/src/instrumentation.ts +0 -319
  175. package/src/logger.test.ts +0 -125
  176. package/src/logger.ts +0 -341
  177. package/src/messaging-adapters.test.ts +0 -595
  178. package/src/messaging-adapters.ts +0 -583
  179. package/src/messaging-testing.test.ts +0 -573
  180. package/src/messaging-testing.ts +0 -935
  181. package/src/messaging.test.ts +0 -1646
  182. package/src/messaging.ts +0 -2245
  183. package/src/metric-helpers.ts +0 -47
  184. package/src/metric-testing.ts +0 -197
  185. package/src/metric.ts +0 -446
  186. package/src/metrics.test.ts +0 -241
  187. package/src/node-require.ts +0 -123
  188. package/src/operation-context.ts +0 -93
  189. package/src/parse-error.test.ts +0 -73
  190. package/src/parse-error.ts +0 -112
  191. package/src/posthog-logs.test.ts +0 -115
  192. package/src/posthog-logs.ts +0 -77
  193. package/src/pretty-console-exporter.test.ts +0 -545
  194. package/src/pretty-console-exporter.ts +0 -413
  195. package/src/pretty-log-formatter.test.ts +0 -123
  196. package/src/pretty-log-formatter.ts +0 -210
  197. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  198. package/src/processors/canonical-log-line-processor.ts +0 -396
  199. package/src/processors.ts +0 -152
  200. package/src/rate-limiter.test.ts +0 -199
  201. package/src/rate-limiter.ts +0 -98
  202. package/src/redact-values.test.ts +0 -90
  203. package/src/redact-values.ts +0 -34
  204. package/src/register.ts +0 -37
  205. package/src/request-logger.test.ts +0 -545
  206. package/src/request-logger.ts +0 -342
  207. package/src/sampling.test.ts +0 -1060
  208. package/src/sampling.ts +0 -737
  209. package/src/security-schema.test.ts +0 -45
  210. package/src/security-schema.ts +0 -107
  211. package/src/semantic-conventions.ts +0 -15
  212. package/src/semantic-helpers.test.ts +0 -226
  213. package/src/semantic-helpers.ts +0 -438
  214. package/src/shutdown.test.ts +0 -364
  215. package/src/shutdown.ts +0 -246
  216. package/src/span-name-normalizer.test.ts +0 -377
  217. package/src/span-name-normalizer.ts +0 -213
  218. package/src/stable-hash.ts +0 -27
  219. package/src/structured-error.test.ts +0 -191
  220. package/src/structured-error.ts +0 -157
  221. package/src/stub.integration.test.ts +0 -361
  222. package/src/tail-sampling-processor.test.ts +0 -230
  223. package/src/tail-sampling-processor.ts +0 -55
  224. package/src/test-span-collector.test.ts +0 -234
  225. package/src/test-span-collector.ts +0 -150
  226. package/src/testing.ts +0 -705
  227. package/src/trace-context.test.ts +0 -73
  228. package/src/trace-context.ts +0 -567
  229. package/src/trace-helpers.new.test.ts +0 -278
  230. package/src/trace-helpers.test.ts +0 -290
  231. package/src/trace-helpers.ts +0 -710
  232. package/src/trace-hybrid.test.ts +0 -42
  233. package/src/trace-hybrid.ts +0 -37
  234. package/src/tracer-provider.test.ts +0 -183
  235. package/src/tracer-provider.ts +0 -266
  236. package/src/track.test.ts +0 -154
  237. package/src/track.ts +0 -216
  238. package/src/validate.test.ts +0 -287
  239. package/src/validate.ts +0 -307
  240. package/src/validation-attributes.ts +0 -43
  241. package/src/validation.test.ts +0 -330
  242. package/src/validation.ts +0 -246
  243. package/src/variable-name-inference.test.ts +0 -178
  244. package/src/variable-name-inference.ts +0 -242
  245. package/src/webhook.test.ts +0 -649
  246. package/src/webhook.ts +0 -637
  247. package/src/workflow-distributed.test.ts +0 -786
  248. package/src/workflow-distributed.ts +0 -916
  249. package/src/workflow.async-safety.integration.test.ts +0 -345
  250. package/src/workflow.test.ts +0 -647
  251. package/src/workflow.ts +0 -810
  252. package/src/yaml-config.test.ts +0 -373
  253. 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
- }