autotel 7.1.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +41 -14
  2. package/dist/analysis.cjs +17 -0
  3. package/dist/analysis.d.cts +9 -1
  4. package/dist/analysis.d.ts +9 -1
  5. package/dist/analysis.js +17 -1
  6. package/dist/auto.cjs +2 -3
  7. package/dist/auto.js +1 -2
  8. package/dist/correlation-id.cjs +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.d.cts +1 -1
  12. package/dist/decorators.d.ts +1 -1
  13. package/dist/decorators.js +1 -1
  14. package/dist/event.cjs +1 -1
  15. package/dist/event.js +1 -1
  16. package/dist/{functional-Cub1ku-C.js → functional-C4js2vTM.js} +44 -7
  17. package/dist/{functional-CJn2zY4o.d.cts → functional-CgWcRXGX.d.cts} +12 -5
  18. package/dist/{functional-BhhP4Er_.cjs → functional-DHlD7V0x.cjs} +47 -10
  19. package/dist/{functional-DXy1cHFF.d.ts → functional-RFFpQzAV.d.ts} +12 -5
  20. package/dist/functional.cjs +1 -1
  21. package/dist/functional.d.cts +1 -1
  22. package/dist/functional.d.ts +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 +53 -15
  27. package/dist/index.d.cts +24 -4
  28. package/dist/index.d.ts +24 -4
  29. package/dist/index.js +40 -5
  30. package/dist/{init-DrlL1eis.d.cts → init-8GSy2bza.d.cts} +1 -1
  31. package/dist/{init-DDyaLzeg.js → init-CsKK9vZO.js} +2196 -1298
  32. package/dist/{init-C5aHRqi2.cjs → init-DmM7dprX.cjs} +2356 -1300
  33. package/dist/{init-BgnYi2sm.d.ts → init-UnKrXhpq.d.ts} +1 -1
  34. package/dist/instrumentation.cjs +2 -3
  35. package/dist/instrumentation.js +1 -2
  36. package/dist/messaging.cjs +5 -5
  37. package/dist/messaging.js +2 -2
  38. package/dist/{sampling-CM5S2Lp3.d.ts → sampling-DBng4Foa.d.ts} +20 -1
  39. package/dist/{sampling-CmRdnXUi.d.cts → sampling-Er217Mge.d.cts} +20 -1
  40. package/dist/sampling.cjs +22 -601
  41. package/dist/sampling.d.cts +2 -2
  42. package/dist/sampling.d.ts +2 -2
  43. package/dist/sampling.js +2 -585
  44. package/dist/semantic-helpers.cjs +1 -1
  45. package/dist/semantic-helpers.js +1 -1
  46. package/dist/tail-sampling-processor.cjs +2 -26
  47. package/dist/tail-sampling-processor.js +1 -25
  48. package/dist/testing.cjs +32 -0
  49. package/dist/testing.d.cts +26 -1
  50. package/dist/testing.d.ts +26 -1
  51. package/dist/testing.js +32 -1
  52. package/dist/webhook.cjs +1 -1
  53. package/dist/webhook.js +1 -1
  54. package/dist/workflow-distributed.cjs +1 -1
  55. package/dist/workflow-distributed.js +1 -1
  56. package/dist/workflow.cjs +1 -1
  57. package/dist/workflow.js +1 -1
  58. package/dist/yaml-config.cjs +4 -248
  59. package/dist/yaml-config.d.cts +2 -2
  60. package/dist/yaml-config.d.ts +2 -2
  61. package/dist/yaml-config.js +1 -242
  62. package/package.json +1 -1
@@ -2,695 +2,1049 @@ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
2
  const require_values = require('./values-DApyq53y.cjs');
3
3
  const require_tracer_provider = require('./tracer-provider.cjs');
4
4
  const require_config = require('./config.cjs');
5
- const require_sampling = require('./sampling.cjs');
5
+ const require_structured_error = require('./structured-error--_wXpd_C.cjs');
6
6
  const require_node_require = require('./node-require-EifOMikz.cjs');
7
- const require_tail_sampling_processor = require('./tail-sampling-processor.cjs');
8
7
  const require_filtering_span_processor = require('./filtering-span-processor.cjs');
9
8
  const require_policy = require('./policy.cjs');
10
9
  const require_span_name_normalizer = require('./span-name-normalizer.cjs');
11
10
  const require_attribute_redacting_processor = require('./attribute-redacting-processor.cjs');
12
11
  const require_pretty_console_exporter = require('./pretty-console-exporter-CMzlrRNg.cjs');
13
- const require_yaml_config = require('./yaml-config.cjs');
14
12
  const require_canonical_log_line_processor = require('./canonical-log-line-processor-DolzdawH.cjs');
15
- const require_structured_error = require('./structured-error--_wXpd_C.cjs');
16
13
  const require_metric = require('./metric.cjs');
17
14
  let _opentelemetry_sdk_node = require("@opentelemetry/sdk-node");
18
15
  let _opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
19
16
  let _opentelemetry_resources = require("@opentelemetry/resources");
20
17
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
21
18
  let _opentelemetry_api = require("@opentelemetry/api");
19
+ let node_async_hooks = require("node:async_hooks");
20
+ node_async_hooks = require_rolldown_runtime.__toESM(node_async_hooks, 1);
22
21
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
23
22
  let _opentelemetry_sdk_logs = require("@opentelemetry/sdk-logs");
23
+ let node_fs = require("node:fs");
24
+ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
25
+ let node_path = require("node:path");
26
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
24
27
  let node_os = require("node:os");
25
- let node_async_hooks = require("node:async_hooks");
26
- node_async_hooks = require_rolldown_runtime.__toESM(node_async_hooks, 1);
27
28
  let _opentelemetry_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
28
29
  let _opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
29
30
  let _opentelemetry_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
30
31
 
31
- //#region src/init-logger.ts
32
- const LOG_LEVELS = {
33
- debug: 0,
34
- info: 1,
35
- warn: 2,
36
- error: 3
37
- };
38
- /** Silent logger used until and unless the application opts into diagnostics. */
39
- const silentLogger = {
40
- info: () => {},
41
- warn: () => {},
42
- error: () => {},
43
- debug: () => {}
44
- };
45
- /** Apply the resolved silence and minimum-level settings to a logger. */
46
- function wrapLogger(base, silent, minLevel) {
47
- if (silent) return silentLogger;
48
- const threshold = LOG_LEVELS[minLevel];
49
- const wrap = (fn, level) => {
50
- if (LOG_LEVELS[level] < threshold) return (() => {});
51
- return ((...args) => fn(...args));
52
- };
53
- return {
54
- debug: wrap(base.debug, "debug"),
55
- info: wrap(base.info, "info"),
56
- warn: wrap(base.warn, "warn"),
57
- error: wrap(base.error, "error")
58
- };
59
- }
60
-
61
- //#endregion
62
- //#region src/posthog-logs.ts
63
- var RedactingLogRecordProcessor = class {
64
- wrapped;
65
- redact;
66
- constructor(wrapped, redact) {
67
- this.wrapped = wrapped;
68
- this.redact = redact;
32
+ //#region src/rate-limiter.ts
33
+ /**
34
+ * Token bucket rate limiter
35
+ *
36
+ * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
37
+ * Thread-safe for async operations.
38
+ */
39
+ var TokenBucketRateLimiter = class {
40
+ tokens;
41
+ maxTokens;
42
+ refillRate;
43
+ lastRefill;
44
+ constructor(config) {
45
+ this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
46
+ this.tokens = this.maxTokens;
47
+ this.refillRate = config.maxEventsPerSecond / 1e3;
48
+ this.lastRefill = Date.now();
69
49
  }
70
- onEmit(logRecord, context) {
71
- const body = require_values.asString(logRecord.body);
72
- if (body) logRecord.body = this.redact(body);
73
- if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
74
- const text = require_values.asString(value);
75
- if (text !== void 0) logRecord.attributes[key] = this.redact(text);
76
- else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
77
- const entry = require_values.asString(item);
78
- return entry === void 0 ? item : this.redact(entry);
79
- });
50
+ /**
51
+ * Try to consume a token (allow an event)
52
+ * Returns true if allowed, false if rate limit exceeded
53
+ */
54
+ tryConsume(count = 1) {
55
+ this.refill();
56
+ if (this.tokens >= count) {
57
+ this.tokens -= count;
58
+ return true;
80
59
  }
81
- this.wrapped.onEmit(logRecord, context);
60
+ return false;
82
61
  }
83
- shutdown() {
84
- return this.wrapped.shutdown();
62
+ /**
63
+ * Wait until a token is available (async rate limiting)
64
+ * Returns a promise that resolves when the event can be processed
65
+ */
66
+ async waitForToken(count = 1) {
67
+ this.refill();
68
+ if (this.tokens >= count) {
69
+ this.tokens -= count;
70
+ return;
71
+ }
72
+ const tokensNeeded = count - this.tokens;
73
+ const waitMs = Math.ceil(tokensNeeded / this.refillRate);
74
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
75
+ return this.waitForToken(count);
85
76
  }
86
- forceFlush() {
87
- return this.wrapped.forceFlush();
77
+ /**
78
+ * Refill tokens based on elapsed time
79
+ */
80
+ refill() {
81
+ const now = Date.now();
82
+ const tokensToAdd = (now - this.lastRefill) * this.refillRate;
83
+ this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
84
+ this.lastRefill = now;
85
+ }
86
+ /**
87
+ * Get current available tokens (for testing/debugging)
88
+ */
89
+ getAvailableTokens() {
90
+ this.refill();
91
+ return Math.floor(this.tokens);
92
+ }
93
+ /**
94
+ * Reset the rate limiter (for testing)
95
+ */
96
+ reset() {
97
+ this.tokens = this.maxTokens;
98
+ this.lastRefill = Date.now();
88
99
  }
89
100
  };
90
- /**
91
- * Build log record processors for PostHog OTLP logs integration.
92
- *
93
- * Resolution order:
94
- * 1. config.url if provided
95
- * 2. POSTHOG_LOGS_URL env var
96
- * 3. Empty array (disabled)
97
- */
98
- function buildPostHogLogProcessors(config, stringRedactor) {
99
- const url = config?.url || process.env.POSTHOG_LOGS_URL;
100
- if (!url) return [];
101
- const sdkLogs = require_node_require.safeRequire("@opentelemetry/sdk-logs");
102
- const exporterModule = require_node_require.safeRequire("@opentelemetry/exporter-logs-otlp-http");
103
- if (!sdkLogs || !exporterModule) return [];
104
- const exporter = new exporterModule.OTLPLogExporter({ url });
105
- let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
106
- if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
107
- return [processor];
108
- }
109
101
 
110
102
  //#endregion
111
- //#region src/baggage-span-processor.ts
103
+ //#region src/correlation-id.ts
112
104
  /**
113
- * Span processor that automatically copies baggage entries to span attributes
105
+ * Correlation ID utilities for event-driven observability
114
106
  *
115
- * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
116
- * without manually calling ctx.setAttribute() for each baggage entry.
107
+ * Provides a stable join key across events, logs, and spans even when traces fragment.
108
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe.
117
109
  *
118
- * @example Enable in init()
110
+ * Lifecycle:
111
+ * 1. Generated at boundary root (HTTP server span, message process span, cron job span)
112
+ * 2. Reused within context (nested work shares it via AsyncLocalStorage)
113
+ * 3. Propagated via baggage (optional, default OFF to avoid header bloat)
114
+ *
115
+ * @example Basic usage
119
116
  * ```typescript
120
- * init({
121
- * service: 'my-app',
122
- * baggage: true // Uses default 'baggage.' prefix
123
- * });
117
+ * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
124
118
  *
125
- * // Now baggage automatically appears as span attributes
126
- * await withBaggage({
127
- * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
128
- * fn: async () => {
129
- * // Span has baggage.tenant.id and baggage.user.id attributes!
130
- * }
131
- * });
132
- * ```
119
+ * // Generate a new correlation ID
120
+ * const id = generateCorrelationId();
121
+ * // Returns: 'a1b2c3d4e5f67890'
133
122
  *
134
- * @example Custom prefix
135
- * ```typescript
136
- * init({
137
- * service: 'my-app',
138
- * baggage: 'ctx' // Uses 'ctx.' prefix
139
- * });
140
- * // Creates attributes: ctx.tenant.id, ctx.user.id
123
+ * // Get current correlation ID from context
124
+ * const currentId = getCorrelationId();
141
125
  * ```
142
126
  */
143
- var BaggageSpanProcessor = class {
144
- prefix;
145
- constructor(options = {}) {
146
- this.prefix = options.prefix ?? "baggage.";
147
- }
148
- onStart(span, parentContext) {
149
- let baggage = _opentelemetry_api.propagation.getBaggage(parentContext);
150
- if (!baggage) baggage = _opentelemetry_api.propagation.getBaggage(_opentelemetry_api.context.active());
151
- if (!baggage) try {
152
- const { getActiveContextWithBaggage } = require_node_require.requireModule("./trace-context");
153
- const storedContext = getActiveContextWithBaggage();
154
- baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
155
- } catch {}
156
- if (!baggage) return;
157
- for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
158
- }
159
- onEnd(_span) {}
160
- async shutdown() {}
161
- async forceFlush() {}
162
- };
163
-
164
- //#endregion
165
- //#region src/redact-values.ts
166
- /** Standalone string redaction for use outside the span processor pipeline. */
167
- function createStringRedactor(config) {
168
- const resolved = typeof config === "string" ? require_attribute_redacting_processor.REDACTOR_PRESETS[config] : config;
169
- const valuePatterns = resolved.valuePatterns ?? [];
170
- const defaultReplacement = resolved.replacement ?? "[REDACTED]";
171
- return (value) => {
172
- let result = value;
173
- for (const { pattern, replacement, mask } of valuePatterns) {
174
- pattern.lastIndex = 0;
175
- result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
176
- }
177
- return result;
178
- };
179
- }
180
-
181
- //#endregion
182
- //#region src/env-config.ts
183
127
  /**
184
- * Validate URL format
128
+ * AsyncLocalStorage for storing correlation ID
129
+ * This allows correlation IDs to persist across async boundaries
185
130
  */
186
- function isValidUrl(urlString) {
187
- try {
188
- const url = new URL(urlString);
189
- return url.protocol === "http:" || url.protocol === "https:";
190
- } catch {
191
- return false;
192
- }
193
- }
131
+ const correlationStorage = new node_async_hooks.AsyncLocalStorage();
194
132
  /**
195
- * Resolve OpenTelemetry environment variables from process.env
133
+ * Baggage key for correlation ID propagation
196
134
  */
197
- function resolveOtelEnv() {
198
- const env = {};
199
- if (process.env.OTEL_SERVICE_NAME) {
200
- const value = process.env.OTEL_SERVICE_NAME.trim();
201
- if (value) env.OTEL_SERVICE_NAME = value;
202
- }
203
- if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
204
- const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
205
- if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
206
- }
207
- if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
208
- const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
209
- if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
210
- }
211
- if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
212
- const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
213
- if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
214
- }
215
- if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
216
- const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
217
- if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
218
- else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
219
- }
220
- if (process.env.OTEL_TRACES_SAMPLER) {
221
- const value = process.env.OTEL_TRACES_SAMPLER.trim();
222
- if (value) env.OTEL_TRACES_SAMPLER = value;
223
- }
224
- if (process.env.OTEL_TRACES_SAMPLER_ARG) {
225
- const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
226
- if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
227
- }
228
- return env;
229
- }
230
- function parseRatioSamplerArg(samplerName, samplerArg) {
231
- if (samplerArg === void 0) return 1;
232
- const ratio = Number(samplerArg);
233
- if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
234
- console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
235
- return 1;
236
- }
237
- return ratio;
238
- }
239
- function warnOnUnusedSamplerArg(samplerName, samplerArg) {
240
- if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
135
+ const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
136
+ /**
137
+ * Generate a new correlation ID
138
+ *
139
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe
140
+ *
141
+ * @returns A new correlation ID
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const id = generateCorrelationId();
146
+ * // Returns: 'a1b2c3d4e5f67890'
147
+ * ```
148
+ */
149
+ function generateCorrelationId() {
150
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
151
+ crypto.getRandomValues(bytes);
152
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
241
153
  }
242
- function createSamplerFromEnv(env) {
243
- const samplerName = env.OTEL_TRACES_SAMPLER;
244
- if (!samplerName) return;
245
- switch (samplerName) {
246
- case "always_on":
247
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
248
- return new _opentelemetry_sdk_trace_base.AlwaysOnSampler();
249
- case "always_off":
250
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
251
- return new _opentelemetry_sdk_trace_base.AlwaysOffSampler();
252
- case "traceidratio": return new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
253
- case "parentbased_always_on":
254
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
255
- return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOnSampler() });
256
- case "parentbased_always_off":
257
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
258
- return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOffSampler() });
259
- case "parentbased_traceidratio": return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
260
- case "jaeger_remote":
261
- case "parentbased_jaeger_remote":
262
- case "xray":
263
- console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
264
- return;
265
- default:
266
- console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
267
- return;
268
- }
154
+ /**
155
+ * Get the current correlation ID from context
156
+ *
157
+ * Resolution order:
158
+ * 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
159
+ * 2. Baggage (if propagated from upstream)
160
+ * 3. Active span's trace ID (first 16 chars as fallback)
161
+ * 4. undefined (if not in any context)
162
+ *
163
+ * @returns Current correlation ID or undefined
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const id = getCorrelationId();
168
+ * if (id) {
169
+ * console.log('Correlation ID:', id);
170
+ * }
171
+ * ```
172
+ */
173
+ function getCorrelationId() {
174
+ const storedId = correlationStorage.getStore()?.value;
175
+ if (storedId) return storedId;
176
+ const activeContext = _opentelemetry_api.context.active();
177
+ const baggageEntry = _opentelemetry_api.propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
178
+ if (baggageEntry?.value) return baggageEntry.value;
179
+ const span = _opentelemetry_api.trace.getActiveSpan();
180
+ if (span) return span.spanContext().traceId.slice(0, 16);
269
181
  }
270
182
  /**
271
- * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
272
- * Example: "service.version=1.0.0,deployment.environment=production"
183
+ * Get or create a correlation ID
184
+ *
185
+ * If a correlation ID exists in the current context, returns it.
186
+ * Otherwise, generates a new one.
187
+ *
188
+ * @returns Existing or new correlation ID
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * const id = getOrCreateCorrelationId();
193
+ * // Always returns a valid correlation ID
194
+ * ```
273
195
  */
274
- function parseResourceAttributes(input) {
275
- if (!input || input.trim() === "") return {};
276
- const attributes = {};
277
- const pairs = input.split(",");
278
- for (const pair of pairs) {
279
- const trimmedPair = pair.trim();
280
- if (!trimmedPair) continue;
281
- const equalIndex = trimmedPair.indexOf("=");
282
- if (equalIndex === -1) continue;
283
- const key = trimmedPair.slice(0, equalIndex).trim();
284
- const value = trimmedPair.slice(equalIndex + 1).trim();
285
- if (key && value) attributes[key] = value;
286
- }
287
- return attributes;
196
+ function getOrCreateCorrelationId() {
197
+ return getCorrelationId() ?? generateCorrelationId();
288
198
  }
289
199
  /**
290
- * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
291
- * Example: "api-key=secret123,x-custom-header=value"
200
+ * Run a function with a specific correlation ID in context
201
+ *
202
+ * The correlation ID will be available via getCorrelationId() throughout
203
+ * the execution of the function and any async operations it spawns.
204
+ *
205
+ * @param correlationId - Correlation ID to use
206
+ * @param fn - Function to execute
207
+ * @returns The return value of the function
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * await runWithCorrelationId('abc123', async () => {
212
+ * // getCorrelationId() returns 'abc123' here
213
+ * await processRequest();
214
+ * });
215
+ * ```
292
216
  */
293
- function parseOtlpHeaders(input) {
294
- if (!input || input.trim() === "") return {};
295
- const headers = {};
296
- const pairs = input.split(",");
297
- for (const pair of pairs) {
298
- const trimmedPair = pair.trim();
299
- if (!trimmedPair) continue;
300
- const equalIndex = trimmedPair.indexOf("=");
301
- if (equalIndex === -1) continue;
302
- const key = trimmedPair.slice(0, equalIndex).trim();
303
- const value = trimmedPair.slice(equalIndex + 1).trim();
304
- if (key && value) headers[key] = value;
305
- }
306
- return headers;
217
+ function runWithCorrelationId(correlationId, fn) {
218
+ return correlationStorage.run({ value: correlationId }, fn);
307
219
  }
308
220
  /**
309
- * Convert resolved environment variables to config
221
+ * Set correlation ID in the current context (mutates context)
222
+ *
223
+ * Note: This updates the AsyncLocalStorage context. For proper scoping
224
+ * across async boundaries, prefer runWithCorrelationId() instead.
225
+ *
226
+ * @param correlationId - Correlation ID to set
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * setCorrelationId('abc123');
231
+ * // Now getCorrelationId() returns 'abc123'
232
+ * ```
310
233
  */
311
- function envToConfig(env) {
312
- const config = {};
313
- if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
314
- if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
315
- if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
316
- if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
317
- const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
318
- if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
319
- const sampler = createSamplerFromEnv(env);
320
- if (sampler) config.otelSampler = sampler;
321
- return config;
234
+ function setCorrelationId(correlationId) {
235
+ enterOrRun(correlationStorage, correlationId);
322
236
  }
323
237
  /**
324
- * Main function to resolve config from environment variables
238
+ * Set correlation ID in baggage for propagation
239
+ *
240
+ * This adds the correlation ID to the W3C baggage header, allowing it
241
+ * to be propagated to downstream services.
242
+ *
243
+ * Note: Only use this when you explicitly want cross-service propagation.
244
+ * Default is OFF to avoid header bloat.
245
+ *
246
+ * @param correlationId - Correlation ID to propagate
247
+ * @returns New context with baggage set
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const newContext = setCorrelationIdInBaggage('abc123');
252
+ * context.with(newContext, () => {
253
+ * // Baggage will be propagated in outgoing requests
254
+ * });
255
+ * ```
325
256
  */
326
- function resolveConfigFromEnv() {
327
- return envToConfig(resolveOtelEnv());
257
+ function setCorrelationIdInBaggage(correlationId) {
258
+ const activeContext = _opentelemetry_api.context.active();
259
+ let baggage = _opentelemetry_api.propagation.getBaggage(activeContext) ?? _opentelemetry_api.propagation.createBaggage();
260
+ baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
261
+ return _opentelemetry_api.propagation.setBaggage(activeContext, baggage);
328
262
  }
329
-
330
- //#endregion
331
- //#region src/devtools.ts
332
- const defaultHost = "127.0.0.1";
333
- const defaultPort = 4318;
334
- function resolveDevtoolsConfig(config) {
335
- if (!config) return {
336
- enabled: false,
337
- endpoint: void 0,
338
- embedded: false,
339
- host: defaultHost,
340
- port: defaultPort,
341
- verbose: false
342
- };
343
- if (config === true) return {
344
- enabled: true,
345
- endpoint: `http://${defaultHost}:${defaultPort}`,
346
- embedded: false,
347
- host: defaultHost,
348
- port: defaultPort,
349
- verbose: false
350
- };
351
- const enabled = config.enabled ?? true;
352
- const host = config.host ?? defaultHost;
353
- const port = config.port ?? defaultPort;
354
- const endpoint = config.endpoint ?? `http://${host}:${port}`;
355
- return {
356
- enabled,
357
- endpoint: enabled ? endpoint : void 0,
358
- embedded: enabled && (config.embedded ?? false),
359
- host,
360
- port,
361
- verbose: config.verbose ?? false
362
- };
263
+ /**
264
+ * Get the correlation storage instance (for internal use in init/shutdown)
265
+ */
266
+ function getCorrelationStorage() {
267
+ return correlationStorage;
363
268
  }
364
269
 
365
270
  //#endregion
366
- //#region src/process-handlers.ts
367
- let removeOwnedHandlers = [];
368
- /**
369
- * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
370
- * a call to `installProcessHandlers`, which clears that list before installing
371
- * its own signal listeners.
372
- */
373
- let removeExitFlush;
271
+ //#region src/event-queue.ts
272
+ const DEFAULT_CONFIG$2 = {
273
+ maxSize: 5e4,
274
+ batchSize: 100,
275
+ flushInterval: 1e4,
276
+ maxRetries: 3,
277
+ rateLimit: {
278
+ maxEventsPerSecond: 100,
279
+ burstCapacity: 200
280
+ }
281
+ };
374
282
  /**
375
- * Shared across every path that can end the process: signals, fatal errors and
376
- * a clean exit. They can overlap — a container stopping a job that has just
377
- * finished its work sends SIGTERM while the exit flush is still draining — and
378
- * a second shutdown would tear down queues the first is still using.
283
+ * Get subscriber name for metrics (stable, low-cardinality)
284
+ *
285
+ * Priority:
286
+ * 1. Explicit config: subscriber.name
287
+ * 2. Class static property (if available)
288
+ * 3. Fallback: lowercase class name without "Subscriber" suffix
379
289
  */
380
- let shutdownInFlight;
290
+ function getSubscriberName(subscriber) {
291
+ if (subscriber.name) return subscriber.name.toLowerCase();
292
+ return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
293
+ }
381
294
  /**
382
- * The clean-exit flush, while it runs. Doubles as the latch that keeps a
383
- * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
384
- * waits on rather than tearing down queues mid-drain.
295
+ * Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
296
+ * a queue rebuilt from the same config must not reuse them.
385
297
  */
386
- let exitFlushInFlight;
387
- const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
388
- function runShutdownOnce(shutdown, timeoutMs) {
389
- if (shutdownInFlight) return shutdownInFlight;
390
- let timeoutHandle;
391
- const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
392
- const timeout = new Promise((resolve) => {
393
- timeoutHandle = setTimeout(resolve, timeoutMs);
394
- timeoutHandle.unref();
395
- });
396
- shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
397
- if (timeoutHandle) clearTimeout(timeoutHandle);
398
- });
399
- return shutdownInFlight;
400
- }
401
- const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
402
- function signalExitCode(signal) {
403
- return 128 + node_os.constants.signals[signal];
404
- }
298
+ const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
405
299
  /**
406
- * Surface a fatal error before shutting down.
300
+ * Events queue with batching and backpressure
407
301
  *
408
- * Registering an `uncaughtException` / `unhandledRejection` listener overrides
409
- * Node's default of printing the stack to stderr, so without this a crash under
410
- * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
411
- * so we print to stderr directly to guarantee the crash stays visible.
302
+ * Features:
303
+ * - Batches events for efficient sending
304
+ * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
305
+ * - Exponential backoff retry
306
+ * - Rate limiting to prevent overwhelming subscribers
307
+ * - Graceful flush on shutdown
412
308
  */
413
- function reportFatalError(error, event) {
414
- const err = error instanceof Error ? error : new Error(String(error));
415
- console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
416
- }
417
- function installProcessHandlers(config, shutdown) {
418
- uninstallProcessHandlers();
419
- const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
420
- let exitScheduled = false;
421
- let resolvedExitCode = 0;
422
- let fatalLatched = false;
423
- const shutdownAndExit = (exitCode, fatal) => {
424
- if (fatal) {
425
- if (!fatalLatched) {
426
- resolvedExitCode = exitCode;
427
- fatalLatched = true;
428
- }
429
- } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
430
- if (exitScheduled) return;
431
- exitScheduled = true;
432
- runShutdownOnce(shutdown, timeoutMs).then(() => {
433
- process.exit(resolvedExitCode);
434
- });
435
- };
436
- for (const signal of config.signals ?? DEFAULT_SIGNALS) {
437
- const listener = () => {
438
- shutdownAndExit(signalExitCode(signal), false);
309
+ var EventQueue = class {
310
+ queue = [];
311
+ flushTimer = null;
312
+ config;
313
+ subscribers;
314
+ rateLimiter;
315
+ flushPromise = null;
316
+ isShuttingDown = false;
317
+ metrics = null;
318
+ observableCleanups = [];
319
+ subscriberHealthy = /* @__PURE__ */ new Map();
320
+ constructor(subscribers, config) {
321
+ const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
322
+ if (live.length < subscribers.length) getLogger().warn({ subscribers: subscribers.filter((s) => shutDownSubscribers.has(s)).map((s) => getSubscriberName(s)) }, "[autotel] Subscriber already shut down, events will not be delivered; pass new subscriber instances to init()");
323
+ this.subscribers = live;
324
+ this.config = {
325
+ ...DEFAULT_CONFIG$2,
326
+ ...config
439
327
  };
440
- process.on(signal, listener);
441
- removeOwnedHandlers.push(() => {
442
- process.removeListener(signal, listener);
443
- });
328
+ this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
329
+ for (const subscriber of this.subscribers) {
330
+ const name = getSubscriberName(subscriber);
331
+ this.subscriberHealthy.set(name, true);
332
+ }
333
+ this.initMetrics();
444
334
  }
445
- if (config.fatalErrors ?? true) {
446
- const uncaughtExceptionListener = (error) => {
447
- reportFatalError(error, "uncaughtException");
448
- shutdownAndExit(1, true);
335
+ /**
336
+ * Initialize OTel metrics for queue observability
337
+ */
338
+ initMetrics() {
339
+ const meter = require_config.getConfig().meter;
340
+ const queueSize = meter.createObservableGauge("autotel.event_delivery.queue.size", {
341
+ description: "Current number of events in the delivery queue",
342
+ unit: "count"
343
+ });
344
+ const queueSizeCallback = (observableResult) => {
345
+ observableResult.observe(this.queue.length);
449
346
  };
450
- const unhandledRejectionListener = (reason) => {
451
- reportFatalError(reason, "unhandledRejection");
452
- shutdownAndExit(1, true);
347
+ queueSize.addCallback(queueSizeCallback);
348
+ this.observableCleanups.push(() => queueSize.removeCallback(queueSizeCallback));
349
+ const oldestAge = meter.createObservableGauge("autotel.event_delivery.queue.oldest_age_ms", {
350
+ description: "Age of the oldest event in the queue in milliseconds",
351
+ unit: "ms"
352
+ });
353
+ const oldestAgeCallback = (observableResult) => {
354
+ if (this.queue.length > 0) {
355
+ const oldest = this.queue[0];
356
+ const ageMs = Date.now() - oldest.timestamp;
357
+ observableResult.observe(ageMs);
358
+ } else observableResult.observe(0);
453
359
  };
454
- process.on("uncaughtException", uncaughtExceptionListener);
455
- process.on("unhandledRejection", unhandledRejectionListener);
456
- removeOwnedHandlers.push(() => {
457
- process.removeListener("uncaughtException", uncaughtExceptionListener);
458
- }, () => {
459
- process.removeListener("unhandledRejection", unhandledRejectionListener);
360
+ oldestAge.addCallback(oldestAgeCallback);
361
+ this.observableCleanups.push(() => oldestAge.removeCallback(oldestAgeCallback));
362
+ const delivered = meter.createCounter("autotel.event_delivery.queue.delivered", {
363
+ description: "Number of events successfully delivered to subscribers",
364
+ unit: "count"
460
365
  });
461
- }
462
- }
463
- /**
464
- * Flush telemetry when the process runs to completion.
465
- *
466
- * The signal and fatal-error handlers above cover a process that is stopped or
467
- * that crashes. Neither fires when a script simply finishes: the event loop
468
- * drains and Node exits, taking whatever the batch span processor was still
469
- * holding with it. `beforeExit` is the only hook for that case, and it is the
470
- * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
471
- *
472
- * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
473
- * only the final one, so tearing the SDK down here would silently kill
474
- * telemetry in a process that goes on to do more work.
475
- */
476
- function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
477
- removeExitFlush?.();
478
- exitFlushInFlight = void 0;
479
- const listener = (code) => {
480
- if (exitFlushInFlight || shutdownInFlight) return;
481
- const deadline = setTimeout(() => {
482
- process.exit(process.exitCode ?? code);
483
- }, timeoutMs);
484
- exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
485
- clearTimeout(deadline);
366
+ const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
367
+ description: "Number of events that failed delivery after all retry attempts",
368
+ unit: "count"
486
369
  });
487
- };
488
- process.on("beforeExit", listener);
489
- removeExitFlush = () => {
490
- process.removeListener("beforeExit", listener);
491
- removeExitFlush = void 0;
492
- };
493
- }
494
- function uninstallProcessHandlers() {
495
- for (const removeHandler of removeOwnedHandlers) removeHandler();
496
- removeOwnedHandlers = [];
497
- removeExitFlush?.();
498
- shutdownInFlight = void 0;
499
- exitFlushInFlight = void 0;
500
- }
501
-
502
- //#endregion
503
- //#region src/rate-limiter.ts
504
- /**
505
- * Token bucket rate limiter
506
- *
507
- * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
508
- * Thread-safe for async operations.
509
- */
510
- var TokenBucketRateLimiter = class {
511
- tokens;
512
- maxTokens;
513
- refillRate;
514
- lastRefill;
515
- constructor(config) {
516
- this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
517
- this.tokens = this.maxTokens;
518
- this.refillRate = config.maxEventsPerSecond / 1e3;
519
- this.lastRefill = Date.now();
370
+ const dropped = meter.createCounter("autotel.event_delivery.queue.dropped", {
371
+ description: "Number of events dropped from the queue",
372
+ unit: "count"
373
+ });
374
+ const latency = meter.createHistogram("autotel.event_delivery.queue.latency_ms", {
375
+ description: "Event delivery latency from enqueue to successful send",
376
+ unit: "ms"
377
+ });
378
+ const subscriberHealth = meter.createObservableGauge("autotel.event_delivery.subscriber.health", {
379
+ description: "Subscriber health status (1=healthy, 0=unhealthy)",
380
+ unit: "1"
381
+ });
382
+ const subscriberHealthCallback = (observableResult) => {
383
+ for (const [subscriberName, isHealthy] of this.subscriberHealthy) observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName });
384
+ };
385
+ subscriberHealth.addCallback(subscriberHealthCallback);
386
+ this.observableCleanups.push(() => subscriberHealth.removeCallback(subscriberHealthCallback));
387
+ this.metrics = {
388
+ queueSize,
389
+ oldestAge,
390
+ delivered,
391
+ failed,
392
+ dropped,
393
+ latency,
394
+ subscriberHealth
395
+ };
520
396
  }
521
397
  /**
522
- * Try to consume a token (allow an event)
523
- * Returns true if allowed, false if rate limit exceeded
398
+ * Record a dropped event with reason and emit debug breadcrumb
524
399
  */
525
- tryConsume(count = 1) {
526
- this.refill();
527
- if (this.tokens >= count) {
528
- this.tokens -= count;
529
- return true;
530
- }
531
- return false;
400
+ recordDropped(reason, event, subscriberName) {
401
+ const attrs = { reason };
402
+ if (subscriberName) attrs.subscriber = subscriberName;
403
+ this.metrics?.dropped.add(1, attrs);
404
+ const logLevel = reason === "payload_invalid" ? "error" : "warn";
405
+ const logger = getLogger();
406
+ if (logLevel === "error") logger.error({
407
+ eventName: event?.name,
408
+ subscriber: subscriberName,
409
+ reason,
410
+ correlationId: event?._correlationId,
411
+ traceId: event?._traceId
412
+ }, `[autotel] Event dropped: ${reason}`);
413
+ else logger.warn({
414
+ eventName: event?.name,
415
+ subscriber: subscriberName,
416
+ reason,
417
+ correlationId: event?._correlationId,
418
+ traceId: event?._traceId
419
+ }, `[autotel] Event dropped: ${reason}`);
532
420
  }
533
421
  /**
534
- * Wait until a token is available (async rate limiting)
535
- * Returns a promise that resolves when the event can be processed
422
+ * Record permanent delivery failure (after all retries exhausted)
423
+ * Increments failed counter and logs error
536
424
  */
537
- async waitForToken(count = 1) {
538
- this.refill();
539
- if (this.tokens >= count) {
540
- this.tokens -= count;
541
- return;
542
- }
543
- const tokensNeeded = count - this.tokens;
544
- const waitMs = Math.ceil(tokensNeeded / this.refillRate);
545
- await new Promise((resolve) => setTimeout(resolve, waitMs));
546
- return this.waitForToken(count);
425
+ recordFailed(event, subscriberName, error) {
426
+ this.metrics?.failed.add(1, { subscriber: subscriberName });
427
+ this.subscriberHealthy.set(subscriberName, false);
428
+ getLogger().error({
429
+ eventName: event.name,
430
+ subscriber: subscriberName,
431
+ correlationId: event._correlationId,
432
+ traceId: event._traceId,
433
+ err: error
434
+ }, `[autotel] Event delivery failed after all retries`);
547
435
  }
548
436
  /**
549
- * Refill tokens based on elapsed time
437
+ * Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
438
+ * Used during retry attempts - only recordFailed should increment the counter
550
439
  */
551
- refill() {
552
- const now = Date.now();
553
- const tokensToAdd = (now - this.lastRefill) * this.refillRate;
554
- this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
555
- this.lastRefill = now;
440
+ markSubscriberUnhealthy(subscriberName) {
441
+ this.subscriberHealthy.set(subscriberName, false);
556
442
  }
557
443
  /**
558
- * Get current available tokens (for testing/debugging)
444
+ * Record successful delivery
559
445
  */
560
- getAvailableTokens() {
561
- this.refill();
562
- return Math.floor(this.tokens);
446
+ recordDelivered(event, subscriberName, startTime) {
447
+ const latencyMs = Date.now() - startTime;
448
+ this.metrics?.delivered.add(1, { subscriber: subscriberName });
449
+ this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
450
+ this.subscriberHealthy.set(subscriberName, true);
563
451
  }
564
452
  /**
565
- * Reset the rate limiter (for testing)
453
+ * Enqueue an event for sending
454
+ *
455
+ * Backpressure policy:
456
+ * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
566
457
  */
567
- reset() {
568
- this.tokens = this.maxTokens;
569
- this.lastRefill = Date.now();
570
- }
571
- };
572
-
573
- //#endregion
574
- //#region src/trace-context.ts
575
- const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
576
- /** Whether a TraceContext consumer explicitly chose this span's status. */
577
- function hasExplicitSpanStatus(span) {
578
- return spansWithExplicitStatus.has(span);
579
- }
580
- /**
581
- * AsyncLocalStorage for storing the active context with baggage
582
- * This allows setters to update the context and have it persist
583
- */
584
- const contextStorage = new node_async_hooks.AsyncLocalStorage();
585
- /**
586
- * Get the context storage instance (for initialization in functional.ts)
587
- */
588
- function getContextStorage() {
589
- return contextStorage;
590
- }
591
- /**
592
- * Get the active OTel context with the latest stored baggage overlaid.
593
- * Span identity always comes from the active OTel scope.
594
- */
595
- function getActiveContextWithBaggage() {
596
- const activeContext = _opentelemetry_api.context.active();
597
- const stored = contextStorage.getStore()?.value;
598
- if (!stored) return activeContext;
599
- const storedBaggage = _opentelemetry_api.propagation.getBaggage(stored);
600
- return storedBaggage ? _opentelemetry_api.propagation.setBaggage(activeContext, storedBaggage) : activeContext;
601
- }
602
- /**
603
- * Set a value in AsyncLocalStorage, preferring enterWith() when available
604
- * (Node.js) and falling back to run() for environments that only support
605
- * run() (e.g. Cloudflare Workers).
606
- *
607
- * On runtimes without enterWith() we mutate the existing run() scope when one
608
- * exists. This is what allows baggage/correlation updates to remain visible
609
- * for the rest of the traced callback in Workers.
610
- */
611
- function enterOrRun(storage, value) {
612
- const existingStore = storage.getStore();
613
- if (existingStore) {
614
- existingStore.value = value;
615
- return;
458
+ enqueue(event) {
459
+ if (this.isShuttingDown) {
460
+ this.recordDropped("shutdown", event);
461
+ return;
462
+ }
463
+ if (this.queue.length >= this.config.maxSize) {
464
+ const droppedEvent = this.queue.shift();
465
+ this.recordDropped("rate_limit", droppedEvent);
466
+ getLogger().warn({ droppedEvent: droppedEvent?.name }, `[autotel] Events queue full (${this.config.maxSize} events). Dropping oldest event. Events are being produced faster than they can be sent. Check your subscribers or reduce tracking frequency.`);
467
+ }
468
+ const enrichedEvent = {
469
+ ...event,
470
+ _correlationId: event._correlationId || getOrCreateCorrelationId()
471
+ };
472
+ this.queue.push(enrichedEvent);
473
+ this.scheduleBatchFlush();
616
474
  }
617
- const boxedValue = { value };
618
- try {
619
- storage.enterWith(boxedValue);
620
- } catch {
621
- storage.run(boxedValue, () => {});
475
+ /**
476
+ * Schedule a batch flush if not already scheduled
477
+ */
478
+ scheduleBatchFlush() {
479
+ if (this.flushTimer || this.flushPromise) return;
480
+ this.flushTimer = setTimeout(() => {
481
+ this.flushTimer = null;
482
+ this.flushBatch();
483
+ }, this.config.flushInterval);
622
484
  }
623
- }
624
- function updateActiveContext(newContext) {
625
- enterOrRun(contextStorage, newContext);
626
- const manager = _opentelemetry_api.context._getContextManager?.();
627
- if (!manager) return;
628
- const asyncLocal = manager._asyncLocalStorage ?? void 0;
629
- if (asyncLocal?.enterWith) {
630
- asyncLocal.enterWith(newContext);
631
- return;
485
+ /**
486
+ * Flush a batch of events
487
+ * Uses promise-based concurrency control to prevent race conditions
488
+ */
489
+ async flushBatch() {
490
+ if (this.queue.length === 0) return;
491
+ if (this.flushPromise) {
492
+ await this.flushPromise;
493
+ return;
494
+ }
495
+ this.flushPromise = this.doFlushBatch();
496
+ try {
497
+ await this.flushPromise;
498
+ } finally {
499
+ this.flushPromise = null;
500
+ if (this.queue.length > 0) this.scheduleBatchFlush();
501
+ }
632
502
  }
633
- if (require_values.isFunction(manager.with)) manager.with(newContext, () => {});
634
- }
635
- /**
636
- * Create a TraceContext from an OpenTelemetry Span
637
- *
638
- * This utility extracts trace context information from a span
639
- * and provides span manipulation methods and baggage operations in a consistent format.
640
- *
641
- * Note: Baggage methods always operate on the currently active context,
642
- * which may differ from the context when createTraceContext was called.
643
- */
644
- function createTraceContext(span) {
645
- const spanContext = span.spanContext();
646
- if (!contextStorage.getStore()?.value) {
647
- const activeContext = _opentelemetry_api.context.active();
648
- enterOrRun(contextStorage, activeContext);
503
+ /**
504
+ * Internal flush implementation
505
+ */
506
+ async doFlushBatch() {
507
+ const batch = this.queue.splice(0, this.config.batchSize);
508
+ await this.sendWithRetry(batch, this.config.maxRetries);
649
509
  }
650
- const traceCtx = {
651
- traceId: spanContext.traceId,
652
- spanId: spanContext.spanId,
653
- correlationId: spanContext.traceId.slice(0, 16),
654
- setAttribute: span.setAttribute.bind(span),
655
- setAttributes: span.setAttributes.bind(span),
656
- setStatus: (status) => {
657
- spansWithExplicitStatus.add(span);
658
- span.setStatus(status);
659
- },
660
- recordException: span.recordException.bind(span),
661
- addEvent: span.addEvent.bind(span),
662
- addLink: span.addLink.bind(span),
663
- addLinks: span.addLinks.bind(span),
664
- updateName: span.updateName.bind(span),
665
- isRecording: span.isRecording.bind(span),
666
- recordError: (cause) => {
667
- const err = cause instanceof Error ? cause : new Error(String(cause));
668
- require_structured_error.recordStructuredError(traceCtx, err);
669
- },
670
- track: (event, data) => {
671
- track(event, data);
672
- },
673
- getBaggage(key) {
674
- const activeCtx = _opentelemetry_api.context.active();
675
- let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
676
- if (!baggage) {
677
- const storedContext = contextStorage.getStore()?.value;
678
- if (storedContext) baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
679
- }
680
- return baggage?.getEntry(key)?.value;
681
- },
682
- setBaggage(key, value) {
683
- const currentContext = getActiveContextWithBaggage();
684
- const updated = (_opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage()).setEntry(key, { value });
685
- updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
686
- return value;
687
- },
688
- deleteBaggage(key) {
689
- const currentContext = getActiveContextWithBaggage();
690
- const baggage = _opentelemetry_api.propagation.getBaggage(currentContext);
691
- if (baggage) {
692
- const updated = baggage.removeEntry(key);
693
- updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
510
+ /**
511
+ * Send events with exponential backoff retry
512
+ * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
513
+ * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
514
+ */
515
+ async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
516
+ const failedDeliveries = await this.sendToSubscribers(events, subscribersByEventIndex);
517
+ if (failedDeliveries.length > 0) if (retriesLeft > 0) {
518
+ const failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
519
+ const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
520
+ const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
521
+ for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
522
+ const set = /* @__PURE__ */ new Set();
523
+ for (const { eventIndex, subscriberName } of failedDeliveries) if (eventIndex === origIndex) set.add(subscriberName);
524
+ failedSubscribersByRetryIndex.set(j, set);
525
+ }
526
+ const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
527
+ await new Promise((resolve) => setTimeout(resolve, delay));
528
+ return this.sendWithRetry(eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex);
529
+ } else {
530
+ for (const { eventIndex, subscriberName, error } of failedDeliveries) {
531
+ const event = events[eventIndex];
532
+ if (event) this.recordFailed(event, subscriberName, error);
533
+ }
534
+ const failedSubscriberNames = [...new Set(failedDeliveries.map((f) => f.subscriberName))];
535
+ getLogger().error({
536
+ failedSubscribers: failedSubscriberNames,
537
+ retriesAttempted: this.config.maxRetries
538
+ }, "[autotel] Failed to send events after retries");
539
+ }
540
+ }
541
+ /**
542
+ * Send events to configured subscribers with rate limiting and metrics.
543
+ * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
544
+ * Returns per-event, per-subscriber failures (empty if all succeeded).
545
+ */
546
+ async sendToSubscribers(events, subscribersByEventIndex) {
547
+ const failedDeliveries = [];
548
+ const sendOne = async (event, eventIndex) => {
549
+ const subscriberNames = subscribersByEventIndex?.get(eventIndex);
550
+ const failures = await this.sendEventToSubscribers(event, subscriberNames ?? void 0);
551
+ for (const failure of failures) failedDeliveries.push({
552
+ eventIndex,
553
+ subscriberName: failure.subscriberName,
554
+ error: failure.error
555
+ });
556
+ };
557
+ if (!this.rateLimiter) {
558
+ for (const [i, event] of events.entries()) if (event) await sendOne(event, i);
559
+ return failedDeliveries;
560
+ }
561
+ for (const [i, event] of events.entries()) {
562
+ await this.rateLimiter.waitForToken();
563
+ if (event) await sendOne(event, i);
564
+ }
565
+ return failedDeliveries;
566
+ }
567
+ /**
568
+ * Send a single event to subscribers.
569
+ * - When subscriberNames is undefined (initial attempt): send to all subscribers.
570
+ * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
571
+ * Returns list of subscribers that failed (empty if all succeeded).
572
+ */
573
+ async sendEventToSubscribers(event, subscriberNames) {
574
+ const startTime = event.timestamp;
575
+ const failures = [];
576
+ const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter((s) => subscriberNames.has(getSubscriberName(s)));
577
+ const results = await Promise.allSettled(subscribersToTry.map(async (subscriber) => {
578
+ const subscriberName = getSubscriberName(subscriber);
579
+ try {
580
+ await subscriber.trackEvent(event.name, event.attributes, {
581
+ autotel: event.autotel,
582
+ schema: event.schema
583
+ });
584
+ this.recordDelivered(event, subscriberName, startTime);
585
+ return {
586
+ subscriberName,
587
+ success: true
588
+ };
589
+ } catch (error) {
590
+ this.markSubscriberUnhealthy(subscriberName);
591
+ return {
592
+ subscriberName,
593
+ success: false,
594
+ error: error instanceof Error ? error : void 0
595
+ };
596
+ }
597
+ }));
598
+ for (const result of results) if (result.status === "fulfilled" && !result.value.success) failures.push({
599
+ subscriberName: result.value.subscriberName,
600
+ error: result.value.error
601
+ });
602
+ return failures;
603
+ }
604
+ /**
605
+ * Flush all remaining events. Queue remains usable after flush (e.g. for
606
+ * auto-flush at root span end). Use shutdown() when tearing down the queue.
607
+ */
608
+ async flush() {
609
+ if (this.flushTimer) {
610
+ clearTimeout(this.flushTimer);
611
+ this.flushTimer = null;
612
+ }
613
+ if (this.flushPromise) await this.flushPromise;
614
+ while (this.queue.length > 0) await this.doFlushBatch();
615
+ }
616
+ /**
617
+ * Flush remaining events and permanently disable the queue (reject new events).
618
+ * Use for process/SDK shutdown; use flush() for periodic or span-end drain.
619
+ */
620
+ async shutdown() {
621
+ this.isShuttingDown = true;
622
+ await this.flush();
623
+ const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
624
+ if (!subscriber.shutdown) return;
625
+ shutDownSubscribers.add(subscriber);
626
+ return subscriber.shutdown();
627
+ }));
628
+ for (const [index, drain] of drains.entries()) {
629
+ if (drain.status !== "rejected") continue;
630
+ const subscriberName = getSubscriberName(this.subscribers[index]);
631
+ this.subscriberHealthy.set(subscriberName, false);
632
+ getLogger().error({
633
+ subscriber: subscriberName,
634
+ err: drain.reason
635
+ }, "[autotel] Subscriber shutdown failed, buffered events may be lost");
636
+ }
637
+ }
638
+ /**
639
+ * Cleanup observable metric callbacks to prevent memory leaks
640
+ * Call this when destroying the EventQueue instance
641
+ */
642
+ cleanup() {
643
+ for (const cleanupFn of this.observableCleanups) try {
644
+ cleanupFn();
645
+ } catch {}
646
+ this.observableCleanups = [];
647
+ }
648
+ /**
649
+ * Get queue size (for testing/debugging)
650
+ */
651
+ size() {
652
+ return this.queue.length;
653
+ }
654
+ /**
655
+ * Get subscriber health status (for testing/debugging)
656
+ */
657
+ getSubscriberHealth() {
658
+ return new Map(this.subscriberHealthy);
659
+ }
660
+ /**
661
+ * Check if a specific subscriber is healthy
662
+ */
663
+ isSubscriberHealthy(subscriberName) {
664
+ return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
665
+ }
666
+ /**
667
+ * Manually mark a subscriber as healthy or unhealthy
668
+ * (used for circuit breaker integration)
669
+ */
670
+ setSubscriberHealth(subscriberName, healthy) {
671
+ this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
672
+ }
673
+ };
674
+
675
+ //#endregion
676
+ //#region src/validation.ts
677
+ const DEFAULT_CONFIG$1 = {
678
+ maxEventNameLength: 100,
679
+ maxAttributeKeyLength: 100,
680
+ maxAttributeValueLength: 1e3,
681
+ maxAttributeCount: 50,
682
+ maxNestingDepth: 3,
683
+ sensitivePatterns: [
684
+ /password/i,
685
+ /secret/i,
686
+ /token/i,
687
+ /api[_-]?key/i,
688
+ /access[_-]?key/i,
689
+ /private[_-]?key/i,
690
+ /auth/i,
691
+ /credential/i,
692
+ /ssn/i,
693
+ /credit[_-]?card/i
694
+ ]
695
+ };
696
+ var ValidationError = class extends Error {
697
+ constructor(message) {
698
+ super(message);
699
+ this.name = "ValidationError";
700
+ }
701
+ };
702
+ /**
703
+ * Validate and sanitize event name
704
+ * Throws ValidationError if invalid
705
+ */
706
+ function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
707
+ if (require_values.asString(eventName) === void 0) throw new ValidationError(`Event name must be a string, got ${require_values.describeValue(eventName)}`);
708
+ const trimmed = eventName.trim();
709
+ if (trimmed.length === 0) throw new ValidationError("Event name cannot be empty");
710
+ if (trimmed.length > config.maxEventNameLength) throw new ValidationError(`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`);
711
+ if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) throw new ValidationError(`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`);
712
+ return trimmed;
713
+ }
714
+ /**
715
+ * Validate and sanitize attributes
716
+ * Returns sanitized attributes (sensitive data redacted)
717
+ */
718
+ function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
719
+ if (attributes === void 0 || attributes === null) return;
720
+ if (!require_values.asRecord(attributes)) throw new ValidationError("Attributes must be an object");
721
+ const keys = Object.keys(attributes);
722
+ if (keys.length > config.maxAttributeCount) throw new ValidationError(`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`);
723
+ const sanitized = {};
724
+ for (const key of keys) {
725
+ if (key.length > config.maxAttributeKeyLength) throw new ValidationError(`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`);
726
+ const value = attributes[key];
727
+ if (isSensitiveString(key, value, config)) {
728
+ sanitized[key] = "[REDACTED]";
729
+ continue;
730
+ }
731
+ sanitized[key] = sanitizeValue(value, config, 1);
732
+ }
733
+ return sanitized;
734
+ }
735
+ /**
736
+ * Whether this key/value pair is a credential to redact. Strings only:
737
+ * numeric and boolean values are not credentials, and replacing them with the
738
+ * literal '[REDACTED]' leaks no signal while breaking every consumer that
739
+ * treats them as a number.
740
+ */
741
+ function isSensitiveString(key, value, config) {
742
+ return require_values.asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
743
+ }
744
+ /**
745
+ * Sanitize attribute value (recursive)
746
+ */
747
+ function sanitizeValue(value, config, depth) {
748
+ if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
749
+ if (value === null || value === void 0) return value;
750
+ const text = require_values.asString(value);
751
+ if (text !== void 0) return text.length > config.maxAttributeValueLength ? text.slice(0, config.maxAttributeValueLength) + "..." : text;
752
+ const scalar = require_values.asNumber(value) ?? require_values.asBoolean(value);
753
+ if (scalar !== void 0) return scalar;
754
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
755
+ const record = require_values.asRecord(value);
756
+ if (record) try {
757
+ JSON.stringify(record);
758
+ const sanitized = {};
759
+ for (const [key, nested] of Object.entries(record)) sanitized[key] = isSensitiveString(key, nested, config) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
760
+ return sanitized;
761
+ } catch {
762
+ return "[CIRCULAR]";
763
+ }
764
+ return `[${require_values.describeValue(value)}]`;
765
+ }
766
+ /**
767
+ * Validate and sanitize an events event
768
+ * Returns { eventName, attributes } with sanitized values
769
+ */
770
+ function validateEvent(eventName, attributes, config) {
771
+ const fullConfig = {
772
+ ...DEFAULT_CONFIG$1,
773
+ ...config
774
+ };
775
+ return {
776
+ eventName: validateEventName(eventName, fullConfig),
777
+ attributes: validateAttributes(attributes, fullConfig)
778
+ };
779
+ }
780
+
781
+ //#endregion
782
+ //#region src/track.ts
783
+ /**
784
+ * Global track() function for business events
785
+ *
786
+ * Simple, no instantiation needed, auto-attaches trace context
787
+ */
788
+ let eventsQueue = null;
789
+ let warnedNoSubscribers = false;
790
+ /**
791
+ * Build autotel event context for trace correlation
792
+ *
793
+ * Works in multiple contexts:
794
+ * 1. Inside a span → use current span's trace_id + span_id
795
+ * 2. Outside span → use correlation_id only
796
+ * 3. With trace URL config → include clickable trace URL
797
+ */
798
+ function buildAutotelContext(span) {
799
+ const eventsConfig = getEventsConfig();
800
+ const config = getConfig();
801
+ const correlationId = getOrCreateCorrelationId();
802
+ if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
803
+ const autotelContext = { correlation_id: correlationId };
804
+ const spanContext = span?.spanContext();
805
+ if (spanContext) {
806
+ autotelContext.trace_id = spanContext.traceId;
807
+ autotelContext.span_id = spanContext.spanId;
808
+ autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
809
+ const traceState = spanContext.traceState;
810
+ if (traceState) try {
811
+ if (typeof traceState.serialize === "function") {
812
+ const traceStateStr = traceState.serialize();
813
+ if (traceStateStr) autotelContext.trace_state = traceStateStr;
814
+ }
815
+ } catch {}
816
+ if (eventsConfig.traceUrl && config) {
817
+ const traceUrl = eventsConfig.traceUrl({
818
+ traceId: spanContext.traceId,
819
+ spanId: spanContext.spanId,
820
+ correlationId,
821
+ serviceName: config.service,
822
+ environment: config.environment
823
+ });
824
+ if (traceUrl) autotelContext.trace_url = traceUrl;
825
+ }
826
+ } else if (eventsConfig.traceUrl && config) {
827
+ const traceUrl = eventsConfig.traceUrl({
828
+ correlationId,
829
+ serviceName: config.service,
830
+ environment: config.environment
831
+ });
832
+ if (traceUrl) autotelContext.trace_url = traceUrl;
833
+ }
834
+ return autotelContext;
835
+ }
836
+ /**
837
+ * Initialize events queue lazily
838
+ */
839
+ function getOrCreateQueue() {
840
+ if (!isInitialized()) {
841
+ warnIfNotInitialized("track()");
842
+ return null;
843
+ }
844
+ if (!eventsQueue) {
845
+ const config = getConfig();
846
+ if (!config?.subscribers || config.subscribers.length === 0) {
847
+ if (!warnedNoSubscribers) {
848
+ warnedNoSubscribers = true;
849
+ getLogger().warn({}, "[autotel] track() dropped an event: init() configured no subscribers. Events (including gen_ai.evaluation.result and audit events) reach a backend through init({ subscribers: [...] }).");
850
+ }
851
+ return null;
852
+ }
853
+ eventsQueue = new EventQueue(config.subscribers);
854
+ }
855
+ return eventsQueue;
856
+ }
857
+ /**
858
+ * Track a business events event
859
+ *
860
+ * Features:
861
+ * - Auto-attaches traceId and spanId if in active span
862
+ * - Batched sending with retry
863
+ * - Type-safe with optional generic
864
+ * - No-op if init() not called or no subscribers configured
865
+ *
866
+ * @example Basic usage
867
+ * ```typescript
868
+ * track('user.signup', { userId: '123', plan: 'pro' })
869
+ * ```
870
+ *
871
+ * @example With type safety
872
+ * ```typescript
873
+ * interface EventDatas {
874
+ * 'user.signup': { userId: string; plan: string }
875
+ * 'plan.upgraded': { userId: string; revenue: number }
876
+ * }
877
+ *
878
+ * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
879
+ * ```
880
+ *
881
+ * @example Trace correlation (automatic)
882
+ * ```typescript
883
+ * @Instrumented()
884
+ * class UserService {
885
+ * async createUser(data: CreateUserData) {
886
+ * // This track call automatically includes traceId + spanId
887
+ * track('user.signup', { userId: data.id })
888
+ * }
889
+ * }
890
+ * ```
891
+ */
892
+ function track(event, data, options) {
893
+ const queue = getOrCreateQueue();
894
+ if (!queue) return;
895
+ const validated = validateEvent(event, data, getValidationConfig() || void 0);
896
+ const span = _opentelemetry_api.trace.getActiveSpan();
897
+ const enrichedData = span ? {
898
+ ...validated.attributes,
899
+ traceId: span.spanContext().traceId,
900
+ spanId: span.spanContext().spanId
901
+ } : validated.attributes;
902
+ const autotelContext = buildAutotelContext(span);
903
+ queue.enqueue({
904
+ name: validated.eventName,
905
+ attributes: enrichedData,
906
+ timestamp: Date.now(),
907
+ autotel: autotelContext,
908
+ schema: options?.schema
909
+ });
910
+ }
911
+ /**
912
+ * Get events queue (for flush/shutdown)
913
+ * @internal
914
+ */
915
+ function getEventQueue() {
916
+ return eventsQueue;
917
+ }
918
+ /**
919
+ * Reset events queue (for shutdown/cleanup)
920
+ * @internal
921
+ */
922
+ function resetEventQueue() {
923
+ eventsQueue = null;
924
+ warnedNoSubscribers = false;
925
+ }
926
+
927
+ //#endregion
928
+ //#region src/trace-context.ts
929
+ const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
930
+ /** Whether a TraceContext consumer explicitly chose this span's status. */
931
+ function hasExplicitSpanStatus(span) {
932
+ return spansWithExplicitStatus.has(span);
933
+ }
934
+ /**
935
+ * AsyncLocalStorage for storing the active context with baggage
936
+ * This allows setters to update the context and have it persist
937
+ */
938
+ const contextStorage = new node_async_hooks.AsyncLocalStorage();
939
+ /**
940
+ * Get the context storage instance (for initialization in functional.ts)
941
+ */
942
+ function getContextStorage() {
943
+ return contextStorage;
944
+ }
945
+ /**
946
+ * Get the active OTel context with the latest stored baggage overlaid.
947
+ * Span identity always comes from the active OTel scope.
948
+ */
949
+ function getActiveContextWithBaggage() {
950
+ const activeContext = _opentelemetry_api.context.active();
951
+ const stored = contextStorage.getStore()?.value;
952
+ if (!stored) return activeContext;
953
+ const storedBaggage = _opentelemetry_api.propagation.getBaggage(stored);
954
+ return storedBaggage ? _opentelemetry_api.propagation.setBaggage(activeContext, storedBaggage) : activeContext;
955
+ }
956
+ /**
957
+ * Set a value in AsyncLocalStorage, preferring enterWith() when available
958
+ * (Node.js) and falling back to run() for environments that only support
959
+ * run() (e.g. Cloudflare Workers).
960
+ *
961
+ * On runtimes without enterWith() we mutate the existing run() scope when one
962
+ * exists. This is what allows baggage/correlation updates to remain visible
963
+ * for the rest of the traced callback in Workers.
964
+ */
965
+ function enterOrRun(storage, value) {
966
+ const existingStore = storage.getStore();
967
+ if (existingStore) {
968
+ existingStore.value = value;
969
+ return;
970
+ }
971
+ const boxedValue = { value };
972
+ try {
973
+ storage.enterWith(boxedValue);
974
+ } catch {
975
+ storage.run(boxedValue, () => {});
976
+ }
977
+ }
978
+ function updateActiveContext(newContext) {
979
+ enterOrRun(contextStorage, newContext);
980
+ const manager = _opentelemetry_api.context._getContextManager?.();
981
+ if (!manager) return;
982
+ const asyncLocal = manager._asyncLocalStorage ?? void 0;
983
+ if (asyncLocal?.enterWith) {
984
+ asyncLocal.enterWith(newContext);
985
+ return;
986
+ }
987
+ if (require_values.isFunction(manager.with)) manager.with(newContext, () => {});
988
+ }
989
+ /**
990
+ * Create a TraceContext from an OpenTelemetry Span
991
+ *
992
+ * This utility extracts trace context information from a span
993
+ * and provides span manipulation methods and baggage operations in a consistent format.
994
+ *
995
+ * Note: Baggage methods always operate on the currently active context,
996
+ * which may differ from the context when createTraceContext was called.
997
+ */
998
+ function createTraceContext(span) {
999
+ const spanContext = span.spanContext();
1000
+ if (!contextStorage.getStore()?.value) {
1001
+ const activeContext = _opentelemetry_api.context.active();
1002
+ enterOrRun(contextStorage, activeContext);
1003
+ }
1004
+ const traceCtx = {
1005
+ traceId: spanContext.traceId,
1006
+ spanId: spanContext.spanId,
1007
+ correlationId: spanContext.traceId.slice(0, 16),
1008
+ setAttribute: span.setAttribute.bind(span),
1009
+ setAttributes: span.setAttributes.bind(span),
1010
+ setStatus: (status) => {
1011
+ spansWithExplicitStatus.add(span);
1012
+ span.setStatus(status);
1013
+ },
1014
+ recordException: span.recordException.bind(span),
1015
+ addEvent: span.addEvent.bind(span),
1016
+ addLink: span.addLink.bind(span),
1017
+ addLinks: span.addLinks.bind(span),
1018
+ updateName: span.updateName.bind(span),
1019
+ isRecording: span.isRecording.bind(span),
1020
+ recordError: (cause) => {
1021
+ const err = cause instanceof Error ? cause : new Error(String(cause));
1022
+ require_structured_error.recordStructuredError(traceCtx, err);
1023
+ },
1024
+ track: (event, data) => {
1025
+ track(event, data);
1026
+ },
1027
+ getBaggage(key) {
1028
+ const activeCtx = _opentelemetry_api.context.active();
1029
+ let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
1030
+ if (!baggage) {
1031
+ const storedContext = contextStorage.getStore()?.value;
1032
+ if (storedContext) baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
1033
+ }
1034
+ return baggage?.getEntry(key)?.value;
1035
+ },
1036
+ setBaggage(key, value) {
1037
+ const currentContext = getActiveContextWithBaggage();
1038
+ const updated = (_opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage()).setEntry(key, { value });
1039
+ updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
1040
+ return value;
1041
+ },
1042
+ deleteBaggage(key) {
1043
+ const currentContext = getActiveContextWithBaggage();
1044
+ const baggage = _opentelemetry_api.propagation.getBaggage(currentContext);
1045
+ if (baggage) {
1046
+ const updated = baggage.removeEntry(key);
1047
+ updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
694
1048
  }
695
1049
  },
696
1050
  getAllBaggage() {
@@ -820,820 +1174,1345 @@ function defineBaggageSchema(namespace) {
820
1174
  }
821
1175
 
822
1176
  //#endregion
823
- //#region src/correlation-id.ts
1177
+ //#region src/sampling.ts
824
1178
  /**
825
- * Correlation ID utilities for event-driven observability
1179
+ * Tail sampling attribute keys (autotel-internal, not OTel semconv)
1180
+ */
1181
+ const AUTOTEL_SAMPLING_TAIL_KEEP = "autotel.sampling.tail.keep";
1182
+ const AUTOTEL_SAMPLING_TAIL_EVALUATED = "autotel.sampling.tail.evaluated";
1183
+ /**
1184
+ * How many events each kept event stands for, expressed as "1 in N".
826
1185
  *
827
- * Provides a stable join key across events, logs, and spans even when traces fragment.
828
- * Format: 16 hex chars (64 bits), crypto-random, URL-safe.
1186
+ * A query that counts sampled spans undercounts the population. Multiplying
1187
+ * each kept event by this rate restores the estimate. Autotel records the
1188
+ * attribute only when N exceeds 1, so fully captured spans stay clean.
1189
+ */
1190
+ const AUTOTEL_SAMPLING_RATE = "autotel.sampling.rate";
1191
+ /**
1192
+ * Spans an explicit `forceKeep()` claimed.
829
1193
  *
830
- * Lifecycle:
831
- * 1. Generated at boundary root (HTTP server span, message process span, cron job span)
832
- * 2. Reused within context (nested work shares it via AsyncLocalStorage)
833
- * 3. Propagated via baggage (optional, default OFF to avoid header bloat)
1194
+ * The tracing wrapper writes the sampler's tail verdict once the body has
1195
+ * run, which is after any `forceKeep()` inside it. Without this the verdict
1196
+ * overwrites the override and the span the caller insisted on is dropped.
1197
+ */
1198
+ /**
1199
+ * Baggage key that turns on full-fidelity capture for a request.
834
1200
  *
835
- * @example Basic usage
836
- * ```typescript
837
- * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
1201
+ * Baggage arrives on the request and propagates, so a gateway, a proxy, a
1202
+ * feature flag or a curl can turn this on for one user and it follows them
1203
+ * across every service. Nobody deploys anything to debug a live problem.
1204
+ */
1205
+ const AUTOTEL_DEBUG_BAGGAGE_KEY = "autotel.debug";
1206
+ const forceKeptSpans = /* @__PURE__ */ new WeakSet();
1207
+ function markForceKept(span) {
1208
+ forceKeptSpans.add(span);
1209
+ }
1210
+ function isForceKept(span) {
1211
+ return forceKeptSpans.has(span);
1212
+ }
1213
+ /**
1214
+ * Convert a keep probability into the "1 in N" rate reported on spans.
838
1215
  *
839
- * // Generate a new correlation ID
840
- * const id = generateCorrelationId();
841
- * // Returns: 'a1b2c3d4e5f67890'
1216
+ * Keeping 10% of traces means each survivor stands for 10, so the two numbers
1217
+ * are reciprocals and easy to publish the wrong way round.
1218
+ */
1219
+ function toSampleRate(probability) {
1220
+ return probability > 0 ? 1 / probability : 0;
1221
+ }
1222
+ /**
1223
+ * Map a string to a stable, evenly spread position in the unit interval.
842
1224
  *
843
- * // Get current correlation ID from context
844
- * const currentId = getCorrelationId();
1225
+ * Two processes that hash the same key reach the same number, which is what
1226
+ * lets independent services agree on one trace's sampling decision.
1227
+ *
1228
+ * The spread matters as much as the stability. Real sampling keys share long
1229
+ * prefixes: `user_1000`, `user_1001`, `checkout-trace-0001`. A plain
1230
+ * multiply-and-add hash lets that shared prefix dominate the high bits, so a
1231
+ * whole key family lands in one narrow band and a rate of 0.1 keeps all of
1232
+ * them or none of them. FNV-1a followed by the murmur3 finalizer mixes the
1233
+ * low bits back through the word, so keys that differ in one character land
1234
+ * far apart.
1235
+ */
1236
+ function hashUnitInterval(value) {
1237
+ let hash = 2166136261;
1238
+ for (let i = 0; i < value.length; i++) {
1239
+ hash ^= value.codePointAt(i) ?? 0;
1240
+ hash = Math.imul(hash, 16777619);
1241
+ }
1242
+ hash ^= hash >>> 16;
1243
+ hash = Math.imul(hash, 2246822507);
1244
+ hash ^= hash >>> 13;
1245
+ hash = Math.imul(hash, 3266489909);
1246
+ hash ^= hash >>> 16;
1247
+ return (hash >>> 0) / 4294967296;
1248
+ }
1249
+ /**
1250
+ * Simple random sampler
1251
+ *
1252
+ * @example
1253
+ * ```typescript
1254
+ * new RandomSampler(0.1) // Sample 10% of requests
845
1255
  * ```
846
1256
  */
1257
+ var RandomSampler = class {
1258
+ rate;
1259
+ constructor(rate) {
1260
+ this.rate = rate;
1261
+ if (rate < 0 || rate > 1) throw new Error("Sample rate must be between 0 and 1");
1262
+ }
1263
+ shouldSample(_context) {
1264
+ return Math.random() < this.rate;
1265
+ }
1266
+ sampleRate() {
1267
+ return toSampleRate(this.rate);
1268
+ }
1269
+ };
847
1270
  /**
848
- * AsyncLocalStorage for storing correlation ID
849
- * This allows correlation IDs to persist across async boundaries
1271
+ * Always sample (100% tracing)
850
1272
  */
851
- const correlationStorage = new node_async_hooks.AsyncLocalStorage();
1273
+ var AlwaysSampler = class {
1274
+ shouldSample(_context) {
1275
+ return true;
1276
+ }
1277
+ };
852
1278
  /**
853
- * Baggage key for correlation ID propagation
1279
+ * Never sample (0% tracing)
854
1280
  */
855
- const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
1281
+ var NeverSampler = class {
1282
+ shouldSample(_context) {
1283
+ return false;
1284
+ }
1285
+ };
856
1286
  /**
857
- * Generate a new correlation ID
1287
+ * Adaptive sampler that always traces errors and slow requests
858
1288
  *
859
- * Format: 16 hex chars (64 bits), crypto-random, URL-safe
1289
+ * This is the recommended sampler for production use.
1290
+ * It ensures you never miss critical issues while keeping costs down.
860
1291
  *
861
- * @returns A new correlation ID
1292
+ * Strategy:
1293
+ * - Always trace errors (critical for debugging)
1294
+ * - Always trace slow requests (performance issues)
1295
+ * - Use baseline sample rate for successful fast requests
1296
+ *
1297
+ * **IMPORTANT - Tail Sampling Requirement:**
1298
+ * This sampler uses tail sampling (makes decisions AFTER execution).
1299
+ * You MUST use TailSamplingSpanProcessor for it to work correctly:
1300
+ *
1301
+ * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
1302
+ * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
1303
+ *
1304
+ * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
1305
+ *
1306
+ * @see TailSamplingSpanProcessor
1307
+ * @see README.md "Tail Sampling with Custom Providers" section
862
1308
  *
863
1309
  * @example
864
1310
  * ```typescript
865
- * const id = generateCorrelationId();
866
- * // Returns: 'a1b2c3d4e5f67890'
1311
+ * new AdaptiveSampler({
1312
+ * baselineSampleRate: 0.1, // 10% of normal requests
1313
+ * slowThresholdMs: 1000, // Requests > 1s are "slow"
1314
+ * alwaysSampleErrors: true, // Always trace errors
1315
+ * alwaysSampleSlow: true // Always trace slow requests
1316
+ * })
867
1317
  * ```
868
1318
  */
869
- function generateCorrelationId() {
870
- const bytes = /* @__PURE__ */ new Uint8Array(8);
871
- crypto.getRandomValues(bytes);
872
- return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
873
- }
1319
+ var AdaptiveSampler = class {
1320
+ baselineSampleRate;
1321
+ slowThresholdMs;
1322
+ alwaysSampleErrors;
1323
+ alwaysSampleSlow;
1324
+ linksBased;
1325
+ linksRate;
1326
+ logger;
1327
+ samplingDecisions = /* @__PURE__ */ new WeakMap();
1328
+ operationResults = /* @__PURE__ */ new WeakMap();
1329
+ constructor(options = {}) {
1330
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1331
+ this.slowThresholdMs = options.slowThresholdMs ?? 1e3;
1332
+ this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
1333
+ this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
1334
+ this.linksBased = options.linksBased ?? false;
1335
+ this.linksRate = options.linksRate ?? 1;
1336
+ this.logger = options.logger;
1337
+ if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) throw new Error("Baseline sample rate must be between 0 and 1");
1338
+ if (this.linksRate < 0 || this.linksRate > 1) throw new Error("Links rate must be between 0 and 1");
1339
+ }
1340
+ needsTailSampling() {
1341
+ return true;
1342
+ }
1343
+ shouldSample(context) {
1344
+ const baselineDecision = Math.random() < this.baselineSampleRate;
1345
+ this.samplingDecisions.set(context.args, baselineDecision);
1346
+ return true;
1347
+ }
1348
+ /**
1349
+ * Check if any links point to sampled spans.
1350
+ *
1351
+ * A span is considered linked to a sampled span if any of its links
1352
+ * have trace_flags with the sampled bit set (0x01).
1353
+ *
1354
+ * @param links - Array of span links to check
1355
+ * @returns true if any linked span is sampled, false otherwise
1356
+ */
1357
+ hasSampledLink(links) {
1358
+ if (!links || links.length === 0) return false;
1359
+ return links.some((link) => link.context && (link.context.traceFlags & _opentelemetry_api.TraceFlags.SAMPLED) !== 0);
1360
+ }
1361
+ /**
1362
+ * Re-evaluate sampling decision after operation completes
1363
+ *
1364
+ * This allows us to always capture errors and slow requests,
1365
+ * even if they weren't initially sampled.
1366
+ *
1367
+ * @param context - Sampling context
1368
+ * @param result - Operation result
1369
+ * @returns true if this operation should be kept (not discarded)
1370
+ */
1371
+ shouldKeepTrace(context, result) {
1372
+ const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
1373
+ if (this.alwaysSampleErrors && !result.success) {
1374
+ if (!baselineDecision) this.logger?.debug({
1375
+ operation: context.operationName,
1376
+ error: result.error?.message
1377
+ }, "Adaptive sampling: Keeping error trace");
1378
+ return true;
1379
+ }
1380
+ if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
1381
+ if (!baselineDecision) this.logger?.debug({
1382
+ operation: context.operationName,
1383
+ duration: result.duration
1384
+ }, "Adaptive sampling: Keeping slow trace");
1385
+ return true;
1386
+ }
1387
+ if (this.linksBased && context.links && this.hasSampledLink(context.links)) {
1388
+ const keepLinked = Math.random() < this.linksRate;
1389
+ if (keepLinked && !baselineDecision) this.logger?.debug({
1390
+ operation: context.operationName,
1391
+ linkCount: context.links.length
1392
+ }, "Adaptive sampling: Keeping trace due to sampled link");
1393
+ return keepLinked;
1394
+ }
1395
+ return baselineDecision;
1396
+ }
1397
+ };
874
1398
  /**
875
- * Get the current correlation ID from context
1399
+ * User-based sampler for consistent tracing
876
1400
  *
877
- * Resolution order:
878
- * 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
879
- * 2. Baggage (if propagated from upstream)
880
- * 3. Active span's trace ID (first 16 chars as fallback)
881
- * 4. undefined (if not in any context)
1401
+ * Always samples requests from specific user IDs.
1402
+ * Useful for debugging specific user issues or monitoring VIP users.
882
1403
  *
883
- * @returns Current correlation ID or undefined
1404
+ * @example
1405
+ * ```typescript
1406
+ * new UserIdSampler({
1407
+ * baselineSampleRate: 0.01, // 1% of normal users
1408
+ * alwaysSampleUsers: ['vip_123'], // Always trace VIP users
1409
+ * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
1410
+ * })
1411
+ * ```
1412
+ */
1413
+ var UserIdSampler = class {
1414
+ baselineSampleRate;
1415
+ alwaysSampleUsers;
1416
+ extractUserId;
1417
+ logger;
1418
+ constructor(options) {
1419
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1420
+ this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
1421
+ this.extractUserId = options.extractUserId;
1422
+ this.logger = options.logger;
1423
+ }
1424
+ shouldSample(context) {
1425
+ const userId = this.extractUserId(context.args);
1426
+ if (userId && this.alwaysSampleUsers.has(userId)) {
1427
+ this.logger?.debug({
1428
+ operation: context.operationName,
1429
+ userId
1430
+ }, "Sampling user request");
1431
+ return true;
1432
+ }
1433
+ if (userId) return this.hashString(userId) < this.baselineSampleRate;
1434
+ return Math.random() < this.baselineSampleRate;
1435
+ }
1436
+ /**
1437
+ * Add user IDs to always-sample list
1438
+ */
1439
+ addAlwaysSampleUsers(...userIds) {
1440
+ for (const userId of userIds) this.alwaysSampleUsers.add(userId);
1441
+ }
1442
+ /**
1443
+ * Remove user IDs from always-sample list
1444
+ */
1445
+ removeAlwaysSampleUsers(...userIds) {
1446
+ for (const userId of userIds) this.alwaysSampleUsers.delete(userId);
1447
+ }
1448
+ /**
1449
+ * Simple hash function for consistent user sampling
1450
+ */
1451
+ hashString(str) {
1452
+ return hashUnitInterval(str);
1453
+ }
1454
+ };
1455
+ /**
1456
+ * Consistent sampler: every service reaches the same verdict for one trace.
1457
+ *
1458
+ * `RandomSampler` rolls the dice per process, so an upstream service can keep
1459
+ * a trace that its downstream drops, leaving a waterfall with holes in it.
1460
+ * Hashing a key that travels with the request removes the disagreement. Pass
1461
+ * the trace id, or any identifier every hop already shares.
884
1462
  *
885
1463
  * @example
886
1464
  * ```typescript
887
- * const id = getCorrelationId();
888
- * if (id) {
889
- * console.log('Correlation ID:', id);
890
- * }
1465
+ * new DeterministicSampler({
1466
+ * sampleRate: 0.1,
1467
+ * key: (context) => trace.getActiveSpan()?.spanContext().traceId,
1468
+ * })
891
1469
  * ```
892
1470
  */
893
- function getCorrelationId() {
894
- const storedId = correlationStorage.getStore()?.value;
895
- if (storedId) return storedId;
896
- const activeContext = _opentelemetry_api.context.active();
897
- const baggageEntry = _opentelemetry_api.propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
898
- if (baggageEntry?.value) return baggageEntry.value;
899
- const span = _opentelemetry_api.trace.getActiveSpan();
900
- if (span) return span.spanContext().traceId.slice(0, 16);
901
- }
1471
+ var DeterministicSampler = class {
1472
+ rate;
1473
+ key;
1474
+ constructor(options) {
1475
+ if (options.sampleRate < 0 || options.sampleRate > 1) throw new Error("Sample rate must be between 0 and 1");
1476
+ this.rate = options.sampleRate;
1477
+ this.key = options.key;
1478
+ }
1479
+ shouldSample(context) {
1480
+ const key = this.key(context);
1481
+ if (key === void 0) return Math.random() < this.rate;
1482
+ return hashUnitInterval(key) < this.rate;
1483
+ }
1484
+ sampleRate() {
1485
+ return toSampleRate(this.rate);
1486
+ }
1487
+ };
1488
+ /** Bucket for keys seen after the tracked map fills up. */
1489
+ const OVERFLOW_KEY = "__overflow__";
902
1490
  /**
903
- * Get or create a correlation ID
1491
+ * Per-key target-rate sampler for workloads with uneven traffic.
904
1492
  *
905
- * If a correlation ID exists in the current context, returns it.
906
- * Otherwise, generates a new one.
1493
+ * A single rate serves a skewed workload badly: 1% floods storage with the
1494
+ * busiest endpoint and still loses the rare tenant whose failures you need.
1495
+ * This sampler counts traffic per key over a rolling window, then sets each
1496
+ * key its own rate so every key contributes roughly `targetPerKey` events.
1497
+ * Quiet keys survive intact; loud keys get thinned.
1498
+ *
1499
+ * The first window keeps everything, because no traffic history exists yet.
1500
+ * Rates take effect from the second window onward.
1501
+ *
1502
+ * @example
1503
+ * ```typescript
1504
+ * new KeyTargetRateSampler({
1505
+ * key: (context) => context.operationName,
1506
+ * targetPerKey: 10, // ~10 events per key per window
1507
+ * windowMs: 30_000,
1508
+ * })
1509
+ * ```
1510
+ */
1511
+ var KeyTargetRateSampler = class {
1512
+ key;
1513
+ targetPerKey;
1514
+ windowMs;
1515
+ maxKeys;
1516
+ counts = /* @__PURE__ */ new Map();
1517
+ rates = /* @__PURE__ */ new Map();
1518
+ windowStart = Date.now();
1519
+ constructor(options) {
1520
+ this.key = options.key;
1521
+ this.targetPerKey = options.targetPerKey ?? 10;
1522
+ this.windowMs = options.windowMs ?? 3e4;
1523
+ this.maxKeys = options.maxKeys ?? 1e3;
1524
+ if (this.targetPerKey <= 0) throw new Error("Target per key must be greater than 0");
1525
+ if (this.windowMs <= 0) throw new Error("Window must be greater than 0");
1526
+ }
1527
+ /** Turn the window's observed counts into the next window's rates. */
1528
+ roll(now) {
1529
+ if (now - this.windowStart < this.windowMs) return;
1530
+ const rates = /* @__PURE__ */ new Map();
1531
+ for (const [key, count] of this.counts) rates.set(key, Math.max(1, count / this.targetPerKey));
1532
+ this.rates = rates;
1533
+ this.counts = /* @__PURE__ */ new Map();
1534
+ this.windowStart = now;
1535
+ }
1536
+ /**
1537
+ * Resolve the key, collapsing into one bucket once the map is full.
1538
+ *
1539
+ * An unbounded key function would otherwise grow the map without limit,
1540
+ * which turns a sampler meant to cut cost into a memory leak.
1541
+ */
1542
+ resolveKey(context) {
1543
+ const key = this.key(context) ?? OVERFLOW_KEY;
1544
+ if (this.counts.has(key) || this.counts.size < this.maxKeys) return key;
1545
+ return OVERFLOW_KEY;
1546
+ }
1547
+ shouldSample(context) {
1548
+ this.roll(Date.now());
1549
+ const key = this.resolveKey(context);
1550
+ this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
1551
+ const rate = this.rates.get(key) ?? 1;
1552
+ return rate <= 1 || Math.random() < 1 / rate;
1553
+ }
1554
+ sampleRate(context) {
1555
+ return this.rates.get(this.resolveKey(context)) ?? 1;
1556
+ }
1557
+ };
1558
+ /**
1559
+ * Composite sampler that combines multiple samplers
1560
+ *
1561
+ * Samples if ANY of the child samplers returns true.
1562
+ *
1563
+ * @example
1564
+ * ```typescript
1565
+ * new CompositeSampler([
1566
+ * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
1567
+ * new AdaptiveSampler({ baselineSampleRate: 0.1 })
1568
+ * ])
1569
+ * ```
1570
+ */
1571
+ var CompositeSampler = class {
1572
+ samplers;
1573
+ constructor(samplers) {
1574
+ this.samplers = samplers;
1575
+ if (samplers.length === 0) throw new Error("CompositeSampler requires at least one child sampler");
1576
+ }
1577
+ shouldSample(context) {
1578
+ return this.samplers.some((sampler) => sampler.shouldSample(context));
1579
+ }
1580
+ };
1581
+ /**
1582
+ * Feature flag sampler
1583
+ *
1584
+ * Always samples requests with specific feature flags enabled.
1585
+ * Perfect for correlating A/B test experiments with metrics.
1586
+ *
1587
+ * @example
1588
+ * ```typescript
1589
+ * new FeatureFlagSampler({
1590
+ * baselineSampleRate: 0.01,
1591
+ * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
1592
+ * extractFlags: (args, metadata) => metadata?.featureFlags
1593
+ * })
1594
+ * ```
1595
+ */
1596
+ var FeatureFlagSampler = class {
1597
+ baselineSampleRate;
1598
+ alwaysSampleFlags;
1599
+ extractFlags;
1600
+ logger;
1601
+ constructor(options) {
1602
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1603
+ this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
1604
+ this.extractFlags = options.extractFlags;
1605
+ this.logger = options.logger;
1606
+ }
1607
+ shouldSample(context) {
1608
+ const flags = this.extractFlags(context.args, context.metadata);
1609
+ if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
1610
+ this.logger?.debug({
1611
+ operation: context.operationName,
1612
+ flags
1613
+ }, "Sampling feature flag request");
1614
+ return true;
1615
+ }
1616
+ return Math.random() < this.baselineSampleRate;
1617
+ }
1618
+ /**
1619
+ * Add feature flags to always-sample list
1620
+ */
1621
+ addAlwaysSampleFlags(...flags) {
1622
+ for (const flag of flags) this.alwaysSampleFlags.add(flag);
1623
+ }
1624
+ /**
1625
+ * Remove feature flags from always-sample list
1626
+ */
1627
+ removeAlwaysSampleFlags(...flags) {
1628
+ for (const flag of flags) this.alwaysSampleFlags.delete(flag);
1629
+ }
1630
+ };
1631
+ /**
1632
+ * Sampling preset factories.
907
1633
  *
908
- * @returns Existing or new correlation ID
1634
+ * For most users, the string shorthand on `init()` is simpler:
1635
+ * ```typescript
1636
+ * init({ service: 'my-app', sampling: 'production' })
1637
+ * ```
909
1638
  *
910
- * @example
1639
+ * Use factories when you need to customize:
911
1640
  * ```typescript
912
- * const id = getOrCreateCorrelationId();
913
- * // Always returns a valid correlation ID
1641
+ * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
914
1642
  * ```
915
1643
  */
916
- function getOrCreateCorrelationId() {
917
- return getCorrelationId() ?? generateCorrelationId();
918
- }
1644
+ const samplingPresets = {
1645
+ /** Capture everything — best for local development and debugging */
1646
+ development: () => new AlwaysSampler(),
1647
+ /** Only bad outcomes — zero baseline, errors always kept */
1648
+ errorsOnly: () => new AdaptiveSampler({
1649
+ baselineSampleRate: 0,
1650
+ alwaysSampleErrors: true
1651
+ }),
1652
+ /**
1653
+ * Balanced production defaults — 10% baseline + errors + slow traces.
1654
+ * Pass overrides to tune (uses the same option names as AdaptiveSampler).
1655
+ */
1656
+ production: (overrides) => new AdaptiveSampler({
1657
+ baselineSampleRate: .1,
1658
+ alwaysSampleErrors: true,
1659
+ alwaysSampleSlow: true,
1660
+ slowThresholdMs: 1e3,
1661
+ ...overrides
1662
+ }),
1663
+ /** Disable sampling entirely */
1664
+ off: () => new NeverSampler()
1665
+ };
919
1666
  /**
920
- * Run a function with a specific correlation ID in context
921
- *
922
- * The correlation ID will be available via getCorrelationId() throughout
923
- * the execution of the function and any async operations it spawns.
924
- *
925
- * @param correlationId - Correlation ID to use
926
- * @param fn - Function to execute
927
- * @returns The return value of the function
1667
+ * Resolve a preset string to a Sampler instance.
1668
+ * Used internally by `init()` when `sampling` string is provided.
928
1669
  *
929
- * @example
930
- * ```typescript
931
- * await runWithCorrelationId('abc123', async () => {
932
- * // getCorrelationId() returns 'abc123' here
933
- * await processRequest();
934
- * });
935
- * ```
1670
+ * @throws Error if preset is not recognized
936
1671
  */
937
- function runWithCorrelationId(correlationId, fn) {
938
- return correlationStorage.run({ value: correlationId }, fn);
1672
+ function resolveSamplingPreset(preset) {
1673
+ switch (preset) {
1674
+ case "development": return samplingPresets.development();
1675
+ case "errors-only": return samplingPresets.errorsOnly();
1676
+ case "production": return samplingPresets.production();
1677
+ case "off": return samplingPresets.off();
1678
+ default: throw new Error(`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`);
1679
+ }
939
1680
  }
940
1681
  /**
941
- * Set correlation ID in the current context (mutates context)
1682
+ * Create a Link from W3C trace context headers (e.g., from a message queue).
942
1683
  *
943
- * Note: This updates the AsyncLocalStorage context. For proper scoping
944
- * across async boundaries, prefer runWithCorrelationId() instead.
1684
+ * This is useful for message consumers that need to link to the producer span.
1685
+ * The headers should contain at least a `traceparent` header in W3C format.
945
1686
  *
946
- * @param correlationId - Correlation ID to set
1687
+ * @param headers - Dictionary containing traceparent/tracestate headers
1688
+ * @param attributes - Optional attributes for the link
1689
+ * @returns Link object if context could be extracted, null otherwise
947
1690
  *
948
1691
  * @example
949
1692
  * ```typescript
950
- * setCorrelationId('abc123');
951
- * // Now getCorrelationId() returns 'abc123'
1693
+ * // In a Kafka consumer
1694
+ * const headers = { traceparent: '00-abc123...-def456...-01' };
1695
+ * const link = createLinkFromHeaders(headers);
1696
+ * if (link) {
1697
+ * // Use with tracer.startActiveSpan options or ctx.addLink()
1698
+ * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
1699
+ * }
952
1700
  * ```
953
1701
  */
954
- function setCorrelationId(correlationId) {
955
- enterOrRun(correlationStorage, correlationId);
1702
+ function createLinkFromHeaders(headers, attributes) {
1703
+ const traceparent = headers.traceparent || headers["traceparent"];
1704
+ if (!traceparent) return null;
1705
+ const spanContext = parseTraceparent(traceparent);
1706
+ if (!spanContext || !isValidSpanContext(spanContext)) return null;
1707
+ return {
1708
+ context: spanContext,
1709
+ attributes: attributes ?? {}
1710
+ };
956
1711
  }
957
1712
  /**
958
- * Set correlation ID in baggage for propagation
1713
+ * Extract Links from a batch of messages for fan-in scenarios.
959
1714
  *
960
- * This adds the correlation ID to the W3C baggage header, allowing it
961
- * to be propagated to downstream services.
962
- *
963
- * Note: Only use this when you explicitly want cross-service propagation.
964
- * Default is OFF to avoid header bloat.
1715
+ * Useful for batch processing where multiple producer spans should be linked.
1716
+ * This enables tracing causality in event-driven architectures where a single
1717
+ * consumer processes messages from multiple producers.
965
1718
  *
966
- * @param correlationId - Correlation ID to propagate
967
- * @returns New context with baggage set
1719
+ * @param messages - List of message objects
1720
+ * @param headersKey - Key in each message containing trace headers (default: 'headers')
1721
+ * @returns List of Link objects for all valid trace contexts
968
1722
  *
969
1723
  * @example
970
1724
  * ```typescript
971
- * const newContext = setCorrelationIdInBaggage('abc123');
972
- * context.with(newContext, () => {
973
- * // Baggage will be propagated in outgoing requests
1725
+ * // Processing a batch of SQS/Kafka messages
1726
+ * const messages = [
1727
+ * { body: '...', headers: { traceparent: '...' } },
1728
+ * { body: '...', headers: { traceparent: '...' } },
1729
+ * ];
1730
+ * const links = extractLinksFromBatch(messages);
1731
+ *
1732
+ * tracer.startActiveSpan('process.batch', { links }, span => {
1733
+ * for (const msg of messages) {
1734
+ * processMessage(msg);
1735
+ * }
974
1736
  * });
975
1737
  * ```
976
1738
  */
977
- function setCorrelationIdInBaggage(correlationId) {
978
- const activeContext = _opentelemetry_api.context.active();
979
- let baggage = _opentelemetry_api.propagation.getBaggage(activeContext) ?? _opentelemetry_api.propagation.createBaggage();
980
- baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
981
- return _opentelemetry_api.propagation.setBaggage(activeContext, baggage);
1739
+ function extractLinksFromBatch(messages, headersKey = "headers") {
1740
+ const links = [];
1741
+ for (const msg of messages) {
1742
+ const msgHeaders = require_values.asStringRecord(msg[headersKey]);
1743
+ if (msgHeaders) {
1744
+ const link = createLinkFromHeaders(msgHeaders, { "messaging.batch.message_index": links.length });
1745
+ if (link) links.push(link);
1746
+ }
1747
+ }
1748
+ return links;
982
1749
  }
983
1750
  /**
984
- * Get the correlation storage instance (for internal use in init/shutdown)
1751
+ * Parse W3C traceparent header into SpanContext
1752
+ * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
1753
+ *
1754
+ * @see https://www.w3.org/TR/trace-context/#traceparent-header
985
1755
  */
986
- function getCorrelationStorage() {
987
- return correlationStorage;
1756
+ function parseTraceparent(traceparent) {
1757
+ const match = traceparent.match(/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
1758
+ if (!match || match.length < 5) return null;
1759
+ const version = match[1];
1760
+ const traceId = match[2];
1761
+ const spanId = match[3];
1762
+ const flags = match[4];
1763
+ if (!version || !traceId || !spanId || !flags) return null;
1764
+ if (version === "ff") return null;
1765
+ return {
1766
+ traceId,
1767
+ spanId,
1768
+ traceFlags: Number.parseInt(flags, 16),
1769
+ isRemote: true
1770
+ };
1771
+ }
1772
+ /**
1773
+ * Check if a SpanContext is valid (has non-zero trace and span IDs)
1774
+ */
1775
+ function isValidSpanContext(spanContext) {
1776
+ if (!spanContext) return false;
1777
+ return spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId !== "0000000000000000";
1778
+ }
1779
+ /** True when the request in flight asked for full-fidelity capture. */
1780
+ function debugCaptureRequested() {
1781
+ const entry = _opentelemetry_api.propagation.getBaggage(getActiveContextWithBaggage())?.getEntry(AUTOTEL_DEBUG_BAGGAGE_KEY);
1782
+ return entry !== void 0 && entry.value !== "" && entry.value !== "0";
988
1783
  }
989
1784
 
990
1785
  //#endregion
991
- //#region src/event-queue.ts
992
- const DEFAULT_CONFIG$2 = {
993
- maxSize: 5e4,
994
- batchSize: 100,
995
- flushInterval: 1e4,
996
- maxRetries: 3,
997
- rateLimit: {
998
- maxEventsPerSecond: 100,
999
- burstCapacity: 200
1000
- }
1786
+ //#region src/init-logger.ts
1787
+ const LOG_LEVELS = {
1788
+ debug: 0,
1789
+ info: 1,
1790
+ warn: 2,
1791
+ error: 3
1001
1792
  };
1002
- /**
1003
- * Get subscriber name for metrics (stable, low-cardinality)
1004
- *
1005
- * Priority:
1006
- * 1. Explicit config: subscriber.name
1007
- * 2. Class static property (if available)
1008
- * 3. Fallback: lowercase class name without "Subscriber" suffix
1009
- */
1010
- function getSubscriberName(subscriber) {
1011
- if (subscriber.name) return subscriber.name.toLowerCase();
1012
- return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
1793
+ /** Silent logger used until and unless the application opts into diagnostics. */
1794
+ const silentLogger = {
1795
+ info: () => {},
1796
+ warn: () => {},
1797
+ error: () => {},
1798
+ debug: () => {}
1799
+ };
1800
+ /** Apply the resolved silence and minimum-level settings to a logger. */
1801
+ function wrapLogger(base, silent, minLevel) {
1802
+ if (silent) return silentLogger;
1803
+ const threshold = LOG_LEVELS[minLevel];
1804
+ const wrap = (fn, level) => {
1805
+ if (LOG_LEVELS[level] < threshold) return (() => {});
1806
+ return ((...args) => fn(...args));
1807
+ };
1808
+ return {
1809
+ debug: wrap(base.debug, "debug"),
1810
+ info: wrap(base.info, "info"),
1811
+ warn: wrap(base.warn, "warn"),
1812
+ error: wrap(base.error, "error")
1813
+ };
1013
1814
  }
1014
- /**
1015
- * Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
1016
- * a queue rebuilt from the same config must not reuse them.
1017
- */
1018
- const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
1019
- /**
1020
- * Events queue with batching and backpressure
1021
- *
1022
- * Features:
1023
- * - Batches events for efficient sending
1024
- * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
1025
- * - Exponential backoff retry
1026
- * - Rate limiting to prevent overwhelming subscribers
1027
- * - Graceful flush on shutdown
1028
- */
1029
- var EventQueue = class {
1030
- queue = [];
1031
- flushTimer = null;
1032
- config;
1033
- subscribers;
1034
- rateLimiter;
1035
- flushPromise = null;
1036
- isShuttingDown = false;
1037
- metrics = null;
1038
- observableCleanups = [];
1039
- subscriberHealthy = /* @__PURE__ */ new Map();
1040
- constructor(subscribers, config) {
1041
- const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
1042
- if (live.length < subscribers.length) getLogger().warn({ subscribers: subscribers.filter((s) => shutDownSubscribers.has(s)).map((s) => getSubscriberName(s)) }, "[autotel] Subscriber already shut down, events will not be delivered; pass new subscriber instances to init()");
1043
- this.subscribers = live;
1044
- this.config = {
1045
- ...DEFAULT_CONFIG$2,
1046
- ...config
1047
- };
1048
- this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
1049
- for (const subscriber of this.subscribers) {
1050
- const name = getSubscriberName(subscriber);
1051
- this.subscriberHealthy.set(name, true);
1052
- }
1053
- this.initMetrics();
1054
- }
1055
- /**
1056
- * Initialize OTel metrics for queue observability
1057
- */
1058
- initMetrics() {
1059
- const meter = require_config.getConfig().meter;
1060
- const queueSize = meter.createObservableGauge("autotel.event_delivery.queue.size", {
1061
- description: "Current number of events in the delivery queue",
1062
- unit: "count"
1063
- });
1064
- const queueSizeCallback = (observableResult) => {
1065
- observableResult.observe(this.queue.length);
1066
- };
1067
- queueSize.addCallback(queueSizeCallback);
1068
- this.observableCleanups.push(() => queueSize.removeCallback(queueSizeCallback));
1069
- const oldestAge = meter.createObservableGauge("autotel.event_delivery.queue.oldest_age_ms", {
1070
- description: "Age of the oldest event in the queue in milliseconds",
1071
- unit: "ms"
1072
- });
1073
- const oldestAgeCallback = (observableResult) => {
1074
- if (this.queue.length > 0) {
1075
- const oldest = this.queue[0];
1076
- const ageMs = Date.now() - oldest.timestamp;
1077
- observableResult.observe(ageMs);
1078
- } else observableResult.observe(0);
1079
- };
1080
- oldestAge.addCallback(oldestAgeCallback);
1081
- this.observableCleanups.push(() => oldestAge.removeCallback(oldestAgeCallback));
1082
- const delivered = meter.createCounter("autotel.event_delivery.queue.delivered", {
1083
- description: "Number of events successfully delivered to subscribers",
1084
- unit: "count"
1085
- });
1086
- const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
1087
- description: "Number of events that failed delivery after all retry attempts",
1088
- unit: "count"
1089
- });
1090
- const dropped = meter.createCounter("autotel.event_delivery.queue.dropped", {
1091
- description: "Number of events dropped from the queue",
1092
- unit: "count"
1093
- });
1094
- const latency = meter.createHistogram("autotel.event_delivery.queue.latency_ms", {
1095
- description: "Event delivery latency from enqueue to successful send",
1096
- unit: "ms"
1097
- });
1098
- const subscriberHealth = meter.createObservableGauge("autotel.event_delivery.subscriber.health", {
1099
- description: "Subscriber health status (1=healthy, 0=unhealthy)",
1100
- unit: "1"
1101
- });
1102
- const subscriberHealthCallback = (observableResult) => {
1103
- for (const [subscriberName, isHealthy] of this.subscriberHealthy) observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName });
1104
- };
1105
- subscriberHealth.addCallback(subscriberHealthCallback);
1106
- this.observableCleanups.push(() => subscriberHealth.removeCallback(subscriberHealthCallback));
1107
- this.metrics = {
1108
- queueSize,
1109
- oldestAge,
1110
- delivered,
1111
- failed,
1112
- dropped,
1113
- latency,
1114
- subscriberHealth
1115
- };
1116
- }
1117
- /**
1118
- * Record a dropped event with reason and emit debug breadcrumb
1119
- */
1120
- recordDropped(reason, event, subscriberName) {
1121
- const attrs = { reason };
1122
- if (subscriberName) attrs.subscriber = subscriberName;
1123
- this.metrics?.dropped.add(1, attrs);
1124
- const logLevel = reason === "payload_invalid" ? "error" : "warn";
1125
- const logger = getLogger();
1126
- if (logLevel === "error") logger.error({
1127
- eventName: event?.name,
1128
- subscriber: subscriberName,
1129
- reason,
1130
- correlationId: event?._correlationId,
1131
- traceId: event?._traceId
1132
- }, `[autotel] Event dropped: ${reason}`);
1133
- else logger.warn({
1134
- eventName: event?.name,
1135
- subscriber: subscriberName,
1136
- reason,
1137
- correlationId: event?._correlationId,
1138
- traceId: event?._traceId
1139
- }, `[autotel] Event dropped: ${reason}`);
1815
+
1816
+ //#endregion
1817
+ //#region src/posthog-logs.ts
1818
+ var RedactingLogRecordProcessor = class {
1819
+ wrapped;
1820
+ redact;
1821
+ constructor(wrapped, redact) {
1822
+ this.wrapped = wrapped;
1823
+ this.redact = redact;
1140
1824
  }
1141
- /**
1142
- * Record permanent delivery failure (after all retries exhausted)
1143
- * Increments failed counter and logs error
1144
- */
1145
- recordFailed(event, subscriberName, error) {
1146
- this.metrics?.failed.add(1, { subscriber: subscriberName });
1147
- this.subscriberHealthy.set(subscriberName, false);
1148
- getLogger().error({
1149
- eventName: event.name,
1150
- subscriber: subscriberName,
1151
- correlationId: event._correlationId,
1152
- traceId: event._traceId,
1153
- err: error
1154
- }, `[autotel] Event delivery failed after all retries`);
1825
+ onEmit(logRecord, context) {
1826
+ const body = require_values.asString(logRecord.body);
1827
+ if (body) logRecord.body = this.redact(body);
1828
+ if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
1829
+ const text = require_values.asString(value);
1830
+ if (text !== void 0) logRecord.attributes[key] = this.redact(text);
1831
+ else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
1832
+ const entry = require_values.asString(item);
1833
+ return entry === void 0 ? item : this.redact(entry);
1834
+ });
1835
+ }
1836
+ this.wrapped.onEmit(logRecord, context);
1155
1837
  }
1156
- /**
1157
- * Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
1158
- * Used during retry attempts - only recordFailed should increment the counter
1159
- */
1160
- markSubscriberUnhealthy(subscriberName) {
1161
- this.subscriberHealthy.set(subscriberName, false);
1838
+ shutdown() {
1839
+ return this.wrapped.shutdown();
1162
1840
  }
1163
- /**
1164
- * Record successful delivery
1165
- */
1166
- recordDelivered(event, subscriberName, startTime) {
1167
- const latencyMs = Date.now() - startTime;
1168
- this.metrics?.delivered.add(1, { subscriber: subscriberName });
1169
- this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
1170
- this.subscriberHealthy.set(subscriberName, true);
1841
+ forceFlush() {
1842
+ return this.wrapped.forceFlush();
1171
1843
  }
1172
- /**
1173
- * Enqueue an event for sending
1174
- *
1175
- * Backpressure policy:
1176
- * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
1177
- */
1178
- enqueue(event) {
1179
- if (this.isShuttingDown) {
1180
- this.recordDropped("shutdown", event);
1181
- return;
1182
- }
1183
- if (this.queue.length >= this.config.maxSize) {
1184
- const droppedEvent = this.queue.shift();
1185
- this.recordDropped("rate_limit", droppedEvent);
1186
- getLogger().warn({ droppedEvent: droppedEvent?.name }, `[autotel] Events queue full (${this.config.maxSize} events). Dropping oldest event. Events are being produced faster than they can be sent. Check your subscribers or reduce tracking frequency.`);
1187
- }
1188
- const enrichedEvent = {
1189
- ...event,
1190
- _correlationId: event._correlationId || getOrCreateCorrelationId()
1191
- };
1192
- this.queue.push(enrichedEvent);
1193
- this.scheduleBatchFlush();
1844
+ };
1845
+ /**
1846
+ * Build log record processors for PostHog OTLP logs integration.
1847
+ *
1848
+ * Resolution order:
1849
+ * 1. config.url if provided
1850
+ * 2. POSTHOG_LOGS_URL env var
1851
+ * 3. Empty array (disabled)
1852
+ */
1853
+ function buildPostHogLogProcessors(config, stringRedactor) {
1854
+ const url = config?.url || process.env.POSTHOG_LOGS_URL;
1855
+ if (!url) return [];
1856
+ const sdkLogs = require_node_require.safeRequire("@opentelemetry/sdk-logs");
1857
+ const exporterModule = require_node_require.safeRequire("@opentelemetry/exporter-logs-otlp-http");
1858
+ if (!sdkLogs || !exporterModule) return [];
1859
+ const exporter = new exporterModule.OTLPLogExporter({ url });
1860
+ let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
1861
+ if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
1862
+ return [processor];
1863
+ }
1864
+
1865
+ //#endregion
1866
+ //#region src/tail-sampling-processor.ts
1867
+ var TailSamplingSpanProcessor = class {
1868
+ wrappedProcessor;
1869
+ constructor(wrappedProcessor) {
1870
+ this.wrappedProcessor = wrappedProcessor;
1194
1871
  }
1195
- /**
1196
- * Schedule a batch flush if not already scheduled
1197
- */
1198
- scheduleBatchFlush() {
1199
- if (this.flushTimer || this.flushPromise) return;
1200
- this.flushTimer = setTimeout(() => {
1201
- this.flushTimer = null;
1202
- this.flushBatch();
1203
- }, this.config.flushInterval);
1872
+ onStart(span, parentContext) {
1873
+ this.wrappedProcessor.onStart(span, parentContext);
1204
1874
  }
1205
- /**
1206
- * Flush a batch of events
1207
- * Uses promise-based concurrency control to prevent race conditions
1208
- */
1209
- async flushBatch() {
1210
- if (this.queue.length === 0) return;
1211
- if (this.flushPromise) {
1212
- await this.flushPromise;
1213
- return;
1214
- }
1215
- this.flushPromise = this.doFlushBatch();
1216
- try {
1217
- await this.flushPromise;
1218
- } finally {
1219
- this.flushPromise = null;
1220
- if (this.queue.length > 0) this.scheduleBatchFlush();
1221
- }
1875
+ onEnd(span) {
1876
+ const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
1877
+ const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
1878
+ if (tailEvaluated === true && shouldKeep === false) return;
1879
+ this.wrappedProcessor.onEnd(span);
1222
1880
  }
1223
- /**
1224
- * Internal flush implementation
1225
- */
1226
- async doFlushBatch() {
1227
- const batch = this.queue.splice(0, this.config.batchSize);
1228
- await this.sendWithRetry(batch, this.config.maxRetries);
1881
+ forceFlush() {
1882
+ return this.wrappedProcessor.forceFlush();
1229
1883
  }
1230
- /**
1231
- * Send events with exponential backoff retry
1232
- * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
1233
- * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
1234
- */
1235
- async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
1236
- const failedDeliveries = await this.sendToSubscribers(events, subscribersByEventIndex);
1237
- if (failedDeliveries.length > 0) if (retriesLeft > 0) {
1238
- const failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
1239
- const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
1240
- const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
1241
- for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
1242
- const set = /* @__PURE__ */ new Set();
1243
- for (const { eventIndex, subscriberName } of failedDeliveries) if (eventIndex === origIndex) set.add(subscriberName);
1244
- failedSubscribersByRetryIndex.set(j, set);
1245
- }
1246
- const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
1247
- await new Promise((resolve) => setTimeout(resolve, delay));
1248
- return this.sendWithRetry(eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex);
1249
- } else {
1250
- for (const { eventIndex, subscriberName, error } of failedDeliveries) {
1251
- const event = events[eventIndex];
1252
- if (event) this.recordFailed(event, subscriberName, error);
1253
- }
1254
- const failedSubscriberNames = [...new Set(failedDeliveries.map((f) => f.subscriberName))];
1255
- getLogger().error({
1256
- failedSubscribers: failedSubscriberNames,
1257
- retriesAttempted: this.config.maxRetries
1258
- }, "[autotel] Failed to send events after retries");
1259
- }
1884
+ shutdown() {
1885
+ return this.wrappedProcessor.shutdown();
1260
1886
  }
1261
- /**
1262
- * Send events to configured subscribers with rate limiting and metrics.
1263
- * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
1264
- * Returns per-event, per-subscriber failures (empty if all succeeded).
1265
- */
1266
- async sendToSubscribers(events, subscribersByEventIndex) {
1267
- const failedDeliveries = [];
1268
- const sendOne = async (event, eventIndex) => {
1269
- const subscriberNames = subscribersByEventIndex?.get(eventIndex);
1270
- const failures = await this.sendEventToSubscribers(event, subscriberNames ?? void 0);
1271
- for (const failure of failures) failedDeliveries.push({
1272
- eventIndex,
1273
- subscriberName: failure.subscriberName,
1274
- error: failure.error
1275
- });
1276
- };
1277
- if (!this.rateLimiter) {
1278
- for (const [i, event] of events.entries()) if (event) await sendOne(event, i);
1279
- return failedDeliveries;
1280
- }
1281
- for (const [i, event] of events.entries()) {
1282
- await this.rateLimiter.waitForToken();
1283
- if (event) await sendOne(event, i);
1284
- }
1285
- return failedDeliveries;
1887
+ };
1888
+
1889
+ //#endregion
1890
+ //#region src/baggage-span-processor.ts
1891
+ /**
1892
+ * Span processor that automatically copies baggage entries to span attributes
1893
+ *
1894
+ * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
1895
+ * without manually calling ctx.setAttribute() for each baggage entry.
1896
+ *
1897
+ * @example Enable in init()
1898
+ * ```typescript
1899
+ * init({
1900
+ * service: 'my-app',
1901
+ * baggage: true // Uses default 'baggage.' prefix
1902
+ * });
1903
+ *
1904
+ * // Now baggage automatically appears as span attributes
1905
+ * await withBaggage({
1906
+ * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
1907
+ * fn: async () => {
1908
+ * // Span has baggage.tenant.id and baggage.user.id attributes!
1909
+ * }
1910
+ * });
1911
+ * ```
1912
+ *
1913
+ * @example Custom prefix
1914
+ * ```typescript
1915
+ * init({
1916
+ * service: 'my-app',
1917
+ * baggage: 'ctx' // Uses 'ctx.' prefix
1918
+ * });
1919
+ * // Creates attributes: ctx.tenant.id, ctx.user.id
1920
+ * ```
1921
+ */
1922
+ var BaggageSpanProcessor = class {
1923
+ prefix;
1924
+ constructor(options = {}) {
1925
+ this.prefix = options.prefix ?? "baggage.";
1286
1926
  }
1287
- /**
1288
- * Send a single event to subscribers.
1289
- * - When subscriberNames is undefined (initial attempt): send to all subscribers.
1290
- * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
1291
- * Returns list of subscribers that failed (empty if all succeeded).
1292
- */
1293
- async sendEventToSubscribers(event, subscriberNames) {
1294
- const startTime = event.timestamp;
1295
- const failures = [];
1296
- const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter((s) => subscriberNames.has(getSubscriberName(s)));
1297
- const results = await Promise.allSettled(subscribersToTry.map(async (subscriber) => {
1298
- const subscriberName = getSubscriberName(subscriber);
1299
- try {
1300
- await subscriber.trackEvent(event.name, event.attributes, {
1301
- autotel: event.autotel,
1302
- schema: event.schema
1303
- });
1304
- this.recordDelivered(event, subscriberName, startTime);
1305
- return {
1306
- subscriberName,
1307
- success: true
1308
- };
1309
- } catch (error) {
1310
- this.markSubscriberUnhealthy(subscriberName);
1311
- return {
1312
- subscriberName,
1313
- success: false,
1314
- error: error instanceof Error ? error : void 0
1315
- };
1316
- }
1317
- }));
1318
- for (const result of results) if (result.status === "fulfilled" && !result.value.success) failures.push({
1319
- subscriberName: result.value.subscriberName,
1320
- error: result.value.error
1321
- });
1322
- return failures;
1927
+ onStart(span, parentContext) {
1928
+ let baggage = _opentelemetry_api.propagation.getBaggage(parentContext);
1929
+ if (!baggage) baggage = _opentelemetry_api.propagation.getBaggage(_opentelemetry_api.context.active());
1930
+ if (!baggage) try {
1931
+ const { getActiveContextWithBaggage } = require_node_require.requireModule("./trace-context");
1932
+ const storedContext = getActiveContextWithBaggage();
1933
+ baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
1934
+ } catch {}
1935
+ if (!baggage) return;
1936
+ for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
1323
1937
  }
1324
- /**
1325
- * Flush all remaining events. Queue remains usable after flush (e.g. for
1326
- * auto-flush at root span end). Use shutdown() when tearing down the queue.
1327
- */
1328
- async flush() {
1329
- if (this.flushTimer) {
1330
- clearTimeout(this.flushTimer);
1331
- this.flushTimer = null;
1938
+ onEnd(_span) {}
1939
+ async shutdown() {}
1940
+ async forceFlush() {}
1941
+ };
1942
+
1943
+ //#endregion
1944
+ //#region src/redact-values.ts
1945
+ /** Standalone string redaction for use outside the span processor pipeline. */
1946
+ function createStringRedactor(config) {
1947
+ const resolved = typeof config === "string" ? require_attribute_redacting_processor.REDACTOR_PRESETS[config] : config;
1948
+ const valuePatterns = resolved.valuePatterns ?? [];
1949
+ const defaultReplacement = resolved.replacement ?? "[REDACTED]";
1950
+ return (value) => {
1951
+ let result = value;
1952
+ for (const { pattern, replacement, mask } of valuePatterns) {
1953
+ pattern.lastIndex = 0;
1954
+ result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
1332
1955
  }
1333
- if (this.flushPromise) await this.flushPromise;
1334
- while (this.queue.length > 0) await this.doFlushBatch();
1956
+ return result;
1957
+ };
1958
+ }
1959
+
1960
+ //#endregion
1961
+ //#region src/env-config.ts
1962
+ /**
1963
+ * Validate URL format
1964
+ */
1965
+ function isValidUrl(urlString) {
1966
+ try {
1967
+ const url = new URL(urlString);
1968
+ return url.protocol === "http:" || url.protocol === "https:";
1969
+ } catch {
1970
+ return false;
1335
1971
  }
1336
- /**
1337
- * Flush remaining events and permanently disable the queue (reject new events).
1338
- * Use for process/SDK shutdown; use flush() for periodic or span-end drain.
1339
- */
1340
- async shutdown() {
1341
- this.isShuttingDown = true;
1342
- await this.flush();
1343
- const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
1344
- if (!subscriber.shutdown) return;
1345
- shutDownSubscribers.add(subscriber);
1346
- return subscriber.shutdown();
1347
- }));
1348
- for (const [index, drain] of drains.entries()) {
1349
- if (drain.status !== "rejected") continue;
1350
- const subscriberName = getSubscriberName(this.subscribers[index]);
1351
- this.subscriberHealthy.set(subscriberName, false);
1352
- getLogger().error({
1353
- subscriber: subscriberName,
1354
- err: drain.reason
1355
- }, "[autotel] Subscriber shutdown failed, buffered events may be lost");
1356
- }
1972
+ }
1973
+ /**
1974
+ * Resolve OpenTelemetry environment variables from process.env
1975
+ */
1976
+ function resolveOtelEnv() {
1977
+ const env = {};
1978
+ if (process.env.OTEL_SERVICE_NAME) {
1979
+ const value = process.env.OTEL_SERVICE_NAME.trim();
1980
+ if (value) env.OTEL_SERVICE_NAME = value;
1357
1981
  }
1358
- /**
1359
- * Cleanup observable metric callbacks to prevent memory leaks
1360
- * Call this when destroying the EventQueue instance
1361
- */
1362
- cleanup() {
1363
- for (const cleanupFn of this.observableCleanups) try {
1364
- cleanupFn();
1365
- } catch {}
1366
- this.observableCleanups = [];
1982
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
1983
+ const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
1984
+ if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
1367
1985
  }
1368
- /**
1369
- * Get queue size (for testing/debugging)
1370
- */
1371
- size() {
1372
- return this.queue.length;
1986
+ if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
1987
+ const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
1988
+ if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
1373
1989
  }
1374
- /**
1375
- * Get subscriber health status (for testing/debugging)
1376
- */
1377
- getSubscriberHealth() {
1378
- return new Map(this.subscriberHealthy);
1990
+ if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
1991
+ const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
1992
+ if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
1379
1993
  }
1380
- /**
1381
- * Check if a specific subscriber is healthy
1382
- */
1383
- isSubscriberHealthy(subscriberName) {
1384
- return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
1994
+ if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
1995
+ const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
1996
+ if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
1997
+ else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
1385
1998
  }
1386
- /**
1387
- * Manually mark a subscriber as healthy or unhealthy
1388
- * (used for circuit breaker integration)
1389
- */
1390
- setSubscriberHealth(subscriberName, healthy) {
1391
- this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
1999
+ if (process.env.OTEL_TRACES_SAMPLER) {
2000
+ const value = process.env.OTEL_TRACES_SAMPLER.trim();
2001
+ if (value) env.OTEL_TRACES_SAMPLER = value;
1392
2002
  }
1393
- };
1394
-
1395
- //#endregion
1396
- //#region src/validation.ts
1397
- const DEFAULT_CONFIG$1 = {
1398
- maxEventNameLength: 100,
1399
- maxAttributeKeyLength: 100,
1400
- maxAttributeValueLength: 1e3,
1401
- maxAttributeCount: 50,
1402
- maxNestingDepth: 3,
1403
- sensitivePatterns: [
1404
- /password/i,
1405
- /secret/i,
1406
- /token/i,
1407
- /api[_-]?key/i,
1408
- /access[_-]?key/i,
1409
- /private[_-]?key/i,
1410
- /auth/i,
1411
- /credential/i,
1412
- /ssn/i,
1413
- /credit[_-]?card/i
1414
- ]
1415
- };
1416
- var ValidationError = class extends Error {
1417
- constructor(message) {
1418
- super(message);
1419
- this.name = "ValidationError";
2003
+ if (process.env.OTEL_TRACES_SAMPLER_ARG) {
2004
+ const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
2005
+ if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
2006
+ }
2007
+ return env;
2008
+ }
2009
+ function parseRatioSamplerArg(samplerName, samplerArg) {
2010
+ if (samplerArg === void 0) return 1;
2011
+ const ratio = Number(samplerArg);
2012
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
2013
+ console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
2014
+ return 1;
2015
+ }
2016
+ return ratio;
2017
+ }
2018
+ function warnOnUnusedSamplerArg(samplerName, samplerArg) {
2019
+ if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
2020
+ }
2021
+ function createSamplerFromEnv(env) {
2022
+ const samplerName = env.OTEL_TRACES_SAMPLER;
2023
+ if (!samplerName) return;
2024
+ switch (samplerName) {
2025
+ case "always_on":
2026
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2027
+ return new _opentelemetry_sdk_trace_base.AlwaysOnSampler();
2028
+ case "always_off":
2029
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2030
+ return new _opentelemetry_sdk_trace_base.AlwaysOffSampler();
2031
+ case "traceidratio": return new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
2032
+ case "parentbased_always_on":
2033
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2034
+ return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOnSampler() });
2035
+ case "parentbased_always_off":
2036
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2037
+ return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOffSampler() });
2038
+ case "parentbased_traceidratio": return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
2039
+ case "jaeger_remote":
2040
+ case "parentbased_jaeger_remote":
2041
+ case "xray":
2042
+ console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
2043
+ return;
2044
+ default:
2045
+ console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
2046
+ return;
2047
+ }
2048
+ }
2049
+ /**
2050
+ * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
2051
+ * Example: "service.version=1.0.0,deployment.environment=production"
2052
+ */
2053
+ function parseResourceAttributes(input) {
2054
+ if (!input || input.trim() === "") return {};
2055
+ const attributes = {};
2056
+ const pairs = input.split(",");
2057
+ for (const pair of pairs) {
2058
+ const trimmedPair = pair.trim();
2059
+ if (!trimmedPair) continue;
2060
+ const equalIndex = trimmedPair.indexOf("=");
2061
+ if (equalIndex === -1) continue;
2062
+ const key = trimmedPair.slice(0, equalIndex).trim();
2063
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2064
+ if (key && value) attributes[key] = value;
2065
+ }
2066
+ return attributes;
2067
+ }
2068
+ /**
2069
+ * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
2070
+ * Example: "api-key=secret123,x-custom-header=value"
2071
+ */
2072
+ function parseOtlpHeaders(input) {
2073
+ if (!input || input.trim() === "") return {};
2074
+ const headers = {};
2075
+ const pairs = input.split(",");
2076
+ for (const pair of pairs) {
2077
+ const trimmedPair = pair.trim();
2078
+ if (!trimmedPair) continue;
2079
+ const equalIndex = trimmedPair.indexOf("=");
2080
+ if (equalIndex === -1) continue;
2081
+ const key = trimmedPair.slice(0, equalIndex).trim();
2082
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2083
+ if (key && value) headers[key] = value;
2084
+ }
2085
+ return headers;
2086
+ }
2087
+ /**
2088
+ * Convert resolved environment variables to config
2089
+ */
2090
+ function envToConfig(env) {
2091
+ const config = {};
2092
+ if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
2093
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
2094
+ if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
2095
+ if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
2096
+ const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
2097
+ if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
2098
+ const sampler = createSamplerFromEnv(env);
2099
+ if (sampler) config.otelSampler = sampler;
2100
+ return config;
2101
+ }
2102
+ /**
2103
+ * Main function to resolve config from environment variables
2104
+ */
2105
+ function resolveConfigFromEnv() {
2106
+ return envToConfig(resolveOtelEnv());
2107
+ }
2108
+
2109
+ //#endregion
2110
+ //#region src/yaml-config.ts
2111
+ /**
2112
+ * YAML configuration loader for autotel
2113
+ *
2114
+ * Supports:
2115
+ * - Auto-discovery of autotel.yaml in cwd
2116
+ * - AUTOTEL_CONFIG_FILE env var override
2117
+ * - Environment variable substitution: ${env:VAR} and ${env:VAR:-default}
2118
+ *
2119
+ * @example Auto-discovery
2120
+ * ```yaml
2121
+ * # autotel.yaml in project root
2122
+ * service:
2123
+ * name: my-service
2124
+ * exporter:
2125
+ * endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}
2126
+ * ```
2127
+ *
2128
+ * @example Explicit path
2129
+ * ```bash
2130
+ * AUTOTEL_CONFIG_FILE=./config/otel.yaml tsx --import autotel/auto src/index.ts
2131
+ * ```
2132
+ */
2133
+ /**
2134
+ * Lazy-load yaml parser (optional peer dependency)
2135
+ * Only loads when a YAML config file is actually found
2136
+ */
2137
+ function loadYamlParser() {
2138
+ try {
2139
+ return require_node_require.requireModule("yaml").parse;
2140
+ } catch {
2141
+ throw new Error("YAML parser not found. Install with: pnpm add yaml");
1420
2142
  }
1421
- };
2143
+ }
1422
2144
  /**
1423
- * Validate and sanitize event name
1424
- * Throws ValidationError if invalid
2145
+ * Environment variable substitution regex
2146
+ * Matches ${env:VAR_NAME} and ${env:VAR_NAME:-default}
1425
2147
  */
1426
- function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
1427
- if (require_values.asString(eventName) === void 0) throw new ValidationError(`Event name must be a string, got ${require_values.describeValue(eventName)}`);
1428
- const trimmed = eventName.trim();
1429
- if (trimmed.length === 0) throw new ValidationError("Event name cannot be empty");
1430
- if (trimmed.length > config.maxEventNameLength) throw new ValidationError(`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`);
1431
- if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) throw new ValidationError(`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`);
1432
- return trimmed;
2148
+ const ENV_VAR_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
2149
+ /**
2150
+ * Substitute ${env:VAR} and ${env:VAR:-default} in a string
2151
+ *
2152
+ * @param value - String potentially containing env var references
2153
+ * @returns String with env vars substituted
2154
+ *
2155
+ * @example
2156
+ * substituteEnvVars('${env:NODE_ENV:-development}')
2157
+ * // Returns 'production' if NODE_ENV=production, else 'development'
2158
+ */
2159
+ function substituteEnvVars(value) {
2160
+ return value.replaceAll(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
2161
+ const envValue = process.env[varName];
2162
+ if (envValue !== void 0) return envValue;
2163
+ if (defaultValue !== void 0) return defaultValue;
2164
+ console.warn(`[autotel] Environment variable ${varName} not set and no default provided`);
2165
+ return "";
2166
+ });
1433
2167
  }
1434
2168
  /**
1435
- * Validate and sanitize attributes
1436
- * Returns sanitized attributes (sensitive data redacted)
2169
+ * Recursively substitute env vars in an object
2170
+ *
2171
+ * @param obj - Object to process
2172
+ * @returns Object with all string values having env vars substituted
1437
2173
  */
1438
- function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
1439
- if (attributes === void 0 || attributes === null) return;
1440
- if (!require_values.asRecord(attributes)) throw new ValidationError("Attributes must be an object");
1441
- const keys = Object.keys(attributes);
1442
- if (keys.length > config.maxAttributeCount) throw new ValidationError(`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`);
1443
- const sanitized = {};
1444
- for (const key of keys) {
1445
- if (key.length > config.maxAttributeKeyLength) throw new ValidationError(`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`);
1446
- const value = attributes[key];
1447
- if (isSensitiveString(key, value, config)) {
1448
- sanitized[key] = "[REDACTED]";
1449
- continue;
1450
- }
1451
- sanitized[key] = sanitizeValue(value, config, 1);
2174
+ function substituteEnvVarsDeep(value) {
2175
+ if (Array.isArray(value)) return value.map(substituteEnvVarsDeep);
2176
+ const mapping = asYamlMapping(value);
2177
+ if (mapping) {
2178
+ const result = {};
2179
+ for (const [key, entry] of Object.entries(mapping)) result[key] = substituteEnvVarsDeep(entry);
2180
+ return result;
1452
2181
  }
1453
- return sanitized;
2182
+ const text = require_values.asString(value);
2183
+ return text === void 0 ? value : substituteEnvVars(text);
2184
+ }
2185
+ /** The mapping a YAML value is, when it is one. */
2186
+ function asYamlMapping(value) {
2187
+ return require_values.asRecord(value) === void 0 ? void 0 : value;
1454
2188
  }
1455
2189
  /**
1456
- * Whether this key/value pair is a credential to redact. Strings only:
1457
- * numeric and boolean values are not credentials, and replacing them with the
1458
- * literal '[REDACTED]' leaks no signal while breaking every consumer that
1459
- * treats them as a number.
2190
+ * A YAML document read into the config shape, with `${env:...}` placeholders
2191
+ * resolved.
2192
+ *
2193
+ * This is the boundary: the file is text until here. Every field of
2194
+ * YamlConfig is optional, so any document satisfies it, and
2195
+ * `yamlToAutotelConfig` picks out only the fields it understands.
1460
2196
  */
1461
- function isSensitiveString(key, value, config) {
1462
- return require_values.asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
2197
+ function parseYamlConfig(content) {
2198
+ return asYamlMapping(substituteEnvVarsDeep(loadYamlParser()(content))) ?? {};
1463
2199
  }
1464
2200
  /**
1465
- * Sanitize attribute value (recursive)
2201
+ * Find YAML config file path
2202
+ *
2203
+ * Priority:
2204
+ * 1. AUTOTEL_CONFIG_FILE env var (explicit path)
2205
+ * 2. autotel.yaml in cwd (convention)
2206
+ * 3. autotel.yml in cwd (alternative extension)
2207
+ *
2208
+ * @returns File path if found, null otherwise
1466
2209
  */
1467
- function sanitizeValue(value, config, depth) {
1468
- if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
1469
- if (value === null || value === void 0) return value;
1470
- const text = require_values.asString(value);
1471
- if (text !== void 0) return text.length > config.maxAttributeValueLength ? text.slice(0, config.maxAttributeValueLength) + "..." : text;
1472
- const scalar = require_values.asNumber(value) ?? require_values.asBoolean(value);
1473
- if (scalar !== void 0) return scalar;
1474
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
1475
- const record = require_values.asRecord(value);
1476
- if (record) try {
1477
- JSON.stringify(record);
1478
- const sanitized = {};
1479
- for (const [key, nested] of Object.entries(record)) sanitized[key] = isSensitiveString(key, nested, config) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
1480
- return sanitized;
1481
- } catch {
1482
- return "[CIRCULAR]";
2210
+ function findConfigFile() {
2211
+ const envPath = process.env.AUTOTEL_CONFIG_FILE;
2212
+ if (envPath) {
2213
+ const resolved = node_path.default.resolve(envPath);
2214
+ if (node_fs.existsSync(resolved)) return resolved;
2215
+ console.warn(`[autotel] Config file not found: ${envPath}`);
2216
+ return null;
1483
2217
  }
1484
- return `[${require_values.describeValue(value)}]`;
2218
+ const conventionPath = node_path.default.resolve(process.cwd(), "autotel.yaml");
2219
+ if (node_fs.existsSync(conventionPath)) return conventionPath;
2220
+ const altPath = node_path.default.resolve(process.cwd(), "autotel.yml");
2221
+ if (node_fs.existsSync(altPath)) return altPath;
2222
+ return null;
1485
2223
  }
1486
2224
  /**
1487
- * Validate and sanitize an events event
1488
- * Returns { eventName, attributes } with sanitized values
2225
+ * Convert YAML config structure to AutotelConfig
2226
+ *
2227
+ * @param yaml - Parsed and env-substituted YAML config
2228
+ * @returns Partial AutotelConfig ready for merging
1489
2229
  */
1490
- function validateEvent(eventName, attributes, config) {
1491
- const fullConfig = {
1492
- ...DEFAULT_CONFIG$1,
1493
- ...config
2230
+ function yamlToAutotelConfig(yaml) {
2231
+ const config = {};
2232
+ if (yaml.service?.name) config.service = yaml.service.name;
2233
+ if (yaml.service?.version) config.version = yaml.service.version;
2234
+ if (yaml.service?.environment) config.environment = yaml.service.environment;
2235
+ if (yaml.exporter?.endpoint) config.endpoint = yaml.exporter.endpoint;
2236
+ if (yaml.exporter?.protocol) config.protocol = yaml.exporter.protocol;
2237
+ if (yaml.exporter?.headers) config.headers = yaml.exporter.headers;
2238
+ if (yaml.exporter?.destinations) config.destinations = yaml.exporter.destinations;
2239
+ if (yaml.resource) config.resourceAttributes = yaml.resource;
2240
+ if (yaml.autoInstrumentations) config.autoInstrumentations = yaml.autoInstrumentations;
2241
+ if (yaml.policies) config.policies = yaml.policies;
2242
+ if (yaml.debug !== void 0) config.debug = yaml.debug;
2243
+ if (yaml.sampling?.preset) {
2244
+ warnOnIgnoredPresetOverrides(yaml.sampling);
2245
+ config.sampling = yaml.sampling.preset;
2246
+ } else {
2247
+ const sampler = createSamplerFromYaml(yaml.sampling);
2248
+ if (sampler) config.sampler = sampler;
2249
+ }
2250
+ return config;
2251
+ }
2252
+ function createSamplerFromYaml(sampling) {
2253
+ if (!sampling) return void 0;
2254
+ if (sampling.preset) return void 0;
2255
+ const type = sampling.type ?? "adaptive";
2256
+ try {
2257
+ switch (type) {
2258
+ case "adaptive": return new AdaptiveSampler({
2259
+ baselineSampleRate: sampling.baseline_rate,
2260
+ alwaysSampleErrors: sampling.always_sample_errors,
2261
+ alwaysSampleSlow: sampling.always_sample_slow,
2262
+ slowThresholdMs: sampling.slow_threshold_ms
2263
+ });
2264
+ case "always_on": return new AlwaysSampler();
2265
+ case "always_off": return new NeverSampler();
2266
+ case "ratio":
2267
+ if (sampling.ratio === void 0) {
2268
+ console.warn("[autotel] sampling.ratio missing in YAML sampling config. Falling back to adaptive sampler.");
2269
+ return new AdaptiveSampler();
2270
+ }
2271
+ return new RandomSampler(sampling.ratio);
2272
+ default:
2273
+ console.warn(`[autotel] Unknown sampling type "${type}" in YAML config. Falling back to defaults.`);
2274
+ return;
2275
+ }
2276
+ } catch (error) {
2277
+ console.warn(`[autotel] Failed to configure sampling from YAML: ${error instanceof Error ? error.message : String(error)}`);
2278
+ return;
2279
+ }
2280
+ }
2281
+ function warnOnIgnoredPresetOverrides(sampling) {
2282
+ const ignoredFields = [
2283
+ "type",
2284
+ "ratio",
2285
+ "baseline_rate",
2286
+ "always_sample_errors",
2287
+ "always_sample_slow",
2288
+ "slow_threshold_ms"
2289
+ ].filter((field) => sampling[field] !== void 0);
2290
+ if (ignoredFields.length === 0) return;
2291
+ console.warn(`[autotel] sampling.preset="${sampling.preset}" ignores these YAML fields: ${ignoredFields.join(", ")}. Use the programmatic API with sampler or samplingPresets.*(...) for tuned presets.`);
2292
+ }
2293
+ /**
2294
+ * Load and parse YAML config file (auto-discovery)
2295
+ *
2296
+ * Automatically finds and loads autotel.yaml or uses AUTOTEL_CONFIG_FILE.
2297
+ * Returns null if no config file found (not an error - YAML config is optional).
2298
+ *
2299
+ * @returns Partial AutotelConfig or null if no config file found
2300
+ *
2301
+ * @example
2302
+ * const yamlConfig = loadYamlConfig();
2303
+ * if (yamlConfig) {
2304
+ * init({ ...yamlConfig, debug: true });
2305
+ * }
2306
+ */
2307
+ function loadYamlConfig() {
2308
+ const filePath = findConfigFile();
2309
+ if (!filePath) return null;
2310
+ try {
2311
+ return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(filePath, "utf8")));
2312
+ } catch (error) {
2313
+ console.error(`[autotel] Failed to load YAML config from ${filePath}:`, error);
2314
+ return null;
2315
+ }
2316
+ }
2317
+ /**
2318
+ * Load YAML config from a specific file path
2319
+ *
2320
+ * Unlike loadYamlConfig(), this throws if the file cannot be read.
2321
+ *
2322
+ * @param filePath - Path to YAML config file
2323
+ * @returns Partial AutotelConfig
2324
+ * @throws Error if file cannot be read or parsed
2325
+ *
2326
+ * @example
2327
+ * import { loadYamlConfigFromFile } from 'autotel/yaml';
2328
+ * import { init } from 'autotel';
2329
+ *
2330
+ * const config = loadYamlConfigFromFile('./config/otel.yaml');
2331
+ * init({ ...config, debug: true });
2332
+ */
2333
+ function loadYamlConfigFromFile(filePath) {
2334
+ const resolved = node_path.default.resolve(filePath);
2335
+ return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(resolved, "utf8")));
2336
+ }
2337
+ /**
2338
+ * Check if a YAML config file exists (without loading it)
2339
+ *
2340
+ * @returns true if a config file would be found by loadYamlConfig()
2341
+ */
2342
+ function hasYamlConfig() {
2343
+ return findConfigFile() !== null;
2344
+ }
2345
+
2346
+ //#endregion
2347
+ //#region src/devtools.ts
2348
+ const defaultHost = "127.0.0.1";
2349
+ const defaultPort = 4318;
2350
+ function resolveDevtoolsConfig(config) {
2351
+ if (!config) return {
2352
+ enabled: false,
2353
+ endpoint: void 0,
2354
+ embedded: false,
2355
+ host: defaultHost,
2356
+ port: defaultPort,
2357
+ verbose: false
2358
+ };
2359
+ if (config === true) return {
2360
+ enabled: true,
2361
+ endpoint: `http://${defaultHost}:${defaultPort}`,
2362
+ embedded: false,
2363
+ host: defaultHost,
2364
+ port: defaultPort,
2365
+ verbose: false
1494
2366
  };
2367
+ const enabled = config.enabled ?? true;
2368
+ const host = config.host ?? defaultHost;
2369
+ const port = config.port ?? defaultPort;
2370
+ const endpoint = config.endpoint ?? `http://${host}:${port}`;
1495
2371
  return {
1496
- eventName: validateEventName(eventName, fullConfig),
1497
- attributes: validateAttributes(attributes, fullConfig)
2372
+ enabled,
2373
+ endpoint: enabled ? endpoint : void 0,
2374
+ embedded: enabled && (config.embedded ?? false),
2375
+ host,
2376
+ port,
2377
+ verbose: config.verbose ?? false
1498
2378
  };
1499
2379
  }
1500
2380
 
1501
2381
  //#endregion
1502
- //#region src/track.ts
2382
+ //#region src/process-handlers.ts
2383
+ let removeOwnedHandlers = [];
1503
2384
  /**
1504
- * Global track() function for business events
1505
- *
1506
- * Simple, no instantiation needed, auto-attaches trace context
2385
+ * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
2386
+ * a call to `installProcessHandlers`, which clears that list before installing
2387
+ * its own signal listeners.
1507
2388
  */
1508
- let eventsQueue = null;
2389
+ let removeExitFlush;
1509
2390
  /**
1510
- * Build autotel event context for trace correlation
2391
+ * Shared across every path that can end the process: signals, fatal errors and
2392
+ * a clean exit. They can overlap — a container stopping a job that has just
2393
+ * finished its work sends SIGTERM while the exit flush is still draining — and
2394
+ * a second shutdown would tear down queues the first is still using.
2395
+ */
2396
+ let shutdownInFlight;
2397
+ /**
2398
+ * The clean-exit flush, while it runs. Doubles as the latch that keeps a
2399
+ * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
2400
+ * waits on rather than tearing down queues mid-drain.
2401
+ */
2402
+ let exitFlushInFlight;
2403
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
2404
+ function runShutdownOnce(shutdown, timeoutMs) {
2405
+ if (shutdownInFlight) return shutdownInFlight;
2406
+ let timeoutHandle;
2407
+ const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
2408
+ const timeout = new Promise((resolve) => {
2409
+ timeoutHandle = setTimeout(resolve, timeoutMs);
2410
+ timeoutHandle.unref();
2411
+ });
2412
+ shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
2413
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2414
+ });
2415
+ return shutdownInFlight;
2416
+ }
2417
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
2418
+ function signalExitCode(signal) {
2419
+ return 128 + node_os.constants.signals[signal];
2420
+ }
2421
+ /**
2422
+ * Surface a fatal error before shutting down.
1511
2423
  *
1512
- * Works in multiple contexts:
1513
- * 1. Inside a span → use current span's trace_id + span_id
1514
- * 2. Outside span → use correlation_id only
1515
- * 3. With trace URL config → include clickable trace URL
2424
+ * Registering an `uncaughtException` / `unhandledRejection` listener overrides
2425
+ * Node's default of printing the stack to stderr, so without this a crash under
2426
+ * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
2427
+ * so we print to stderr directly to guarantee the crash stays visible.
1516
2428
  */
1517
- function buildAutotelContext(span) {
1518
- const eventsConfig = getEventsConfig();
1519
- const config = getConfig();
1520
- const correlationId = getOrCreateCorrelationId();
1521
- if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
1522
- const autotelContext = { correlation_id: correlationId };
1523
- const spanContext = span?.spanContext();
1524
- if (spanContext) {
1525
- autotelContext.trace_id = spanContext.traceId;
1526
- autotelContext.span_id = spanContext.spanId;
1527
- autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
1528
- const traceState = spanContext.traceState;
1529
- if (traceState) try {
1530
- if (typeof traceState.serialize === "function") {
1531
- const traceStateStr = traceState.serialize();
1532
- if (traceStateStr) autotelContext.trace_state = traceStateStr;
1533
- }
1534
- } catch {}
1535
- if (eventsConfig.traceUrl && config) {
1536
- const traceUrl = eventsConfig.traceUrl({
1537
- traceId: spanContext.traceId,
1538
- spanId: spanContext.spanId,
1539
- correlationId,
1540
- serviceName: config.service,
1541
- environment: config.environment
1542
- });
1543
- if (traceUrl) autotelContext.trace_url = traceUrl;
1544
- }
1545
- } else if (eventsConfig.traceUrl && config) {
1546
- const traceUrl = eventsConfig.traceUrl({
1547
- correlationId,
1548
- serviceName: config.service,
1549
- environment: config.environment
2429
+ function reportFatalError(error, event) {
2430
+ const err = error instanceof Error ? error : new Error(String(error));
2431
+ console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
2432
+ }
2433
+ function installProcessHandlers(config, shutdown) {
2434
+ uninstallProcessHandlers();
2435
+ const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
2436
+ let exitScheduled = false;
2437
+ let resolvedExitCode = 0;
2438
+ let fatalLatched = false;
2439
+ const shutdownAndExit = (exitCode, fatal) => {
2440
+ if (fatal) {
2441
+ if (!fatalLatched) {
2442
+ resolvedExitCode = exitCode;
2443
+ fatalLatched = true;
2444
+ }
2445
+ } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
2446
+ if (exitScheduled) return;
2447
+ exitScheduled = true;
2448
+ runShutdownOnce(shutdown, timeoutMs).then(() => {
2449
+ process.exit(resolvedExitCode);
2450
+ });
2451
+ };
2452
+ for (const signal of config.signals ?? DEFAULT_SIGNALS) {
2453
+ const listener = () => {
2454
+ shutdownAndExit(signalExitCode(signal), false);
2455
+ };
2456
+ process.on(signal, listener);
2457
+ removeOwnedHandlers.push(() => {
2458
+ process.removeListener(signal, listener);
1550
2459
  });
1551
- if (traceUrl) autotelContext.trace_url = traceUrl;
1552
- }
1553
- return autotelContext;
1554
- }
1555
- /**
1556
- * Initialize events queue lazily
1557
- */
1558
- function getOrCreateQueue() {
1559
- if (!isInitialized()) {
1560
- warnIfNotInitialized("track()");
1561
- return null;
1562
2460
  }
1563
- if (!eventsQueue) {
1564
- const config = getConfig();
1565
- if (!config?.subscribers || config.subscribers.length === 0) return null;
1566
- eventsQueue = new EventQueue(config.subscribers);
2461
+ if (config.fatalErrors ?? true) {
2462
+ const uncaughtExceptionListener = (error) => {
2463
+ reportFatalError(error, "uncaughtException");
2464
+ shutdownAndExit(1, true);
2465
+ };
2466
+ const unhandledRejectionListener = (reason) => {
2467
+ reportFatalError(reason, "unhandledRejection");
2468
+ shutdownAndExit(1, true);
2469
+ };
2470
+ process.on("uncaughtException", uncaughtExceptionListener);
2471
+ process.on("unhandledRejection", unhandledRejectionListener);
2472
+ removeOwnedHandlers.push(() => {
2473
+ process.removeListener("uncaughtException", uncaughtExceptionListener);
2474
+ }, () => {
2475
+ process.removeListener("unhandledRejection", unhandledRejectionListener);
2476
+ });
1567
2477
  }
1568
- return eventsQueue;
1569
2478
  }
1570
2479
  /**
1571
- * Track a business events event
1572
- *
1573
- * Features:
1574
- * - Auto-attaches traceId and spanId if in active span
1575
- * - Batched sending with retry
1576
- * - Type-safe with optional generic
1577
- * - No-op if init() not called or no subscribers configured
1578
- *
1579
- * @example Basic usage
1580
- * ```typescript
1581
- * track('user.signup', { userId: '123', plan: 'pro' })
1582
- * ```
1583
- *
1584
- * @example With type safety
1585
- * ```typescript
1586
- * interface EventDatas {
1587
- * 'user.signup': { userId: string; plan: string }
1588
- * 'plan.upgraded': { userId: string; revenue: number }
1589
- * }
2480
+ * Flush telemetry when the process runs to completion.
1590
2481
  *
1591
- * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
1592
- * ```
2482
+ * The signal and fatal-error handlers above cover a process that is stopped or
2483
+ * that crashes. Neither fires when a script simply finishes: the event loop
2484
+ * drains and Node exits, taking whatever the batch span processor was still
2485
+ * holding with it. `beforeExit` is the only hook for that case, and it is the
2486
+ * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
1593
2487
  *
1594
- * @example Trace correlation (automatic)
1595
- * ```typescript
1596
- * @Instrumented()
1597
- * class UserService {
1598
- * async createUser(data: CreateUserData) {
1599
- * // This track call automatically includes traceId + spanId
1600
- * track('user.signup', { userId: data.id })
1601
- * }
1602
- * }
1603
- * ```
1604
- */
1605
- function track(event, data, options) {
1606
- const queue = getOrCreateQueue();
1607
- if (!queue) return;
1608
- const validated = validateEvent(event, data, getValidationConfig() || void 0);
1609
- const span = _opentelemetry_api.trace.getActiveSpan();
1610
- const enrichedData = span ? {
1611
- ...validated.attributes,
1612
- traceId: span.spanContext().traceId,
1613
- spanId: span.spanContext().spanId
1614
- } : validated.attributes;
1615
- const autotelContext = buildAutotelContext(span);
1616
- queue.enqueue({
1617
- name: validated.eventName,
1618
- attributes: enrichedData,
1619
- timestamp: Date.now(),
1620
- autotel: autotelContext,
1621
- schema: options?.schema
1622
- });
1623
- }
1624
- /**
1625
- * Get events queue (for flush/shutdown)
1626
- * @internal
2488
+ * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
2489
+ * only the final one, so tearing the SDK down here would silently kill
2490
+ * telemetry in a process that goes on to do more work.
1627
2491
  */
1628
- function getEventQueue() {
1629
- return eventsQueue;
2492
+ function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
2493
+ removeExitFlush?.();
2494
+ exitFlushInFlight = void 0;
2495
+ const listener = (code) => {
2496
+ if (exitFlushInFlight || shutdownInFlight) return;
2497
+ const deadline = setTimeout(() => {
2498
+ process.exit(process.exitCode ?? code);
2499
+ }, timeoutMs);
2500
+ exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
2501
+ clearTimeout(deadline);
2502
+ });
2503
+ };
2504
+ process.on("beforeExit", listener);
2505
+ removeExitFlush = () => {
2506
+ process.removeListener("beforeExit", listener);
2507
+ removeExitFlush = void 0;
2508
+ };
1630
2509
  }
1631
- /**
1632
- * Reset events queue (for shutdown/cleanup)
1633
- * @internal
1634
- */
1635
- function resetEventQueue() {
1636
- eventsQueue = null;
2510
+ function uninstallProcessHandlers() {
2511
+ for (const removeHandler of removeOwnedHandlers) removeHandler();
2512
+ removeOwnedHandlers = [];
2513
+ removeExitFlush?.();
2514
+ shutdownInFlight = void 0;
2515
+ exitFlushInFlight = void 0;
1637
2516
  }
1638
2517
 
1639
2518
  //#endregion
@@ -2472,6 +3351,15 @@ function errorCode(cause) {
2472
3351
  return require_values.asString(require_values.readProperty(cause, "code"));
2473
3352
  }
2474
3353
  /**
3354
+ * A bare "Flush timeout" left you with a stack trace into bundled internals and
3355
+ * nothing to act on. Name how long we waited and where we were sending, since
3356
+ * the usual cause is that nothing is listening at that endpoint.
3357
+ */
3358
+ function flushTimeoutMessage(timeout) {
3359
+ const endpoint = getConfig()?.endpoint;
3360
+ return `Flush timeout after ${timeout}ms${endpoint === void 0 ? "" : ` sending to ${endpoint}`}. No collector acknowledged the export; check that one is running and reachable.`;
3361
+ }
3362
+ /**
2475
3363
  * True when the error (or every error it wraps) is an unreachable-endpoint
2476
3364
  * failure. Traverses `AggregateError.errors` and the `cause` chain, since the
2477
3365
  * SDK often wraps the underlying network error.
@@ -2528,7 +3416,7 @@ async function flush(options) {
2528
3416
  await Promise.race([doFlush().finally(() => {
2529
3417
  if (timeoutHandle) clearTimeout(timeoutHandle);
2530
3418
  }), new Promise((_, reject) => {
2531
- timeoutHandle = setTimeout(() => reject(/* @__PURE__ */ new Error("Flush timeout")), timeout);
3419
+ timeoutHandle = setTimeout(() => reject(new Error(flushTimeoutMessage(timeout))), timeout);
2532
3420
  timeoutHandle.unref();
2533
3421
  })]);
2534
3422
  } catch (error) {
@@ -2990,7 +3878,7 @@ function toOtelSampler(sampler) {
2990
3878
  const shouldTrace = sampler.shouldSample(samplingContext);
2991
3879
  const rate = sampler.sampleRate?.(samplingContext);
2992
3880
  const result = { decision: shouldTrace ? _opentelemetry_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED : _opentelemetry_sdk_trace_base.SamplingDecision.NOT_RECORD };
2993
- if (shouldTrace && rate !== void 0 && rate > 1) result.attributes = { [require_sampling.AUTOTEL_SAMPLING_RATE]: rate };
3881
+ if (shouldTrace && rate !== void 0 && rate > 1) result.attributes = { [AUTOTEL_SAMPLING_RATE]: rate };
2994
3882
  return result;
2995
3883
  },
2996
3884
  toString() {
@@ -3090,7 +3978,7 @@ function isLoggerLocked() {
3090
3978
  function init(cfg) {
3091
3979
  if (locked) return;
3092
3980
  const envConfig = resolveConfigFromEnv();
3093
- const yamlConfig = require_yaml_config.loadYamlConfig() ?? {};
3981
+ const yamlConfig = loadYamlConfig() ?? {};
3094
3982
  const mergedConfig = {
3095
3983
  ...envConfig,
3096
3984
  ...yamlConfig,
@@ -3157,14 +4045,14 @@ function init(cfg) {
3157
4045
  const configuredLogRecordProcessors = mergedConfig.logRecordProcessors && mergedConfig.logRecordProcessors.length > 0 ? mergedConfig.logRecordProcessors : mergedConfig.logRecordProcessor ? [mergedConfig.logRecordProcessor] : void 0;
3158
4046
  let spanProcessors = [];
3159
4047
  if (configuredSpanProcessors && configuredSpanProcessors.length > 0) spanProcessors.push(...configuredSpanProcessors);
3160
- else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new require_tail_sampling_processor.TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(exporter)));
4048
+ else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(exporter)));
3161
4049
  else for (const destination of otlpDestinations) {
3162
4050
  if (!destinationSupportsSignal(destination, "traces")) continue;
3163
4051
  const traceExporter = createTraceExporter(destination.protocol, {
3164
4052
  url: formatEndpointUrl(destination.endpoint, "traces", destination.protocol),
3165
4053
  headers: destination.headers
3166
4054
  });
3167
- spanProcessors.push(new require_tail_sampling_processor.TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(traceExporter)));
4055
+ spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(traceExporter)));
3168
4056
  }
3169
4057
  if (mergedConfig.baggage || mergedConfig.baggage === "") {
3170
4058
  const prefix = typeof mergedConfig.baggage === "string" ? mergedConfig.baggage ? `${mergedConfig.baggage}.` : "" : "baggage.";
@@ -3255,9 +4143,9 @@ function init(cfg) {
3255
4143
  logger.warn({}, `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`);
3256
4144
  }
3257
4145
  }
3258
- const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? require_sampling.resolveSamplingPreset(mergedConfig.sampling) : void 0);
4146
+ const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? resolveSamplingPreset(mergedConfig.sampling) : void 0);
3259
4147
  if (autotelSampler) mergedConfig.sampler = autotelSampler;
3260
- const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(require_sampling.samplingPresets.production());
4148
+ const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(samplingPresets.production());
3261
4149
  const sdkOptions = {
3262
4150
  resource,
3263
4151
  serviceName: mergedConfig.service,
@@ -3336,6 +4224,18 @@ function warnIfNotInitialized(context) {
3336
4224
  }
3337
4225
  }
3338
4226
  /**
4227
+ * The sampler `init()` was configured with, or undefined when it was not.
4228
+ *
4229
+ * Separate from {@link getDefaultSampler} on purpose: the tracing wrapper needs
4230
+ * to know whether a sampler was *chosen*. Falling back to the production preset
4231
+ * there would start dropping spans for every app that never asked to sample.
4232
+ *
4233
+ * @internal
4234
+ */
4235
+ function getConfiguredSampler() {
4236
+ return config?.sampler;
4237
+ }
4238
+ /**
3339
4239
  * @internal Close embedded devtools if running.
3340
4240
  */
3341
4241
  async function _closeEmbeddedDevtools() {
@@ -3352,6 +4252,42 @@ function getSdk() {
3352
4252
  }
3353
4253
 
3354
4254
  //#endregion
4255
+ Object.defineProperty(exports, 'AUTOTEL_DEBUG_BAGGAGE_KEY', {
4256
+ enumerable: true,
4257
+ get: function () {
4258
+ return AUTOTEL_DEBUG_BAGGAGE_KEY;
4259
+ }
4260
+ });
4261
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_RATE', {
4262
+ enumerable: true,
4263
+ get: function () {
4264
+ return AUTOTEL_SAMPLING_RATE;
4265
+ }
4266
+ });
4267
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_EVALUATED', {
4268
+ enumerable: true,
4269
+ get: function () {
4270
+ return AUTOTEL_SAMPLING_TAIL_EVALUATED;
4271
+ }
4272
+ });
4273
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_KEEP', {
4274
+ enumerable: true,
4275
+ get: function () {
4276
+ return AUTOTEL_SAMPLING_TAIL_KEEP;
4277
+ }
4278
+ });
4279
+ Object.defineProperty(exports, 'AdaptiveSampler', {
4280
+ enumerable: true,
4281
+ get: function () {
4282
+ return AdaptiveSampler;
4283
+ }
4284
+ });
4285
+ Object.defineProperty(exports, 'AlwaysSampler', {
4286
+ enumerable: true,
4287
+ get: function () {
4288
+ return AlwaysSampler;
4289
+ }
4290
+ });
3355
4291
  Object.defineProperty(exports, 'BaggageSpanProcessor', {
3356
4292
  enumerable: true,
3357
4293
  get: function () {
@@ -3364,12 +4300,66 @@ Object.defineProperty(exports, 'CORRELATION_ID_BAGGAGE_KEY', {
3364
4300
  return CORRELATION_ID_BAGGAGE_KEY;
3365
4301
  }
3366
4302
  });
4303
+ Object.defineProperty(exports, 'CompositeSampler', {
4304
+ enumerable: true,
4305
+ get: function () {
4306
+ return CompositeSampler;
4307
+ }
4308
+ });
4309
+ Object.defineProperty(exports, 'DeterministicSampler', {
4310
+ enumerable: true,
4311
+ get: function () {
4312
+ return DeterministicSampler;
4313
+ }
4314
+ });
3367
4315
  Object.defineProperty(exports, 'Event', {
3368
4316
  enumerable: true,
3369
4317
  get: function () {
3370
4318
  return Event;
3371
4319
  }
3372
4320
  });
4321
+ Object.defineProperty(exports, 'FeatureFlagSampler', {
4322
+ enumerable: true,
4323
+ get: function () {
4324
+ return FeatureFlagSampler;
4325
+ }
4326
+ });
4327
+ Object.defineProperty(exports, 'KeyTargetRateSampler', {
4328
+ enumerable: true,
4329
+ get: function () {
4330
+ return KeyTargetRateSampler;
4331
+ }
4332
+ });
4333
+ Object.defineProperty(exports, 'NeverSampler', {
4334
+ enumerable: true,
4335
+ get: function () {
4336
+ return NeverSampler;
4337
+ }
4338
+ });
4339
+ Object.defineProperty(exports, 'RandomSampler', {
4340
+ enumerable: true,
4341
+ get: function () {
4342
+ return RandomSampler;
4343
+ }
4344
+ });
4345
+ Object.defineProperty(exports, 'TailSamplingSpanProcessor', {
4346
+ enumerable: true,
4347
+ get: function () {
4348
+ return TailSamplingSpanProcessor;
4349
+ }
4350
+ });
4351
+ Object.defineProperty(exports, 'UserIdSampler', {
4352
+ enumerable: true,
4353
+ get: function () {
4354
+ return UserIdSampler;
4355
+ }
4356
+ });
4357
+ Object.defineProperty(exports, 'createLinkFromHeaders', {
4358
+ enumerable: true,
4359
+ get: function () {
4360
+ return createLinkFromHeaders;
4361
+ }
4362
+ });
3373
4363
  Object.defineProperty(exports, 'createStringRedactor', {
3374
4364
  enumerable: true,
3375
4365
  get: function () {
@@ -3382,12 +4372,24 @@ Object.defineProperty(exports, 'createTraceContext', {
3382
4372
  return createTraceContext;
3383
4373
  }
3384
4374
  });
4375
+ Object.defineProperty(exports, 'debugCaptureRequested', {
4376
+ enumerable: true,
4377
+ get: function () {
4378
+ return debugCaptureRequested;
4379
+ }
4380
+ });
3385
4381
  Object.defineProperty(exports, 'defineBaggageSchema', {
3386
4382
  enumerable: true,
3387
4383
  get: function () {
3388
4384
  return defineBaggageSchema;
3389
4385
  }
3390
4386
  });
4387
+ Object.defineProperty(exports, 'extractLinksFromBatch', {
4388
+ enumerable: true,
4389
+ get: function () {
4390
+ return extractLinksFromBatch;
4391
+ }
4392
+ });
3391
4393
  Object.defineProperty(exports, 'flush', {
3392
4394
  enumerable: true,
3393
4395
  get: function () {
@@ -3412,6 +4414,12 @@ Object.defineProperty(exports, 'getConfig', {
3412
4414
  return getConfig;
3413
4415
  }
3414
4416
  });
4417
+ Object.defineProperty(exports, 'getConfiguredSampler', {
4418
+ enumerable: true,
4419
+ get: function () {
4420
+ return getConfiguredSampler;
4421
+ }
4422
+ });
3415
4423
  Object.defineProperty(exports, 'getContextStorage', {
3416
4424
  enumerable: true,
3417
4425
  get: function () {
@@ -3472,12 +4480,30 @@ Object.defineProperty(exports, 'hasExplicitSpanStatus', {
3472
4480
  return hasExplicitSpanStatus;
3473
4481
  }
3474
4482
  });
4483
+ Object.defineProperty(exports, 'hasYamlConfig', {
4484
+ enumerable: true,
4485
+ get: function () {
4486
+ return hasYamlConfig;
4487
+ }
4488
+ });
4489
+ Object.defineProperty(exports, 'hashUnitInterval', {
4490
+ enumerable: true,
4491
+ get: function () {
4492
+ return hashUnitInterval;
4493
+ }
4494
+ });
3475
4495
  Object.defineProperty(exports, 'init', {
3476
4496
  enumerable: true,
3477
4497
  get: function () {
3478
4498
  return init;
3479
4499
  }
3480
4500
  });
4501
+ Object.defineProperty(exports, 'isForceKept', {
4502
+ enumerable: true,
4503
+ get: function () {
4504
+ return isForceKept;
4505
+ }
4506
+ });
3481
4507
  Object.defineProperty(exports, 'isInitialized', {
3482
4508
  enumerable: true,
3483
4509
  get: function () {
@@ -3490,18 +4516,42 @@ Object.defineProperty(exports, 'isLoggerLocked', {
3490
4516
  return isLoggerLocked;
3491
4517
  }
3492
4518
  });
4519
+ Object.defineProperty(exports, 'loadYamlConfig', {
4520
+ enumerable: true,
4521
+ get: function () {
4522
+ return loadYamlConfig;
4523
+ }
4524
+ });
4525
+ Object.defineProperty(exports, 'loadYamlConfigFromFile', {
4526
+ enumerable: true,
4527
+ get: function () {
4528
+ return loadYamlConfigFromFile;
4529
+ }
4530
+ });
3493
4531
  Object.defineProperty(exports, 'lockLogger', {
3494
4532
  enumerable: true,
3495
4533
  get: function () {
3496
4534
  return lockLogger;
3497
4535
  }
3498
4536
  });
4537
+ Object.defineProperty(exports, 'markForceKept', {
4538
+ enumerable: true,
4539
+ get: function () {
4540
+ return markForceKept;
4541
+ }
4542
+ });
3499
4543
  Object.defineProperty(exports, 'resetEvents', {
3500
4544
  enumerable: true,
3501
4545
  get: function () {
3502
4546
  return resetEvents;
3503
4547
  }
3504
4548
  });
4549
+ Object.defineProperty(exports, 'resolveSamplingPreset', {
4550
+ enumerable: true,
4551
+ get: function () {
4552
+ return resolveSamplingPreset;
4553
+ }
4554
+ });
3505
4555
  Object.defineProperty(exports, 'runInOperationContext', {
3506
4556
  enumerable: true,
3507
4557
  get: function () {
@@ -3514,6 +4564,12 @@ Object.defineProperty(exports, 'runWithCorrelationId', {
3514
4564
  return runWithCorrelationId;
3515
4565
  }
3516
4566
  });
4567
+ Object.defineProperty(exports, 'samplingPresets', {
4568
+ enumerable: true,
4569
+ get: function () {
4570
+ return samplingPresets;
4571
+ }
4572
+ });
3517
4573
  Object.defineProperty(exports, 'setCorrelationId', {
3518
4574
  enumerable: true,
3519
4575
  get: function () {