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/sampling.ts DELETED
@@ -1,737 +0,0 @@
1
- /**
2
- * Sampling Strategies
3
- *
4
- * Provides intelligent sampling beyond simple random rates.
5
- * Helps reduce telemetry costs while capturing critical data.
6
- *
7
- * Key strategies:
8
- * - Always trace errors and slow requests (critical for debugging)
9
- * - Sample by user ID for consistent request tracing
10
- * - Adaptive sampling based on load
11
- * - Sample by feature flags for A/B testing correlation
12
- *
13
- * @example
14
- * ```typescript
15
- * import { AlwaysOnErrorSampler, UserIdSampler } from './sampling'
16
- *
17
- * @Instrumented({
18
- * serviceName: 'user',
19
- * sampler: new AlwaysOnErrorSampler(0.1) // 10% baseline, 100% on errors
20
- * })
21
- * class UserService { }
22
- * ```
23
- */
24
-
25
- import type { Link, Attributes } from '@opentelemetry/api';
26
- import { TraceFlags } from '@opentelemetry/api';
27
- import { type Logger } from './logger';
28
-
29
- /**
30
- * Tail sampling attribute keys (autotel-internal, not OTel semconv)
31
- */
32
- export const AUTOTEL_SAMPLING_TAIL_KEEP = 'autotel.sampling.tail.keep';
33
- export const AUTOTEL_SAMPLING_TAIL_EVALUATED =
34
- 'autotel.sampling.tail.evaluated';
35
-
36
- /**
37
- * Sampler interface - return true to trace, false to skip
38
- */
39
- export interface Sampler {
40
- /**
41
- * Decide whether to trace this operation
42
- *
43
- * @param context - Sampling context
44
- * @returns true to trace, false to skip
45
- */
46
- shouldSample(context: SamplingContext): boolean;
47
-
48
- /**
49
- * Whether this sampler needs tail sampling (post-execution decision)
50
- * If true, spans are always created and shouldKeepTrace() is called after execution
51
- *
52
- * @returns true if this sampler needs to evaluate after operation completes
53
- */
54
- needsTailSampling?(): boolean;
55
-
56
- /**
57
- * Re-evaluate sampling decision after operation completes (tail sampling)
58
- * Only called if needsTailSampling() returns true
59
- *
60
- * @param context - Sampling context
61
- * @param result - Operation result
62
- * @returns true if this trace should be kept, false to drop it
63
- */
64
- shouldKeepTrace?(context: SamplingContext, result: OperationResult): boolean;
65
- }
66
-
67
- /**
68
- * Context information for sampling decisions
69
- */
70
- export interface SamplingContext {
71
- /** Operation name */
72
- operationName: string;
73
- /** Method arguments (for extracting user IDs, etc.) */
74
- args: unknown[];
75
- /** Optional metadata (e.g., feature flags, request headers) */
76
- metadata?: Record<string, unknown>;
77
- /** Optional span links for links-based sampling */
78
- links?: Link[];
79
- }
80
-
81
- /**
82
- * Result of a trace operation (for post-execution sampling)
83
- */
84
- export interface OperationResult {
85
- /** Whether the operation succeeded */
86
- success: boolean;
87
- /** Duration in milliseconds */
88
- duration: number;
89
- /** Error if operation failed */
90
- error?: Error;
91
- }
92
-
93
- /**
94
- * Simple random sampler
95
- *
96
- * @example
97
- * ```typescript
98
- * new RandomSampler(0.1) // Sample 10% of requests
99
- * ```
100
- */
101
- export class RandomSampler implements Sampler {
102
- constructor(private readonly sampleRate: number) {
103
- if (sampleRate < 0 || sampleRate > 1) {
104
- throw new Error('Sample rate must be between 0 and 1');
105
- }
106
- }
107
-
108
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
109
- shouldSample(_context: SamplingContext): boolean {
110
- return Math.random() < this.sampleRate;
111
- }
112
- }
113
-
114
- /**
115
- * Always sample (100% tracing)
116
- */
117
- export class AlwaysSampler implements Sampler {
118
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
- shouldSample(_context: SamplingContext): boolean {
120
- return true;
121
- }
122
- }
123
-
124
- /**
125
- * Never sample (0% tracing)
126
- */
127
- export class NeverSampler implements Sampler {
128
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
129
- shouldSample(_context: SamplingContext): boolean {
130
- return false;
131
- }
132
- }
133
-
134
- /**
135
- * Adaptive sampler that always traces errors and slow requests
136
- *
137
- * This is the recommended sampler for production use.
138
- * It ensures you never miss critical issues while keeping costs down.
139
- *
140
- * Strategy:
141
- * - Always trace errors (critical for debugging)
142
- * - Always trace slow requests (performance issues)
143
- * - Use baseline sample rate for successful fast requests
144
- *
145
- * **IMPORTANT - Tail Sampling Requirement:**
146
- * This sampler uses tail sampling (makes decisions AFTER execution).
147
- * You MUST use TailSamplingSpanProcessor for it to work correctly:
148
- *
149
- * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
150
- * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
151
- *
152
- * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
153
- *
154
- * @see TailSamplingSpanProcessor
155
- * @see README.md "Tail Sampling with Custom Providers" section
156
- *
157
- * @example
158
- * ```typescript
159
- * new AdaptiveSampler({
160
- * baselineSampleRate: 0.1, // 10% of normal requests
161
- * slowThresholdMs: 1000, // Requests > 1s are "slow"
162
- * alwaysSampleErrors: true, // Always trace errors
163
- * alwaysSampleSlow: true // Always trace slow requests
164
- * })
165
- * ```
166
- */
167
- export class AdaptiveSampler implements Sampler {
168
- private baselineSampleRate: number;
169
- private slowThresholdMs: number;
170
- private alwaysSampleErrors: boolean;
171
- private alwaysSampleSlow: boolean;
172
- private linksBased: boolean;
173
- private linksRate: number;
174
- private logger?: Logger;
175
-
176
- // Track whether we should sample this request
177
- private readonly samplingDecisions = new WeakMap<unknown[], boolean>();
178
- // Track operation results to enable post-execution decision
179
- private readonly operationResults = new WeakMap<unknown[], OperationResult>();
180
-
181
- constructor(
182
- options: {
183
- baselineSampleRate?: number;
184
- slowThresholdMs?: number;
185
- alwaysSampleErrors?: boolean;
186
- alwaysSampleSlow?: boolean;
187
- /** Enable links-based sampling for event-driven architectures */
188
- linksBased?: boolean;
189
- /** Sampling rate for spans linked to sampled spans (0.0-1.0) */
190
- linksRate?: number;
191
- logger?: Logger;
192
- } = {},
193
- ) {
194
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
195
- this.slowThresholdMs = options.slowThresholdMs ?? 1000;
196
- this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
197
- this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
198
- this.linksBased = options.linksBased ?? false;
199
- this.linksRate = options.linksRate ?? 1;
200
- this.logger = options.logger;
201
-
202
- if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) {
203
- throw new Error('Baseline sample rate must be between 0 and 1');
204
- }
205
- if (this.linksRate < 0 || this.linksRate > 1) {
206
- throw new Error('Links rate must be between 0 and 1');
207
- }
208
- }
209
-
210
- needsTailSampling(): boolean {
211
- // AdaptiveSampler ALWAYS needs tail sampling to implement error/slow capture
212
- return true;
213
- }
214
-
215
- shouldSample(context: SamplingContext): boolean {
216
- // For tail sampling, we optimistically create spans for all requests
217
- // The real decision happens in shouldKeepTrace() after execution
218
- // We still store the baseline decision for shouldKeepTrace() to use
219
- const baselineDecision = Math.random() < this.baselineSampleRate;
220
- this.samplingDecisions.set(context.args, baselineDecision);
221
-
222
- // Always return true to create the span (tail sampling will decide if we keep it)
223
- return true;
224
- }
225
-
226
- /**
227
- * Check if any links point to sampled spans.
228
- *
229
- * A span is considered linked to a sampled span if any of its links
230
- * have trace_flags with the sampled bit set (0x01).
231
- *
232
- * @param links - Array of span links to check
233
- * @returns true if any linked span is sampled, false otherwise
234
- */
235
- hasSampledLink(links: Link[]): boolean {
236
- if (!links || links.length === 0) {
237
- return false;
238
- }
239
- return links.some(
240
- (link) =>
241
- link.context && (link.context.traceFlags & TraceFlags.SAMPLED) !== 0,
242
- );
243
- }
244
-
245
- /**
246
- * Re-evaluate sampling decision after operation completes
247
- *
248
- * This allows us to always capture errors and slow requests,
249
- * even if they weren't initially sampled.
250
- *
251
- * @param context - Sampling context
252
- * @param result - Operation result
253
- * @returns true if this operation should be kept (not discarded)
254
- */
255
- shouldKeepTrace(context: SamplingContext, result: OperationResult): boolean {
256
- const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
257
-
258
- // Always keep errors
259
- if (this.alwaysSampleErrors && !result.success) {
260
- if (!baselineDecision) {
261
- this.logger?.debug(
262
- {
263
- operation: context.operationName,
264
- error: result.error?.message,
265
- },
266
- 'Adaptive sampling: Keeping error trace',
267
- );
268
- }
269
- return true;
270
- }
271
-
272
- // Always keep slow requests
273
- if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
274
- if (!baselineDecision) {
275
- this.logger?.debug(
276
- {
277
- operation: context.operationName,
278
- duration: result.duration,
279
- },
280
- 'Adaptive sampling: Keeping slow trace',
281
- );
282
- }
283
- return true;
284
- }
285
-
286
- // Check for sampled links (links-based sampling for event-driven systems)
287
- if (
288
- this.linksBased &&
289
- context.links &&
290
- this.hasSampledLink(context.links)
291
- ) {
292
- // Use linksRate to decide whether to keep the linked span
293
- const keepLinked = Math.random() < this.linksRate;
294
- if (keepLinked && !baselineDecision) {
295
- this.logger?.debug(
296
- {
297
- operation: context.operationName,
298
- linkCount: context.links.length,
299
- },
300
- 'Adaptive sampling: Keeping trace due to sampled link',
301
- );
302
- }
303
- return keepLinked;
304
- }
305
-
306
- // Otherwise, use baseline decision
307
- return baselineDecision;
308
- }
309
- }
310
-
311
- /**
312
- * User-based sampler for consistent tracing
313
- *
314
- * Always samples requests from specific user IDs.
315
- * Useful for debugging specific user issues or monitoring VIP users.
316
- *
317
- * @example
318
- * ```typescript
319
- * new UserIdSampler({
320
- * baselineSampleRate: 0.01, // 1% of normal users
321
- * alwaysSampleUsers: ['vip_123'], // Always trace VIP users
322
- * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
323
- * })
324
- * ```
325
- */
326
- export class UserIdSampler implements Sampler {
327
- private baselineSampleRate: number;
328
- private alwaysSampleUsers: Set<string>;
329
- private extractUserId: (args: unknown[]) => string | undefined;
330
- private logger?: Logger;
331
-
332
- constructor(options: {
333
- baselineSampleRate?: number;
334
- alwaysSampleUsers?: string[];
335
- extractUserId: (args: unknown[]) => string | undefined;
336
- logger?: Logger;
337
- }) {
338
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
339
- this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
340
- this.extractUserId = options.extractUserId;
341
- this.logger = options.logger;
342
- }
343
-
344
- shouldSample(context: SamplingContext): boolean {
345
- const userId = this.extractUserId(context.args);
346
-
347
- // Always sample specific users
348
- if (userId && this.alwaysSampleUsers.has(userId)) {
349
- this.logger?.debug(
350
- {
351
- operation: context.operationName,
352
- userId,
353
- },
354
- 'Sampling user request',
355
- );
356
- return true;
357
- }
358
-
359
- // For consistent per-user sampling, hash the user ID
360
- if (userId) {
361
- const hash = this.hashString(userId);
362
- return hash < this.baselineSampleRate;
363
- }
364
-
365
- // Fallback to random sampling if no user ID
366
- return Math.random() < this.baselineSampleRate;
367
- }
368
-
369
- /**
370
- * Add user IDs to always-sample list
371
- */
372
- addAlwaysSampleUsers(...userIds: string[]): void {
373
- for (const userId of userIds) {
374
- this.alwaysSampleUsers.add(userId);
375
- }
376
- }
377
-
378
- /**
379
- * Remove user IDs from always-sample list
380
- */
381
- removeAlwaysSampleUsers(...userIds: string[]): void {
382
- for (const userId of userIds) {
383
- this.alwaysSampleUsers.delete(userId);
384
- }
385
- }
386
-
387
- /**
388
- * Simple hash function for consistent user sampling
389
- */
390
- private hashString(str: string): number {
391
- let hash = 0;
392
- for (let i = 0; i < str.length; i++) {
393
- const char = str.codePointAt(i) ?? 0;
394
- hash = (hash << 5) - hash + char;
395
- hash = hash & hash; // Convert to 32-bit integer
396
- }
397
- return Math.abs(hash) / 2_147_483_647; // Normalize to 0-1
398
- }
399
- }
400
-
401
- /**
402
- * Composite sampler that combines multiple samplers
403
- *
404
- * Samples if ANY of the child samplers returns true.
405
- *
406
- * @example
407
- * ```typescript
408
- * new CompositeSampler([
409
- * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
410
- * new AdaptiveSampler({ baselineSampleRate: 0.1 })
411
- * ])
412
- * ```
413
- */
414
- export class CompositeSampler implements Sampler {
415
- constructor(private readonly samplers: Sampler[]) {
416
- if (samplers.length === 0) {
417
- throw new Error('CompositeSampler requires at least one child sampler');
418
- }
419
- }
420
-
421
- shouldSample(context: SamplingContext): boolean {
422
- return this.samplers.some((sampler) => sampler.shouldSample(context));
423
- }
424
- }
425
-
426
- /**
427
- * Feature flag sampler
428
- *
429
- * Always samples requests with specific feature flags enabled.
430
- * Perfect for correlating A/B test experiments with metrics.
431
- *
432
- * @example
433
- * ```typescript
434
- * new FeatureFlagSampler({
435
- * baselineSampleRate: 0.01,
436
- * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
437
- * extractFlags: (args, metadata) => metadata?.featureFlags
438
- * })
439
- * ```
440
- */
441
- export class FeatureFlagSampler implements Sampler {
442
- private baselineSampleRate: number;
443
- private alwaysSampleFlags: Set<string>;
444
- private extractFlags: (
445
- args: unknown[],
446
- metadata?: Record<string, unknown>,
447
- ) => string[] | undefined;
448
- private logger?: Logger;
449
-
450
- constructor(options: {
451
- baselineSampleRate?: number;
452
- alwaysSampleFlags?: string[];
453
- extractFlags: (
454
- args: unknown[],
455
- metadata?: Record<string, unknown>,
456
- ) => string[] | undefined;
457
- logger?: Logger;
458
- }) {
459
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
460
- this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
461
- this.extractFlags = options.extractFlags;
462
- this.logger = options.logger;
463
- }
464
-
465
- shouldSample(context: SamplingContext): boolean {
466
- const flags = this.extractFlags(context.args, context.metadata);
467
-
468
- // Always sample if any monitored flag is enabled
469
- if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
470
- this.logger?.debug(
471
- {
472
- operation: context.operationName,
473
- flags,
474
- },
475
- 'Sampling feature flag request',
476
- );
477
- return true;
478
- }
479
-
480
- // Fallback to random sampling
481
- return Math.random() < this.baselineSampleRate;
482
- }
483
-
484
- /**
485
- * Add feature flags to always-sample list
486
- */
487
- addAlwaysSampleFlags(...flags: string[]): void {
488
- for (const flag of flags) {
489
- this.alwaysSampleFlags.add(flag);
490
- }
491
- }
492
-
493
- /**
494
- * Remove feature flags from always-sample list
495
- */
496
- removeAlwaysSampleFlags(...flags: string[]): void {
497
- for (const flag of flags) {
498
- this.alwaysSampleFlags.delete(flag);
499
- }
500
- }
501
- }
502
-
503
- // ============================================================================
504
- // Sampling Presets
505
- // ============================================================================
506
-
507
- /**
508
- * Named sampling presets for common environments.
509
- * Use with `init({ sampling: 'production' })` or directly via factories.
510
- */
511
- export type SamplingPreset =
512
- | 'development'
513
- | 'errors-only'
514
- | 'production'
515
- | 'off';
516
-
517
- /**
518
- * Sampling preset factories.
519
- *
520
- * For most users, the string shorthand on `init()` is simpler:
521
- * ```typescript
522
- * init({ service: 'my-app', sampling: 'production' })
523
- * ```
524
- *
525
- * Use factories when you need to customize:
526
- * ```typescript
527
- * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
528
- * ```
529
- */
530
- export const samplingPresets = {
531
- /** Capture everything — best for local development and debugging */
532
- development: () => new AlwaysSampler(),
533
-
534
- /** Only bad outcomes — zero baseline, errors always kept */
535
- errorsOnly: () =>
536
- new AdaptiveSampler({
537
- baselineSampleRate: 0,
538
- alwaysSampleErrors: true,
539
- }),
540
-
541
- /**
542
- * Balanced production defaults — 10% baseline + errors + slow traces.
543
- * Pass overrides to tune (uses the same option names as AdaptiveSampler).
544
- */
545
- production: (overrides?: {
546
- baselineSampleRate?: number;
547
- slowThresholdMs?: number;
548
- alwaysSampleErrors?: boolean;
549
- alwaysSampleSlow?: boolean;
550
- }) =>
551
- new AdaptiveSampler({
552
- baselineSampleRate: 0.1,
553
- alwaysSampleErrors: true,
554
- alwaysSampleSlow: true,
555
- slowThresholdMs: 1000,
556
- ...overrides,
557
- }),
558
-
559
- /** Disable sampling entirely */
560
- off: () => new NeverSampler(),
561
- };
562
-
563
- /**
564
- * Resolve a preset string to a Sampler instance.
565
- * Used internally by `init()` when `sampling` string is provided.
566
- *
567
- * @throws Error if preset is not recognized
568
- */
569
- export function resolveSamplingPreset(preset: SamplingPreset): Sampler {
570
- switch (preset) {
571
- case 'development':
572
- return samplingPresets.development();
573
- case 'errors-only':
574
- return samplingPresets.errorsOnly();
575
- case 'production':
576
- return samplingPresets.production();
577
- case 'off':
578
- return samplingPresets.off();
579
- default:
580
- throw new Error(
581
- `Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`,
582
- );
583
- }
584
- }
585
-
586
- // ============================================================================
587
- // Link Helper Functions
588
- // ============================================================================
589
-
590
- /**
591
- * Create a Link from W3C trace context headers (e.g., from a message queue).
592
- *
593
- * This is useful for message consumers that need to link to the producer span.
594
- * The headers should contain at least a `traceparent` header in W3C format.
595
- *
596
- * @param headers - Dictionary containing traceparent/tracestate headers
597
- * @param attributes - Optional attributes for the link
598
- * @returns Link object if context could be extracted, null otherwise
599
- *
600
- * @example
601
- * ```typescript
602
- * // In a Kafka consumer
603
- * const headers = { traceparent: '00-abc123...-def456...-01' };
604
- * const link = createLinkFromHeaders(headers);
605
- * if (link) {
606
- * // Use with tracer.startActiveSpan options or ctx.addLink()
607
- * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
608
- * }
609
- * ```
610
- */
611
- export function createLinkFromHeaders(
612
- headers: Record<string, string>,
613
- attributes?: Attributes,
614
- ): Link | null {
615
- // Parse W3C traceparent header directly for reliability
616
- // Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
617
- const traceparent = headers.traceparent || headers['traceparent'];
618
- if (!traceparent) {
619
- return null;
620
- }
621
-
622
- const spanContext = parseTraceparent(traceparent);
623
- if (!spanContext || !isValidSpanContext(spanContext)) {
624
- return null;
625
- }
626
-
627
- return {
628
- context: spanContext,
629
- attributes: attributes ?? {},
630
- };
631
- }
632
-
633
- /**
634
- * Extract Links from a batch of messages for fan-in scenarios.
635
- *
636
- * Useful for batch processing where multiple producer spans should be linked.
637
- * This enables tracing causality in event-driven architectures where a single
638
- * consumer processes messages from multiple producers.
639
- *
640
- * @param messages - List of message objects
641
- * @param headersKey - Key in each message containing trace headers (default: 'headers')
642
- * @returns List of Link objects for all valid trace contexts
643
- *
644
- * @example
645
- * ```typescript
646
- * // Processing a batch of SQS/Kafka messages
647
- * const messages = [
648
- * { body: '...', headers: { traceparent: '...' } },
649
- * { body: '...', headers: { traceparent: '...' } },
650
- * ];
651
- * const links = extractLinksFromBatch(messages);
652
- *
653
- * tracer.startActiveSpan('process.batch', { links }, span => {
654
- * for (const msg of messages) {
655
- * processMessage(msg);
656
- * }
657
- * });
658
- * ```
659
- */
660
- export function extractLinksFromBatch(
661
- messages: Array<{ [key: string]: unknown }>,
662
- headersKey: string = 'headers',
663
- ): Link[] {
664
- const links: Link[] = [];
665
-
666
- for (const msg of messages) {
667
- const msgHeaders = msg[headersKey];
668
- if (msgHeaders && typeof msgHeaders === 'object' && msgHeaders !== null) {
669
- const link = createLinkFromHeaders(msgHeaders as Record<string, string>, {
670
- 'messaging.batch.message_index': links.length,
671
- });
672
- if (link) {
673
- links.push(link);
674
- }
675
- }
676
- }
677
-
678
- return links;
679
- }
680
-
681
- /**
682
- * Parse W3C traceparent header into SpanContext
683
- * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
684
- *
685
- * @see https://www.w3.org/TR/trace-context/#traceparent-header
686
- */
687
- function parseTraceparent(
688
- traceparent: string,
689
- ): import('@opentelemetry/api').SpanContext | null {
690
- // W3C traceparent format: version-traceId-parentId-traceFlags
691
- // Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
692
- const TRACEPARENT_REGEX =
693
- /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
694
-
695
- const match = traceparent.match(TRACEPARENT_REGEX);
696
- if (!match || match.length < 5) {
697
- return null;
698
- }
699
-
700
- const version = match[1];
701
- const traceId = match[2];
702
- const spanId = match[3];
703
- const flags = match[4];
704
-
705
- // Validate all parts are present (TypeScript narrowing)
706
- if (!version || !traceId || !spanId || !flags) {
707
- return null;
708
- }
709
-
710
- // Version 00 is currently the only version, but we should be forward compatible
711
- if (version === 'ff') {
712
- // Version ff is invalid according to spec
713
- return null;
714
- }
715
-
716
- return {
717
- traceId,
718
- spanId,
719
- traceFlags: Number.parseInt(flags, 16),
720
- isRemote: true,
721
- };
722
- }
723
-
724
- /**
725
- * Check if a SpanContext is valid (has non-zero trace and span IDs)
726
- */
727
- function isValidSpanContext(
728
- spanContext: import('@opentelemetry/api').SpanContext | null,
729
- ): spanContext is import('@opentelemetry/api').SpanContext {
730
- if (!spanContext) return false;
731
- // TraceId should not be all zeros (00000000000000000000000000000000)
732
- // SpanId should not be all zeros (0000000000000000)
733
- return (
734
- spanContext.traceId !== '00000000000000000000000000000000' &&
735
- spanContext.spanId !== '0000000000000000'
736
- );
737
- }