autotel 7.0.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +25 -0
  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/evidence.cjs +57 -0
  17. package/dist/evidence.d.cts +61 -0
  18. package/dist/evidence.d.ts +61 -0
  19. package/dist/evidence.js +50 -0
  20. package/dist/{functional-C2ONG9PK.cjs → functional-C925G95W.cjs} +68 -17
  21. package/dist/{functional-CJn2zY4o.d.cts → functional-CgWcRXGX.d.cts} +12 -5
  22. package/dist/{functional-nS5U4KCQ.js → functional-Doi0hFm3.js} +64 -13
  23. package/dist/{functional-DXy1cHFF.d.ts → functional-RFFpQzAV.d.ts} +12 -5
  24. package/dist/functional.cjs +1 -1
  25. package/dist/functional.d.cts +1 -1
  26. package/dist/functional.d.ts +1 -1
  27. package/dist/functional.js +1 -1
  28. package/dist/http.cjs +1 -1
  29. package/dist/http.js +1 -1
  30. package/dist/index.cjs +53 -15
  31. package/dist/index.d.cts +24 -4
  32. package/dist/index.d.ts +24 -4
  33. package/dist/index.js +40 -5
  34. package/dist/{init-DrlL1eis.d.cts → init-8GSy2bza.d.cts} +1 -1
  35. package/dist/{init-DDyaLzeg.js → init-DzAdgcFM.js} +2181 -1303
  36. package/dist/{init-BgnYi2sm.d.ts → init-UnKrXhpq.d.ts} +1 -1
  37. package/dist/{init-C5aHRqi2.cjs → init-aTZKsAwy.cjs} +2324 -1294
  38. package/dist/instrumentation.cjs +2 -3
  39. package/dist/instrumentation.js +1 -2
  40. package/dist/messaging.cjs +5 -5
  41. package/dist/messaging.js +2 -2
  42. package/dist/{sampling-CM5S2Lp3.d.ts → sampling-DBng4Foa.d.ts} +20 -1
  43. package/dist/{sampling-CmRdnXUi.d.cts → sampling-Er217Mge.d.cts} +20 -1
  44. package/dist/sampling.cjs +22 -601
  45. package/dist/sampling.d.cts +2 -2
  46. package/dist/sampling.d.ts +2 -2
  47. package/dist/sampling.js +2 -585
  48. package/dist/semantic-helpers.cjs +1 -1
  49. package/dist/semantic-helpers.js +1 -1
  50. package/dist/tail-sampling-processor.cjs +2 -26
  51. package/dist/tail-sampling-processor.js +1 -25
  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 +6 -1
@@ -2,695 +2,1041 @@ 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
+ /**
790
+ * Build autotel event context for trace correlation
791
+ *
792
+ * Works in multiple contexts:
793
+ * 1. Inside a span → use current span's trace_id + span_id
794
+ * 2. Outside span → use correlation_id only
795
+ * 3. With trace URL config → include clickable trace URL
796
+ */
797
+ function buildAutotelContext(span) {
798
+ const eventsConfig = getEventsConfig();
799
+ const config = getConfig();
800
+ const correlationId = getOrCreateCorrelationId();
801
+ if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
802
+ const autotelContext = { correlation_id: correlationId };
803
+ const spanContext = span?.spanContext();
804
+ if (spanContext) {
805
+ autotelContext.trace_id = spanContext.traceId;
806
+ autotelContext.span_id = spanContext.spanId;
807
+ autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
808
+ const traceState = spanContext.traceState;
809
+ if (traceState) try {
810
+ if (typeof traceState.serialize === "function") {
811
+ const traceStateStr = traceState.serialize();
812
+ if (traceStateStr) autotelContext.trace_state = traceStateStr;
813
+ }
814
+ } catch {}
815
+ if (eventsConfig.traceUrl && config) {
816
+ const traceUrl = eventsConfig.traceUrl({
817
+ traceId: spanContext.traceId,
818
+ spanId: spanContext.spanId,
819
+ correlationId,
820
+ serviceName: config.service,
821
+ environment: config.environment
822
+ });
823
+ if (traceUrl) autotelContext.trace_url = traceUrl;
824
+ }
825
+ } else if (eventsConfig.traceUrl && config) {
826
+ const traceUrl = eventsConfig.traceUrl({
827
+ correlationId,
828
+ serviceName: config.service,
829
+ environment: config.environment
830
+ });
831
+ if (traceUrl) autotelContext.trace_url = traceUrl;
832
+ }
833
+ return autotelContext;
834
+ }
835
+ /**
836
+ * Initialize events queue lazily
837
+ */
838
+ function getOrCreateQueue() {
839
+ if (!isInitialized()) {
840
+ warnIfNotInitialized("track()");
841
+ return null;
842
+ }
843
+ if (!eventsQueue) {
844
+ const config = getConfig();
845
+ if (!config?.subscribers || config.subscribers.length === 0) return null;
846
+ eventsQueue = new EventQueue(config.subscribers);
847
+ }
848
+ return eventsQueue;
849
+ }
850
+ /**
851
+ * Track a business events event
852
+ *
853
+ * Features:
854
+ * - Auto-attaches traceId and spanId if in active span
855
+ * - Batched sending with retry
856
+ * - Type-safe with optional generic
857
+ * - No-op if init() not called or no subscribers configured
858
+ *
859
+ * @example Basic usage
860
+ * ```typescript
861
+ * track('user.signup', { userId: '123', plan: 'pro' })
862
+ * ```
863
+ *
864
+ * @example With type safety
865
+ * ```typescript
866
+ * interface EventDatas {
867
+ * 'user.signup': { userId: string; plan: string }
868
+ * 'plan.upgraded': { userId: string; revenue: number }
869
+ * }
870
+ *
871
+ * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
872
+ * ```
873
+ *
874
+ * @example Trace correlation (automatic)
875
+ * ```typescript
876
+ * @Instrumented()
877
+ * class UserService {
878
+ * async createUser(data: CreateUserData) {
879
+ * // This track call automatically includes traceId + spanId
880
+ * track('user.signup', { userId: data.id })
881
+ * }
882
+ * }
883
+ * ```
884
+ */
885
+ function track(event, data, options) {
886
+ const queue = getOrCreateQueue();
887
+ if (!queue) return;
888
+ const validated = validateEvent(event, data, getValidationConfig() || void 0);
889
+ const span = _opentelemetry_api.trace.getActiveSpan();
890
+ const enrichedData = span ? {
891
+ ...validated.attributes,
892
+ traceId: span.spanContext().traceId,
893
+ spanId: span.spanContext().spanId
894
+ } : validated.attributes;
895
+ const autotelContext = buildAutotelContext(span);
896
+ queue.enqueue({
897
+ name: validated.eventName,
898
+ attributes: enrichedData,
899
+ timestamp: Date.now(),
900
+ autotel: autotelContext,
901
+ schema: options?.schema
902
+ });
903
+ }
904
+ /**
905
+ * Get events queue (for flush/shutdown)
906
+ * @internal
907
+ */
908
+ function getEventQueue() {
909
+ return eventsQueue;
910
+ }
911
+ /**
912
+ * Reset events queue (for shutdown/cleanup)
913
+ * @internal
914
+ */
915
+ function resetEventQueue() {
916
+ eventsQueue = null;
917
+ }
918
+
919
+ //#endregion
920
+ //#region src/trace-context.ts
921
+ const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
922
+ /** Whether a TraceContext consumer explicitly chose this span's status. */
923
+ function hasExplicitSpanStatus(span) {
924
+ return spansWithExplicitStatus.has(span);
925
+ }
926
+ /**
927
+ * AsyncLocalStorage for storing the active context with baggage
928
+ * This allows setters to update the context and have it persist
929
+ */
930
+ const contextStorage = new node_async_hooks.AsyncLocalStorage();
931
+ /**
932
+ * Get the context storage instance (for initialization in functional.ts)
933
+ */
934
+ function getContextStorage() {
935
+ return contextStorage;
936
+ }
937
+ /**
938
+ * Get the active OTel context with the latest stored baggage overlaid.
939
+ * Span identity always comes from the active OTel scope.
940
+ */
941
+ function getActiveContextWithBaggage() {
942
+ const activeContext = _opentelemetry_api.context.active();
943
+ const stored = contextStorage.getStore()?.value;
944
+ if (!stored) return activeContext;
945
+ const storedBaggage = _opentelemetry_api.propagation.getBaggage(stored);
946
+ return storedBaggage ? _opentelemetry_api.propagation.setBaggage(activeContext, storedBaggage) : activeContext;
947
+ }
948
+ /**
949
+ * Set a value in AsyncLocalStorage, preferring enterWith() when available
950
+ * (Node.js) and falling back to run() for environments that only support
951
+ * run() (e.g. Cloudflare Workers).
952
+ *
953
+ * On runtimes without enterWith() we mutate the existing run() scope when one
954
+ * exists. This is what allows baggage/correlation updates to remain visible
955
+ * for the rest of the traced callback in Workers.
956
+ */
957
+ function enterOrRun(storage, value) {
958
+ const existingStore = storage.getStore();
959
+ if (existingStore) {
960
+ existingStore.value = value;
961
+ return;
962
+ }
963
+ const boxedValue = { value };
964
+ try {
965
+ storage.enterWith(boxedValue);
966
+ } catch {
967
+ storage.run(boxedValue, () => {});
968
+ }
969
+ }
970
+ function updateActiveContext(newContext) {
971
+ enterOrRun(contextStorage, newContext);
972
+ const manager = _opentelemetry_api.context._getContextManager?.();
973
+ if (!manager) return;
974
+ const asyncLocal = manager._asyncLocalStorage ?? void 0;
975
+ if (asyncLocal?.enterWith) {
976
+ asyncLocal.enterWith(newContext);
977
+ return;
978
+ }
979
+ if (require_values.isFunction(manager.with)) manager.with(newContext, () => {});
980
+ }
981
+ /**
982
+ * Create a TraceContext from an OpenTelemetry Span
983
+ *
984
+ * This utility extracts trace context information from a span
985
+ * and provides span manipulation methods and baggage operations in a consistent format.
986
+ *
987
+ * Note: Baggage methods always operate on the currently active context,
988
+ * which may differ from the context when createTraceContext was called.
989
+ */
990
+ function createTraceContext(span) {
991
+ const spanContext = span.spanContext();
992
+ if (!contextStorage.getStore()?.value) {
993
+ const activeContext = _opentelemetry_api.context.active();
994
+ enterOrRun(contextStorage, activeContext);
995
+ }
996
+ const traceCtx = {
997
+ traceId: spanContext.traceId,
998
+ spanId: spanContext.spanId,
999
+ correlationId: spanContext.traceId.slice(0, 16),
1000
+ setAttribute: span.setAttribute.bind(span),
1001
+ setAttributes: span.setAttributes.bind(span),
1002
+ setStatus: (status) => {
1003
+ spansWithExplicitStatus.add(span);
1004
+ span.setStatus(status);
1005
+ },
1006
+ recordException: span.recordException.bind(span),
1007
+ addEvent: span.addEvent.bind(span),
1008
+ addLink: span.addLink.bind(span),
1009
+ addLinks: span.addLinks.bind(span),
1010
+ updateName: span.updateName.bind(span),
1011
+ isRecording: span.isRecording.bind(span),
1012
+ recordError: (cause) => {
1013
+ const err = cause instanceof Error ? cause : new Error(String(cause));
1014
+ require_structured_error.recordStructuredError(traceCtx, err);
1015
+ },
1016
+ track: (event, data) => {
1017
+ track(event, data);
1018
+ },
1019
+ getBaggage(key) {
1020
+ const activeCtx = _opentelemetry_api.context.active();
1021
+ let baggage = _opentelemetry_api.propagation.getBaggage(activeCtx);
1022
+ if (!baggage) {
1023
+ const storedContext = contextStorage.getStore()?.value;
1024
+ if (storedContext) baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
1025
+ }
1026
+ return baggage?.getEntry(key)?.value;
1027
+ },
1028
+ setBaggage(key, value) {
1029
+ const currentContext = getActiveContextWithBaggage();
1030
+ const updated = (_opentelemetry_api.propagation.getBaggage(currentContext) ?? _opentelemetry_api.propagation.createBaggage()).setEntry(key, { value });
1031
+ updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
1032
+ return value;
1033
+ },
1034
+ deleteBaggage(key) {
1035
+ const currentContext = getActiveContextWithBaggage();
1036
+ const baggage = _opentelemetry_api.propagation.getBaggage(currentContext);
1037
+ if (baggage) {
1038
+ const updated = baggage.removeEntry(key);
1039
+ updateActiveContext(_opentelemetry_api.propagation.setBaggage(currentContext, updated));
694
1040
  }
695
1041
  },
696
1042
  getAllBaggage() {
@@ -820,820 +1166,1345 @@ function defineBaggageSchema(namespace) {
820
1166
  }
821
1167
 
822
1168
  //#endregion
823
- //#region src/correlation-id.ts
1169
+ //#region src/sampling.ts
824
1170
  /**
825
- * Correlation ID utilities for event-driven observability
1171
+ * Tail sampling attribute keys (autotel-internal, not OTel semconv)
1172
+ */
1173
+ const AUTOTEL_SAMPLING_TAIL_KEEP = "autotel.sampling.tail.keep";
1174
+ const AUTOTEL_SAMPLING_TAIL_EVALUATED = "autotel.sampling.tail.evaluated";
1175
+ /**
1176
+ * How many events each kept event stands for, expressed as "1 in N".
826
1177
  *
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.
1178
+ * A query that counts sampled spans undercounts the population. Multiplying
1179
+ * each kept event by this rate restores the estimate. Autotel records the
1180
+ * attribute only when N exceeds 1, so fully captured spans stay clean.
1181
+ */
1182
+ const AUTOTEL_SAMPLING_RATE = "autotel.sampling.rate";
1183
+ /**
1184
+ * Spans an explicit `forceKeep()` claimed.
829
1185
  *
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)
1186
+ * The tracing wrapper writes the sampler's tail verdict once the body has
1187
+ * run, which is after any `forceKeep()` inside it. Without this the verdict
1188
+ * overwrites the override and the span the caller insisted on is dropped.
1189
+ */
1190
+ /**
1191
+ * Baggage key that turns on full-fidelity capture for a request.
834
1192
  *
835
- * @example Basic usage
836
- * ```typescript
837
- * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
1193
+ * Baggage arrives on the request and propagates, so a gateway, a proxy, a
1194
+ * feature flag or a curl can turn this on for one user and it follows them
1195
+ * across every service. Nobody deploys anything to debug a live problem.
1196
+ */
1197
+ const AUTOTEL_DEBUG_BAGGAGE_KEY = "autotel.debug";
1198
+ const forceKeptSpans = /* @__PURE__ */ new WeakSet();
1199
+ function markForceKept(span) {
1200
+ forceKeptSpans.add(span);
1201
+ }
1202
+ function isForceKept(span) {
1203
+ return forceKeptSpans.has(span);
1204
+ }
1205
+ /**
1206
+ * Convert a keep probability into the "1 in N" rate reported on spans.
838
1207
  *
839
- * // Generate a new correlation ID
840
- * const id = generateCorrelationId();
841
- * // Returns: 'a1b2c3d4e5f67890'
1208
+ * Keeping 10% of traces means each survivor stands for 10, so the two numbers
1209
+ * are reciprocals and easy to publish the wrong way round.
1210
+ */
1211
+ function toSampleRate(probability) {
1212
+ return probability > 0 ? 1 / probability : 0;
1213
+ }
1214
+ /**
1215
+ * Map a string to a stable, evenly spread position in the unit interval.
842
1216
  *
843
- * // Get current correlation ID from context
844
- * const currentId = getCorrelationId();
1217
+ * Two processes that hash the same key reach the same number, which is what
1218
+ * lets independent services agree on one trace's sampling decision.
1219
+ *
1220
+ * The spread matters as much as the stability. Real sampling keys share long
1221
+ * prefixes: `user_1000`, `user_1001`, `checkout-trace-0001`. A plain
1222
+ * multiply-and-add hash lets that shared prefix dominate the high bits, so a
1223
+ * whole key family lands in one narrow band and a rate of 0.1 keeps all of
1224
+ * them or none of them. FNV-1a followed by the murmur3 finalizer mixes the
1225
+ * low bits back through the word, so keys that differ in one character land
1226
+ * far apart.
1227
+ */
1228
+ function hashUnitInterval(value) {
1229
+ let hash = 2166136261;
1230
+ for (let i = 0; i < value.length; i++) {
1231
+ hash ^= value.codePointAt(i) ?? 0;
1232
+ hash = Math.imul(hash, 16777619);
1233
+ }
1234
+ hash ^= hash >>> 16;
1235
+ hash = Math.imul(hash, 2246822507);
1236
+ hash ^= hash >>> 13;
1237
+ hash = Math.imul(hash, 3266489909);
1238
+ hash ^= hash >>> 16;
1239
+ return (hash >>> 0) / 4294967296;
1240
+ }
1241
+ /**
1242
+ * Simple random sampler
1243
+ *
1244
+ * @example
1245
+ * ```typescript
1246
+ * new RandomSampler(0.1) // Sample 10% of requests
845
1247
  * ```
846
1248
  */
1249
+ var RandomSampler = class {
1250
+ rate;
1251
+ constructor(rate) {
1252
+ this.rate = rate;
1253
+ if (rate < 0 || rate > 1) throw new Error("Sample rate must be between 0 and 1");
1254
+ }
1255
+ shouldSample(_context) {
1256
+ return Math.random() < this.rate;
1257
+ }
1258
+ sampleRate() {
1259
+ return toSampleRate(this.rate);
1260
+ }
1261
+ };
847
1262
  /**
848
- * AsyncLocalStorage for storing correlation ID
849
- * This allows correlation IDs to persist across async boundaries
1263
+ * Always sample (100% tracing)
850
1264
  */
851
- const correlationStorage = new node_async_hooks.AsyncLocalStorage();
1265
+ var AlwaysSampler = class {
1266
+ shouldSample(_context) {
1267
+ return true;
1268
+ }
1269
+ };
852
1270
  /**
853
- * Baggage key for correlation ID propagation
1271
+ * Never sample (0% tracing)
854
1272
  */
855
- const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
1273
+ var NeverSampler = class {
1274
+ shouldSample(_context) {
1275
+ return false;
1276
+ }
1277
+ };
856
1278
  /**
857
- * Generate a new correlation ID
1279
+ * Adaptive sampler that always traces errors and slow requests
858
1280
  *
859
- * Format: 16 hex chars (64 bits), crypto-random, URL-safe
1281
+ * This is the recommended sampler for production use.
1282
+ * It ensures you never miss critical issues while keeping costs down.
860
1283
  *
861
- * @returns A new correlation ID
1284
+ * Strategy:
1285
+ * - Always trace errors (critical for debugging)
1286
+ * - Always trace slow requests (performance issues)
1287
+ * - Use baseline sample rate for successful fast requests
1288
+ *
1289
+ * **IMPORTANT - Tail Sampling Requirement:**
1290
+ * This sampler uses tail sampling (makes decisions AFTER execution).
1291
+ * You MUST use TailSamplingSpanProcessor for it to work correctly:
1292
+ *
1293
+ * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
1294
+ * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
1295
+ *
1296
+ * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
1297
+ *
1298
+ * @see TailSamplingSpanProcessor
1299
+ * @see README.md "Tail Sampling with Custom Providers" section
862
1300
  *
863
1301
  * @example
864
1302
  * ```typescript
865
- * const id = generateCorrelationId();
866
- * // Returns: 'a1b2c3d4e5f67890'
1303
+ * new AdaptiveSampler({
1304
+ * baselineSampleRate: 0.1, // 10% of normal requests
1305
+ * slowThresholdMs: 1000, // Requests > 1s are "slow"
1306
+ * alwaysSampleErrors: true, // Always trace errors
1307
+ * alwaysSampleSlow: true // Always trace slow requests
1308
+ * })
867
1309
  * ```
868
1310
  */
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
- }
1311
+ var AdaptiveSampler = class {
1312
+ baselineSampleRate;
1313
+ slowThresholdMs;
1314
+ alwaysSampleErrors;
1315
+ alwaysSampleSlow;
1316
+ linksBased;
1317
+ linksRate;
1318
+ logger;
1319
+ samplingDecisions = /* @__PURE__ */ new WeakMap();
1320
+ operationResults = /* @__PURE__ */ new WeakMap();
1321
+ constructor(options = {}) {
1322
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1323
+ this.slowThresholdMs = options.slowThresholdMs ?? 1e3;
1324
+ this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
1325
+ this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
1326
+ this.linksBased = options.linksBased ?? false;
1327
+ this.linksRate = options.linksRate ?? 1;
1328
+ this.logger = options.logger;
1329
+ if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) throw new Error("Baseline sample rate must be between 0 and 1");
1330
+ if (this.linksRate < 0 || this.linksRate > 1) throw new Error("Links rate must be between 0 and 1");
1331
+ }
1332
+ needsTailSampling() {
1333
+ return true;
1334
+ }
1335
+ shouldSample(context) {
1336
+ const baselineDecision = Math.random() < this.baselineSampleRate;
1337
+ this.samplingDecisions.set(context.args, baselineDecision);
1338
+ return true;
1339
+ }
1340
+ /**
1341
+ * Check if any links point to sampled spans.
1342
+ *
1343
+ * A span is considered linked to a sampled span if any of its links
1344
+ * have trace_flags with the sampled bit set (0x01).
1345
+ *
1346
+ * @param links - Array of span links to check
1347
+ * @returns true if any linked span is sampled, false otherwise
1348
+ */
1349
+ hasSampledLink(links) {
1350
+ if (!links || links.length === 0) return false;
1351
+ return links.some((link) => link.context && (link.context.traceFlags & _opentelemetry_api.TraceFlags.SAMPLED) !== 0);
1352
+ }
1353
+ /**
1354
+ * Re-evaluate sampling decision after operation completes
1355
+ *
1356
+ * This allows us to always capture errors and slow requests,
1357
+ * even if they weren't initially sampled.
1358
+ *
1359
+ * @param context - Sampling context
1360
+ * @param result - Operation result
1361
+ * @returns true if this operation should be kept (not discarded)
1362
+ */
1363
+ shouldKeepTrace(context, result) {
1364
+ const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
1365
+ if (this.alwaysSampleErrors && !result.success) {
1366
+ if (!baselineDecision) this.logger?.debug({
1367
+ operation: context.operationName,
1368
+ error: result.error?.message
1369
+ }, "Adaptive sampling: Keeping error trace");
1370
+ return true;
1371
+ }
1372
+ if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
1373
+ if (!baselineDecision) this.logger?.debug({
1374
+ operation: context.operationName,
1375
+ duration: result.duration
1376
+ }, "Adaptive sampling: Keeping slow trace");
1377
+ return true;
1378
+ }
1379
+ if (this.linksBased && context.links && this.hasSampledLink(context.links)) {
1380
+ const keepLinked = Math.random() < this.linksRate;
1381
+ if (keepLinked && !baselineDecision) this.logger?.debug({
1382
+ operation: context.operationName,
1383
+ linkCount: context.links.length
1384
+ }, "Adaptive sampling: Keeping trace due to sampled link");
1385
+ return keepLinked;
1386
+ }
1387
+ return baselineDecision;
1388
+ }
1389
+ };
874
1390
  /**
875
- * Get the current correlation ID from context
1391
+ * User-based sampler for consistent tracing
876
1392
  *
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)
1393
+ * Always samples requests from specific user IDs.
1394
+ * Useful for debugging specific user issues or monitoring VIP users.
882
1395
  *
883
- * @returns Current correlation ID or undefined
1396
+ * @example
1397
+ * ```typescript
1398
+ * new UserIdSampler({
1399
+ * baselineSampleRate: 0.01, // 1% of normal users
1400
+ * alwaysSampleUsers: ['vip_123'], // Always trace VIP users
1401
+ * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
1402
+ * })
1403
+ * ```
1404
+ */
1405
+ var UserIdSampler = class {
1406
+ baselineSampleRate;
1407
+ alwaysSampleUsers;
1408
+ extractUserId;
1409
+ logger;
1410
+ constructor(options) {
1411
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1412
+ this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
1413
+ this.extractUserId = options.extractUserId;
1414
+ this.logger = options.logger;
1415
+ }
1416
+ shouldSample(context) {
1417
+ const userId = this.extractUserId(context.args);
1418
+ if (userId && this.alwaysSampleUsers.has(userId)) {
1419
+ this.logger?.debug({
1420
+ operation: context.operationName,
1421
+ userId
1422
+ }, "Sampling user request");
1423
+ return true;
1424
+ }
1425
+ if (userId) return this.hashString(userId) < this.baselineSampleRate;
1426
+ return Math.random() < this.baselineSampleRate;
1427
+ }
1428
+ /**
1429
+ * Add user IDs to always-sample list
1430
+ */
1431
+ addAlwaysSampleUsers(...userIds) {
1432
+ for (const userId of userIds) this.alwaysSampleUsers.add(userId);
1433
+ }
1434
+ /**
1435
+ * Remove user IDs from always-sample list
1436
+ */
1437
+ removeAlwaysSampleUsers(...userIds) {
1438
+ for (const userId of userIds) this.alwaysSampleUsers.delete(userId);
1439
+ }
1440
+ /**
1441
+ * Simple hash function for consistent user sampling
1442
+ */
1443
+ hashString(str) {
1444
+ return hashUnitInterval(str);
1445
+ }
1446
+ };
1447
+ /**
1448
+ * Consistent sampler: every service reaches the same verdict for one trace.
1449
+ *
1450
+ * `RandomSampler` rolls the dice per process, so an upstream service can keep
1451
+ * a trace that its downstream drops, leaving a waterfall with holes in it.
1452
+ * Hashing a key that travels with the request removes the disagreement. Pass
1453
+ * the trace id, or any identifier every hop already shares.
884
1454
  *
885
1455
  * @example
886
1456
  * ```typescript
887
- * const id = getCorrelationId();
888
- * if (id) {
889
- * console.log('Correlation ID:', id);
890
- * }
1457
+ * new DeterministicSampler({
1458
+ * sampleRate: 0.1,
1459
+ * key: (context) => trace.getActiveSpan()?.spanContext().traceId,
1460
+ * })
891
1461
  * ```
892
1462
  */
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
- }
1463
+ var DeterministicSampler = class {
1464
+ rate;
1465
+ key;
1466
+ constructor(options) {
1467
+ if (options.sampleRate < 0 || options.sampleRate > 1) throw new Error("Sample rate must be between 0 and 1");
1468
+ this.rate = options.sampleRate;
1469
+ this.key = options.key;
1470
+ }
1471
+ shouldSample(context) {
1472
+ const key = this.key(context);
1473
+ if (key === void 0) return Math.random() < this.rate;
1474
+ return hashUnitInterval(key) < this.rate;
1475
+ }
1476
+ sampleRate() {
1477
+ return toSampleRate(this.rate);
1478
+ }
1479
+ };
1480
+ /** Bucket for keys seen after the tracked map fills up. */
1481
+ const OVERFLOW_KEY = "__overflow__";
1482
+ /**
1483
+ * Per-key target-rate sampler for workloads with uneven traffic.
1484
+ *
1485
+ * A single rate serves a skewed workload badly: 1% floods storage with the
1486
+ * busiest endpoint and still loses the rare tenant whose failures you need.
1487
+ * This sampler counts traffic per key over a rolling window, then sets each
1488
+ * key its own rate so every key contributes roughly `targetPerKey` events.
1489
+ * Quiet keys survive intact; loud keys get thinned.
1490
+ *
1491
+ * The first window keeps everything, because no traffic history exists yet.
1492
+ * Rates take effect from the second window onward.
1493
+ *
1494
+ * @example
1495
+ * ```typescript
1496
+ * new KeyTargetRateSampler({
1497
+ * key: (context) => context.operationName,
1498
+ * targetPerKey: 10, // ~10 events per key per window
1499
+ * windowMs: 30_000,
1500
+ * })
1501
+ * ```
1502
+ */
1503
+ var KeyTargetRateSampler = class {
1504
+ key;
1505
+ targetPerKey;
1506
+ windowMs;
1507
+ maxKeys;
1508
+ counts = /* @__PURE__ */ new Map();
1509
+ rates = /* @__PURE__ */ new Map();
1510
+ windowStart = Date.now();
1511
+ constructor(options) {
1512
+ this.key = options.key;
1513
+ this.targetPerKey = options.targetPerKey ?? 10;
1514
+ this.windowMs = options.windowMs ?? 3e4;
1515
+ this.maxKeys = options.maxKeys ?? 1e3;
1516
+ if (this.targetPerKey <= 0) throw new Error("Target per key must be greater than 0");
1517
+ if (this.windowMs <= 0) throw new Error("Window must be greater than 0");
1518
+ }
1519
+ /** Turn the window's observed counts into the next window's rates. */
1520
+ roll(now) {
1521
+ if (now - this.windowStart < this.windowMs) return;
1522
+ const rates = /* @__PURE__ */ new Map();
1523
+ for (const [key, count] of this.counts) rates.set(key, Math.max(1, count / this.targetPerKey));
1524
+ this.rates = rates;
1525
+ this.counts = /* @__PURE__ */ new Map();
1526
+ this.windowStart = now;
1527
+ }
1528
+ /**
1529
+ * Resolve the key, collapsing into one bucket once the map is full.
1530
+ *
1531
+ * An unbounded key function would otherwise grow the map without limit,
1532
+ * which turns a sampler meant to cut cost into a memory leak.
1533
+ */
1534
+ resolveKey(context) {
1535
+ const key = this.key(context) ?? OVERFLOW_KEY;
1536
+ if (this.counts.has(key) || this.counts.size < this.maxKeys) return key;
1537
+ return OVERFLOW_KEY;
1538
+ }
1539
+ shouldSample(context) {
1540
+ this.roll(Date.now());
1541
+ const key = this.resolveKey(context);
1542
+ this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
1543
+ const rate = this.rates.get(key) ?? 1;
1544
+ return rate <= 1 || Math.random() < 1 / rate;
1545
+ }
1546
+ sampleRate(context) {
1547
+ return this.rates.get(this.resolveKey(context)) ?? 1;
1548
+ }
1549
+ };
1550
+ /**
1551
+ * Composite sampler that combines multiple samplers
1552
+ *
1553
+ * Samples if ANY of the child samplers returns true.
1554
+ *
1555
+ * @example
1556
+ * ```typescript
1557
+ * new CompositeSampler([
1558
+ * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
1559
+ * new AdaptiveSampler({ baselineSampleRate: 0.1 })
1560
+ * ])
1561
+ * ```
1562
+ */
1563
+ var CompositeSampler = class {
1564
+ samplers;
1565
+ constructor(samplers) {
1566
+ this.samplers = samplers;
1567
+ if (samplers.length === 0) throw new Error("CompositeSampler requires at least one child sampler");
1568
+ }
1569
+ shouldSample(context) {
1570
+ return this.samplers.some((sampler) => sampler.shouldSample(context));
1571
+ }
1572
+ };
902
1573
  /**
903
- * Get or create a correlation ID
904
- *
905
- * If a correlation ID exists in the current context, returns it.
906
- * Otherwise, generates a new one.
1574
+ * Feature flag sampler
907
1575
  *
908
- * @returns Existing or new correlation ID
1576
+ * Always samples requests with specific feature flags enabled.
1577
+ * Perfect for correlating A/B test experiments with metrics.
909
1578
  *
910
1579
  * @example
911
1580
  * ```typescript
912
- * const id = getOrCreateCorrelationId();
913
- * // Always returns a valid correlation ID
1581
+ * new FeatureFlagSampler({
1582
+ * baselineSampleRate: 0.01,
1583
+ * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
1584
+ * extractFlags: (args, metadata) => metadata?.featureFlags
1585
+ * })
914
1586
  * ```
915
1587
  */
916
- function getOrCreateCorrelationId() {
917
- return getCorrelationId() ?? generateCorrelationId();
918
- }
1588
+ var FeatureFlagSampler = class {
1589
+ baselineSampleRate;
1590
+ alwaysSampleFlags;
1591
+ extractFlags;
1592
+ logger;
1593
+ constructor(options) {
1594
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1595
+ this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
1596
+ this.extractFlags = options.extractFlags;
1597
+ this.logger = options.logger;
1598
+ }
1599
+ shouldSample(context) {
1600
+ const flags = this.extractFlags(context.args, context.metadata);
1601
+ if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
1602
+ this.logger?.debug({
1603
+ operation: context.operationName,
1604
+ flags
1605
+ }, "Sampling feature flag request");
1606
+ return true;
1607
+ }
1608
+ return Math.random() < this.baselineSampleRate;
1609
+ }
1610
+ /**
1611
+ * Add feature flags to always-sample list
1612
+ */
1613
+ addAlwaysSampleFlags(...flags) {
1614
+ for (const flag of flags) this.alwaysSampleFlags.add(flag);
1615
+ }
1616
+ /**
1617
+ * Remove feature flags from always-sample list
1618
+ */
1619
+ removeAlwaysSampleFlags(...flags) {
1620
+ for (const flag of flags) this.alwaysSampleFlags.delete(flag);
1621
+ }
1622
+ };
919
1623
  /**
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.
1624
+ * Sampling preset factories.
924
1625
  *
925
- * @param correlationId - Correlation ID to use
926
- * @param fn - Function to execute
927
- * @returns The return value of the function
1626
+ * For most users, the string shorthand on `init()` is simpler:
1627
+ * ```typescript
1628
+ * init({ service: 'my-app', sampling: 'production' })
1629
+ * ```
928
1630
  *
929
- * @example
1631
+ * Use factories when you need to customize:
930
1632
  * ```typescript
931
- * await runWithCorrelationId('abc123', async () => {
932
- * // getCorrelationId() returns 'abc123' here
933
- * await processRequest();
934
- * });
1633
+ * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
935
1634
  * ```
936
1635
  */
937
- function runWithCorrelationId(correlationId, fn) {
938
- return correlationStorage.run({ value: correlationId }, fn);
1636
+ const samplingPresets = {
1637
+ /** Capture everything — best for local development and debugging */
1638
+ development: () => new AlwaysSampler(),
1639
+ /** Only bad outcomes — zero baseline, errors always kept */
1640
+ errorsOnly: () => new AdaptiveSampler({
1641
+ baselineSampleRate: 0,
1642
+ alwaysSampleErrors: true
1643
+ }),
1644
+ /**
1645
+ * Balanced production defaults — 10% baseline + errors + slow traces.
1646
+ * Pass overrides to tune (uses the same option names as AdaptiveSampler).
1647
+ */
1648
+ production: (overrides) => new AdaptiveSampler({
1649
+ baselineSampleRate: .1,
1650
+ alwaysSampleErrors: true,
1651
+ alwaysSampleSlow: true,
1652
+ slowThresholdMs: 1e3,
1653
+ ...overrides
1654
+ }),
1655
+ /** Disable sampling entirely */
1656
+ off: () => new NeverSampler()
1657
+ };
1658
+ /**
1659
+ * Resolve a preset string to a Sampler instance.
1660
+ * Used internally by `init()` when `sampling` string is provided.
1661
+ *
1662
+ * @throws Error if preset is not recognized
1663
+ */
1664
+ function resolveSamplingPreset(preset) {
1665
+ switch (preset) {
1666
+ case "development": return samplingPresets.development();
1667
+ case "errors-only": return samplingPresets.errorsOnly();
1668
+ case "production": return samplingPresets.production();
1669
+ case "off": return samplingPresets.off();
1670
+ default: throw new Error(`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`);
1671
+ }
939
1672
  }
940
1673
  /**
941
- * Set correlation ID in the current context (mutates context)
1674
+ * Create a Link from W3C trace context headers (e.g., from a message queue).
942
1675
  *
943
- * Note: This updates the AsyncLocalStorage context. For proper scoping
944
- * across async boundaries, prefer runWithCorrelationId() instead.
1676
+ * This is useful for message consumers that need to link to the producer span.
1677
+ * The headers should contain at least a `traceparent` header in W3C format.
945
1678
  *
946
- * @param correlationId - Correlation ID to set
1679
+ * @param headers - Dictionary containing traceparent/tracestate headers
1680
+ * @param attributes - Optional attributes for the link
1681
+ * @returns Link object if context could be extracted, null otherwise
947
1682
  *
948
1683
  * @example
949
1684
  * ```typescript
950
- * setCorrelationId('abc123');
951
- * // Now getCorrelationId() returns 'abc123'
1685
+ * // In a Kafka consumer
1686
+ * const headers = { traceparent: '00-abc123...-def456...-01' };
1687
+ * const link = createLinkFromHeaders(headers);
1688
+ * if (link) {
1689
+ * // Use with tracer.startActiveSpan options or ctx.addLink()
1690
+ * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
1691
+ * }
952
1692
  * ```
953
1693
  */
954
- function setCorrelationId(correlationId) {
955
- enterOrRun(correlationStorage, correlationId);
1694
+ function createLinkFromHeaders(headers, attributes) {
1695
+ const traceparent = headers.traceparent || headers["traceparent"];
1696
+ if (!traceparent) return null;
1697
+ const spanContext = parseTraceparent(traceparent);
1698
+ if (!spanContext || !isValidSpanContext(spanContext)) return null;
1699
+ return {
1700
+ context: spanContext,
1701
+ attributes: attributes ?? {}
1702
+ };
956
1703
  }
957
1704
  /**
958
- * Set correlation ID in baggage for propagation
959
- *
960
- * This adds the correlation ID to the W3C baggage header, allowing it
961
- * to be propagated to downstream services.
1705
+ * Extract Links from a batch of messages for fan-in scenarios.
962
1706
  *
963
- * Note: Only use this when you explicitly want cross-service propagation.
964
- * Default is OFF to avoid header bloat.
1707
+ * Useful for batch processing where multiple producer spans should be linked.
1708
+ * This enables tracing causality in event-driven architectures where a single
1709
+ * consumer processes messages from multiple producers.
965
1710
  *
966
- * @param correlationId - Correlation ID to propagate
967
- * @returns New context with baggage set
1711
+ * @param messages - List of message objects
1712
+ * @param headersKey - Key in each message containing trace headers (default: 'headers')
1713
+ * @returns List of Link objects for all valid trace contexts
968
1714
  *
969
1715
  * @example
970
1716
  * ```typescript
971
- * const newContext = setCorrelationIdInBaggage('abc123');
972
- * context.with(newContext, () => {
973
- * // Baggage will be propagated in outgoing requests
1717
+ * // Processing a batch of SQS/Kafka messages
1718
+ * const messages = [
1719
+ * { body: '...', headers: { traceparent: '...' } },
1720
+ * { body: '...', headers: { traceparent: '...' } },
1721
+ * ];
1722
+ * const links = extractLinksFromBatch(messages);
1723
+ *
1724
+ * tracer.startActiveSpan('process.batch', { links }, span => {
1725
+ * for (const msg of messages) {
1726
+ * processMessage(msg);
1727
+ * }
974
1728
  * });
975
1729
  * ```
976
1730
  */
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);
1731
+ function extractLinksFromBatch(messages, headersKey = "headers") {
1732
+ const links = [];
1733
+ for (const msg of messages) {
1734
+ const msgHeaders = require_values.asStringRecord(msg[headersKey]);
1735
+ if (msgHeaders) {
1736
+ const link = createLinkFromHeaders(msgHeaders, { "messaging.batch.message_index": links.length });
1737
+ if (link) links.push(link);
1738
+ }
1739
+ }
1740
+ return links;
982
1741
  }
983
1742
  /**
984
- * Get the correlation storage instance (for internal use in init/shutdown)
1743
+ * Parse W3C traceparent header into SpanContext
1744
+ * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
1745
+ *
1746
+ * @see https://www.w3.org/TR/trace-context/#traceparent-header
985
1747
  */
986
- function getCorrelationStorage() {
987
- return correlationStorage;
1748
+ function parseTraceparent(traceparent) {
1749
+ const match = traceparent.match(/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
1750
+ if (!match || match.length < 5) return null;
1751
+ const version = match[1];
1752
+ const traceId = match[2];
1753
+ const spanId = match[3];
1754
+ const flags = match[4];
1755
+ if (!version || !traceId || !spanId || !flags) return null;
1756
+ if (version === "ff") return null;
1757
+ return {
1758
+ traceId,
1759
+ spanId,
1760
+ traceFlags: Number.parseInt(flags, 16),
1761
+ isRemote: true
1762
+ };
1763
+ }
1764
+ /**
1765
+ * Check if a SpanContext is valid (has non-zero trace and span IDs)
1766
+ */
1767
+ function isValidSpanContext(spanContext) {
1768
+ if (!spanContext) return false;
1769
+ return spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId !== "0000000000000000";
1770
+ }
1771
+ /** True when the request in flight asked for full-fidelity capture. */
1772
+ function debugCaptureRequested() {
1773
+ const entry = _opentelemetry_api.propagation.getBaggage(getActiveContextWithBaggage())?.getEntry(AUTOTEL_DEBUG_BAGGAGE_KEY);
1774
+ return entry !== void 0 && entry.value !== "" && entry.value !== "0";
988
1775
  }
989
1776
 
990
1777
  //#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
- }
1778
+ //#region src/init-logger.ts
1779
+ const LOG_LEVELS = {
1780
+ debug: 0,
1781
+ info: 1,
1782
+ warn: 2,
1783
+ error: 3
1001
1784
  };
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();
1785
+ /** Silent logger used until and unless the application opts into diagnostics. */
1786
+ const silentLogger = {
1787
+ info: () => {},
1788
+ warn: () => {},
1789
+ error: () => {},
1790
+ debug: () => {}
1791
+ };
1792
+ /** Apply the resolved silence and minimum-level settings to a logger. */
1793
+ function wrapLogger(base, silent, minLevel) {
1794
+ if (silent) return silentLogger;
1795
+ const threshold = LOG_LEVELS[minLevel];
1796
+ const wrap = (fn, level) => {
1797
+ if (LOG_LEVELS[level] < threshold) return (() => {});
1798
+ return ((...args) => fn(...args));
1799
+ };
1800
+ return {
1801
+ debug: wrap(base.debug, "debug"),
1802
+ info: wrap(base.info, "info"),
1803
+ warn: wrap(base.warn, "warn"),
1804
+ error: wrap(base.error, "error")
1805
+ };
1013
1806
  }
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();
1807
+
1808
+ //#endregion
1809
+ //#region src/posthog-logs.ts
1810
+ var RedactingLogRecordProcessor = class {
1811
+ wrapped;
1812
+ redact;
1813
+ constructor(wrapped, redact) {
1814
+ this.wrapped = wrapped;
1815
+ this.redact = redact;
1054
1816
  }
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
- };
1817
+ onEmit(logRecord, context) {
1818
+ const body = require_values.asString(logRecord.body);
1819
+ if (body) logRecord.body = this.redact(body);
1820
+ if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
1821
+ const text = require_values.asString(value);
1822
+ if (text !== void 0) logRecord.attributes[key] = this.redact(text);
1823
+ else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
1824
+ const entry = require_values.asString(item);
1825
+ return entry === void 0 ? item : this.redact(entry);
1826
+ });
1827
+ }
1828
+ this.wrapped.onEmit(logRecord, context);
1116
1829
  }
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}`);
1830
+ shutdown() {
1831
+ return this.wrapped.shutdown();
1140
1832
  }
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`);
1833
+ forceFlush() {
1834
+ return this.wrapped.forceFlush();
1155
1835
  }
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);
1836
+ };
1837
+ /**
1838
+ * Build log record processors for PostHog OTLP logs integration.
1839
+ *
1840
+ * Resolution order:
1841
+ * 1. config.url if provided
1842
+ * 2. POSTHOG_LOGS_URL env var
1843
+ * 3. Empty array (disabled)
1844
+ */
1845
+ function buildPostHogLogProcessors(config, stringRedactor) {
1846
+ const url = config?.url || process.env.POSTHOG_LOGS_URL;
1847
+ if (!url) return [];
1848
+ const sdkLogs = require_node_require.safeRequire("@opentelemetry/sdk-logs");
1849
+ const exporterModule = require_node_require.safeRequire("@opentelemetry/exporter-logs-otlp-http");
1850
+ if (!sdkLogs || !exporterModule) return [];
1851
+ const exporter = new exporterModule.OTLPLogExporter({ url });
1852
+ let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
1853
+ if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
1854
+ return [processor];
1855
+ }
1856
+
1857
+ //#endregion
1858
+ //#region src/tail-sampling-processor.ts
1859
+ var TailSamplingSpanProcessor = class {
1860
+ wrappedProcessor;
1861
+ constructor(wrappedProcessor) {
1862
+ this.wrappedProcessor = wrappedProcessor;
1162
1863
  }
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);
1864
+ onStart(span, parentContext) {
1865
+ this.wrappedProcessor.onStart(span, parentContext);
1171
1866
  }
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();
1867
+ onEnd(span) {
1868
+ const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
1869
+ const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
1870
+ if (tailEvaluated === true && shouldKeep === false) return;
1871
+ this.wrappedProcessor.onEnd(span);
1194
1872
  }
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);
1873
+ forceFlush() {
1874
+ return this.wrappedProcessor.forceFlush();
1204
1875
  }
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
- }
1876
+ shutdown() {
1877
+ return this.wrappedProcessor.shutdown();
1222
1878
  }
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);
1879
+ };
1880
+
1881
+ //#endregion
1882
+ //#region src/baggage-span-processor.ts
1883
+ /**
1884
+ * Span processor that automatically copies baggage entries to span attributes
1885
+ *
1886
+ * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
1887
+ * without manually calling ctx.setAttribute() for each baggage entry.
1888
+ *
1889
+ * @example Enable in init()
1890
+ * ```typescript
1891
+ * init({
1892
+ * service: 'my-app',
1893
+ * baggage: true // Uses default 'baggage.' prefix
1894
+ * });
1895
+ *
1896
+ * // Now baggage automatically appears as span attributes
1897
+ * await withBaggage({
1898
+ * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
1899
+ * fn: async () => {
1900
+ * // Span has baggage.tenant.id and baggage.user.id attributes!
1901
+ * }
1902
+ * });
1903
+ * ```
1904
+ *
1905
+ * @example Custom prefix
1906
+ * ```typescript
1907
+ * init({
1908
+ * service: 'my-app',
1909
+ * baggage: 'ctx' // Uses 'ctx.' prefix
1910
+ * });
1911
+ * // Creates attributes: ctx.tenant.id, ctx.user.id
1912
+ * ```
1913
+ */
1914
+ var BaggageSpanProcessor = class {
1915
+ prefix;
1916
+ constructor(options = {}) {
1917
+ this.prefix = options.prefix ?? "baggage.";
1229
1918
  }
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");
1919
+ onStart(span, parentContext) {
1920
+ let baggage = _opentelemetry_api.propagation.getBaggage(parentContext);
1921
+ if (!baggage) baggage = _opentelemetry_api.propagation.getBaggage(_opentelemetry_api.context.active());
1922
+ if (!baggage) try {
1923
+ const { getActiveContextWithBaggage } = require_node_require.requireModule("./trace-context");
1924
+ const storedContext = getActiveContextWithBaggage();
1925
+ baggage = _opentelemetry_api.propagation.getBaggage(storedContext);
1926
+ } catch {}
1927
+ if (!baggage) return;
1928
+ for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
1929
+ }
1930
+ onEnd(_span) {}
1931
+ async shutdown() {}
1932
+ async forceFlush() {}
1933
+ };
1934
+
1935
+ //#endregion
1936
+ //#region src/redact-values.ts
1937
+ /** Standalone string redaction for use outside the span processor pipeline. */
1938
+ function createStringRedactor(config) {
1939
+ const resolved = typeof config === "string" ? require_attribute_redacting_processor.REDACTOR_PRESETS[config] : config;
1940
+ const valuePatterns = resolved.valuePatterns ?? [];
1941
+ const defaultReplacement = resolved.replacement ?? "[REDACTED]";
1942
+ return (value) => {
1943
+ let result = value;
1944
+ for (const { pattern, replacement, mask } of valuePatterns) {
1945
+ pattern.lastIndex = 0;
1946
+ result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
1259
1947
  }
1948
+ return result;
1949
+ };
1950
+ }
1951
+
1952
+ //#endregion
1953
+ //#region src/env-config.ts
1954
+ /**
1955
+ * Validate URL format
1956
+ */
1957
+ function isValidUrl(urlString) {
1958
+ try {
1959
+ const url = new URL(urlString);
1960
+ return url.protocol === "http:" || url.protocol === "https:";
1961
+ } catch {
1962
+ return false;
1963
+ }
1964
+ }
1965
+ /**
1966
+ * Resolve OpenTelemetry environment variables from process.env
1967
+ */
1968
+ function resolveOtelEnv() {
1969
+ const env = {};
1970
+ if (process.env.OTEL_SERVICE_NAME) {
1971
+ const value = process.env.OTEL_SERVICE_NAME.trim();
1972
+ if (value) env.OTEL_SERVICE_NAME = value;
1260
1973
  }
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;
1974
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
1975
+ const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
1976
+ if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
1286
1977
  }
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;
1978
+ if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
1979
+ const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
1980
+ if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
1323
1981
  }
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;
1332
- }
1333
- if (this.flushPromise) await this.flushPromise;
1334
- while (this.queue.length > 0) await this.doFlushBatch();
1982
+ if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
1983
+ const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
1984
+ if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
1335
1985
  }
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
- }
1986
+ if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
1987
+ const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
1988
+ if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
1989
+ else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
1357
1990
  }
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 = [];
1991
+ if (process.env.OTEL_TRACES_SAMPLER) {
1992
+ const value = process.env.OTEL_TRACES_SAMPLER.trim();
1993
+ if (value) env.OTEL_TRACES_SAMPLER = value;
1367
1994
  }
1368
- /**
1369
- * Get queue size (for testing/debugging)
1370
- */
1371
- size() {
1372
- return this.queue.length;
1995
+ if (process.env.OTEL_TRACES_SAMPLER_ARG) {
1996
+ const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
1997
+ if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
1373
1998
  }
1374
- /**
1375
- * Get subscriber health status (for testing/debugging)
1376
- */
1377
- getSubscriberHealth() {
1378
- return new Map(this.subscriberHealthy);
1999
+ return env;
2000
+ }
2001
+ function parseRatioSamplerArg(samplerName, samplerArg) {
2002
+ if (samplerArg === void 0) return 1;
2003
+ const ratio = Number(samplerArg);
2004
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
2005
+ console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
2006
+ return 1;
1379
2007
  }
1380
- /**
1381
- * Check if a specific subscriber is healthy
1382
- */
1383
- isSubscriberHealthy(subscriberName) {
1384
- return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
2008
+ return ratio;
2009
+ }
2010
+ function warnOnUnusedSamplerArg(samplerName, samplerArg) {
2011
+ if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
2012
+ }
2013
+ function createSamplerFromEnv(env) {
2014
+ const samplerName = env.OTEL_TRACES_SAMPLER;
2015
+ if (!samplerName) return;
2016
+ switch (samplerName) {
2017
+ case "always_on":
2018
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2019
+ return new _opentelemetry_sdk_trace_base.AlwaysOnSampler();
2020
+ case "always_off":
2021
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2022
+ return new _opentelemetry_sdk_trace_base.AlwaysOffSampler();
2023
+ case "traceidratio": return new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
2024
+ case "parentbased_always_on":
2025
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2026
+ return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOnSampler() });
2027
+ case "parentbased_always_off":
2028
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2029
+ return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.AlwaysOffSampler() });
2030
+ case "parentbased_traceidratio": return new _opentelemetry_sdk_trace_base.ParentBasedSampler({ root: new _opentelemetry_sdk_trace_base.TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
2031
+ case "jaeger_remote":
2032
+ case "parentbased_jaeger_remote":
2033
+ case "xray":
2034
+ console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
2035
+ return;
2036
+ default:
2037
+ console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
2038
+ return;
1385
2039
  }
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);
2040
+ }
2041
+ /**
2042
+ * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
2043
+ * Example: "service.version=1.0.0,deployment.environment=production"
2044
+ */
2045
+ function parseResourceAttributes(input) {
2046
+ if (!input || input.trim() === "") return {};
2047
+ const attributes = {};
2048
+ const pairs = input.split(",");
2049
+ for (const pair of pairs) {
2050
+ const trimmedPair = pair.trim();
2051
+ if (!trimmedPair) continue;
2052
+ const equalIndex = trimmedPair.indexOf("=");
2053
+ if (equalIndex === -1) continue;
2054
+ const key = trimmedPair.slice(0, equalIndex).trim();
2055
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2056
+ if (key && value) attributes[key] = value;
2057
+ }
2058
+ return attributes;
2059
+ }
2060
+ /**
2061
+ * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
2062
+ * Example: "api-key=secret123,x-custom-header=value"
2063
+ */
2064
+ function parseOtlpHeaders(input) {
2065
+ if (!input || input.trim() === "") return {};
2066
+ const headers = {};
2067
+ const pairs = input.split(",");
2068
+ for (const pair of pairs) {
2069
+ const trimmedPair = pair.trim();
2070
+ if (!trimmedPair) continue;
2071
+ const equalIndex = trimmedPair.indexOf("=");
2072
+ if (equalIndex === -1) continue;
2073
+ const key = trimmedPair.slice(0, equalIndex).trim();
2074
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2075
+ if (key && value) headers[key] = value;
2076
+ }
2077
+ return headers;
2078
+ }
2079
+ /**
2080
+ * Convert resolved environment variables to config
2081
+ */
2082
+ function envToConfig(env) {
2083
+ const config = {};
2084
+ if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
2085
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
2086
+ if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
2087
+ if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
2088
+ const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
2089
+ if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
2090
+ const sampler = createSamplerFromEnv(env);
2091
+ if (sampler) config.otelSampler = sampler;
2092
+ return config;
2093
+ }
2094
+ /**
2095
+ * Main function to resolve config from environment variables
2096
+ */
2097
+ function resolveConfigFromEnv() {
2098
+ return envToConfig(resolveOtelEnv());
2099
+ }
2100
+
2101
+ //#endregion
2102
+ //#region src/yaml-config.ts
2103
+ /**
2104
+ * YAML configuration loader for autotel
2105
+ *
2106
+ * Supports:
2107
+ * - Auto-discovery of autotel.yaml in cwd
2108
+ * - AUTOTEL_CONFIG_FILE env var override
2109
+ * - Environment variable substitution: ${env:VAR} and ${env:VAR:-default}
2110
+ *
2111
+ * @example Auto-discovery
2112
+ * ```yaml
2113
+ * # autotel.yaml in project root
2114
+ * service:
2115
+ * name: my-service
2116
+ * exporter:
2117
+ * endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}
2118
+ * ```
2119
+ *
2120
+ * @example Explicit path
2121
+ * ```bash
2122
+ * AUTOTEL_CONFIG_FILE=./config/otel.yaml tsx --import autotel/auto src/index.ts
2123
+ * ```
2124
+ */
2125
+ /**
2126
+ * Lazy-load yaml parser (optional peer dependency)
2127
+ * Only loads when a YAML config file is actually found
2128
+ */
2129
+ function loadYamlParser() {
2130
+ try {
2131
+ return require_node_require.requireModule("yaml").parse;
2132
+ } catch {
2133
+ throw new Error("YAML parser not found. Install with: pnpm add yaml");
1392
2134
  }
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";
2135
+ }
2136
+ /**
2137
+ * Environment variable substitution regex
2138
+ * Matches ${env:VAR_NAME} and ${env:VAR_NAME:-default}
2139
+ */
2140
+ const ENV_VAR_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
2141
+ /**
2142
+ * Substitute ${env:VAR} and ${env:VAR:-default} in a string
2143
+ *
2144
+ * @param value - String potentially containing env var references
2145
+ * @returns String with env vars substituted
2146
+ *
2147
+ * @example
2148
+ * substituteEnvVars('${env:NODE_ENV:-development}')
2149
+ * // Returns 'production' if NODE_ENV=production, else 'development'
2150
+ */
2151
+ function substituteEnvVars(value) {
2152
+ return value.replaceAll(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
2153
+ const envValue = process.env[varName];
2154
+ if (envValue !== void 0) return envValue;
2155
+ if (defaultValue !== void 0) return defaultValue;
2156
+ console.warn(`[autotel] Environment variable ${varName} not set and no default provided`);
2157
+ return "";
2158
+ });
2159
+ }
2160
+ /**
2161
+ * Recursively substitute env vars in an object
2162
+ *
2163
+ * @param obj - Object to process
2164
+ * @returns Object with all string values having env vars substituted
2165
+ */
2166
+ function substituteEnvVarsDeep(value) {
2167
+ if (Array.isArray(value)) return value.map(substituteEnvVarsDeep);
2168
+ const mapping = asYamlMapping(value);
2169
+ if (mapping) {
2170
+ const result = {};
2171
+ for (const [key, entry] of Object.entries(mapping)) result[key] = substituteEnvVarsDeep(entry);
2172
+ return result;
1420
2173
  }
1421
- };
2174
+ const text = require_values.asString(value);
2175
+ return text === void 0 ? value : substituteEnvVars(text);
2176
+ }
2177
+ /** The mapping a YAML value is, when it is one. */
2178
+ function asYamlMapping(value) {
2179
+ return require_values.asRecord(value) === void 0 ? void 0 : value;
2180
+ }
1422
2181
  /**
1423
- * Validate and sanitize event name
1424
- * Throws ValidationError if invalid
2182
+ * A YAML document read into the config shape, with `${env:...}` placeholders
2183
+ * resolved.
2184
+ *
2185
+ * This is the boundary: the file is text until here. Every field of
2186
+ * YamlConfig is optional, so any document satisfies it, and
2187
+ * `yamlToAutotelConfig` picks out only the fields it understands.
1425
2188
  */
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;
2189
+ function parseYamlConfig(content) {
2190
+ return asYamlMapping(substituteEnvVarsDeep(loadYamlParser()(content))) ?? {};
1433
2191
  }
1434
2192
  /**
1435
- * Validate and sanitize attributes
1436
- * Returns sanitized attributes (sensitive data redacted)
2193
+ * Find YAML config file path
2194
+ *
2195
+ * Priority:
2196
+ * 1. AUTOTEL_CONFIG_FILE env var (explicit path)
2197
+ * 2. autotel.yaml in cwd (convention)
2198
+ * 3. autotel.yml in cwd (alternative extension)
2199
+ *
2200
+ * @returns File path if found, null otherwise
1437
2201
  */
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);
2202
+ function findConfigFile() {
2203
+ const envPath = process.env.AUTOTEL_CONFIG_FILE;
2204
+ if (envPath) {
2205
+ const resolved = node_path.default.resolve(envPath);
2206
+ if (node_fs.existsSync(resolved)) return resolved;
2207
+ console.warn(`[autotel] Config file not found: ${envPath}`);
2208
+ return null;
1452
2209
  }
1453
- return sanitized;
2210
+ const conventionPath = node_path.default.resolve(process.cwd(), "autotel.yaml");
2211
+ if (node_fs.existsSync(conventionPath)) return conventionPath;
2212
+ const altPath = node_path.default.resolve(process.cwd(), "autotel.yml");
2213
+ if (node_fs.existsSync(altPath)) return altPath;
2214
+ return null;
1454
2215
  }
1455
2216
  /**
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.
2217
+ * Convert YAML config structure to AutotelConfig
2218
+ *
2219
+ * @param yaml - Parsed and env-substituted YAML config
2220
+ * @returns Partial AutotelConfig ready for merging
1460
2221
  */
1461
- function isSensitiveString(key, value, config) {
1462
- return require_values.asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
2222
+ function yamlToAutotelConfig(yaml) {
2223
+ const config = {};
2224
+ if (yaml.service?.name) config.service = yaml.service.name;
2225
+ if (yaml.service?.version) config.version = yaml.service.version;
2226
+ if (yaml.service?.environment) config.environment = yaml.service.environment;
2227
+ if (yaml.exporter?.endpoint) config.endpoint = yaml.exporter.endpoint;
2228
+ if (yaml.exporter?.protocol) config.protocol = yaml.exporter.protocol;
2229
+ if (yaml.exporter?.headers) config.headers = yaml.exporter.headers;
2230
+ if (yaml.exporter?.destinations) config.destinations = yaml.exporter.destinations;
2231
+ if (yaml.resource) config.resourceAttributes = yaml.resource;
2232
+ if (yaml.autoInstrumentations) config.autoInstrumentations = yaml.autoInstrumentations;
2233
+ if (yaml.policies) config.policies = yaml.policies;
2234
+ if (yaml.debug !== void 0) config.debug = yaml.debug;
2235
+ if (yaml.sampling?.preset) {
2236
+ warnOnIgnoredPresetOverrides(yaml.sampling);
2237
+ config.sampling = yaml.sampling.preset;
2238
+ } else {
2239
+ const sampler = createSamplerFromYaml(yaml.sampling);
2240
+ if (sampler) config.sampler = sampler;
2241
+ }
2242
+ return config;
2243
+ }
2244
+ function createSamplerFromYaml(sampling) {
2245
+ if (!sampling) return void 0;
2246
+ if (sampling.preset) return void 0;
2247
+ const type = sampling.type ?? "adaptive";
2248
+ try {
2249
+ switch (type) {
2250
+ case "adaptive": return new AdaptiveSampler({
2251
+ baselineSampleRate: sampling.baseline_rate,
2252
+ alwaysSampleErrors: sampling.always_sample_errors,
2253
+ alwaysSampleSlow: sampling.always_sample_slow,
2254
+ slowThresholdMs: sampling.slow_threshold_ms
2255
+ });
2256
+ case "always_on": return new AlwaysSampler();
2257
+ case "always_off": return new NeverSampler();
2258
+ case "ratio":
2259
+ if (sampling.ratio === void 0) {
2260
+ console.warn("[autotel] sampling.ratio missing in YAML sampling config. Falling back to adaptive sampler.");
2261
+ return new AdaptiveSampler();
2262
+ }
2263
+ return new RandomSampler(sampling.ratio);
2264
+ default:
2265
+ console.warn(`[autotel] Unknown sampling type "${type}" in YAML config. Falling back to defaults.`);
2266
+ return;
2267
+ }
2268
+ } catch (error) {
2269
+ console.warn(`[autotel] Failed to configure sampling from YAML: ${error instanceof Error ? error.message : String(error)}`);
2270
+ return;
2271
+ }
2272
+ }
2273
+ function warnOnIgnoredPresetOverrides(sampling) {
2274
+ const ignoredFields = [
2275
+ "type",
2276
+ "ratio",
2277
+ "baseline_rate",
2278
+ "always_sample_errors",
2279
+ "always_sample_slow",
2280
+ "slow_threshold_ms"
2281
+ ].filter((field) => sampling[field] !== void 0);
2282
+ if (ignoredFields.length === 0) return;
2283
+ console.warn(`[autotel] sampling.preset="${sampling.preset}" ignores these YAML fields: ${ignoredFields.join(", ")}. Use the programmatic API with sampler or samplingPresets.*(...) for tuned presets.`);
1463
2284
  }
1464
2285
  /**
1465
- * Sanitize attribute value (recursive)
2286
+ * Load and parse YAML config file (auto-discovery)
2287
+ *
2288
+ * Automatically finds and loads autotel.yaml or uses AUTOTEL_CONFIG_FILE.
2289
+ * Returns null if no config file found (not an error - YAML config is optional).
2290
+ *
2291
+ * @returns Partial AutotelConfig or null if no config file found
2292
+ *
2293
+ * @example
2294
+ * const yamlConfig = loadYamlConfig();
2295
+ * if (yamlConfig) {
2296
+ * init({ ...yamlConfig, debug: true });
2297
+ * }
1466
2298
  */
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]";
2299
+ function loadYamlConfig() {
2300
+ const filePath = findConfigFile();
2301
+ if (!filePath) return null;
2302
+ try {
2303
+ return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(filePath, "utf8")));
2304
+ } catch (error) {
2305
+ console.error(`[autotel] Failed to load YAML config from ${filePath}:`, error);
2306
+ return null;
1483
2307
  }
1484
- return `[${require_values.describeValue(value)}]`;
1485
2308
  }
1486
2309
  /**
1487
- * Validate and sanitize an events event
1488
- * Returns { eventName, attributes } with sanitized values
2310
+ * Load YAML config from a specific file path
2311
+ *
2312
+ * Unlike loadYamlConfig(), this throws if the file cannot be read.
2313
+ *
2314
+ * @param filePath - Path to YAML config file
2315
+ * @returns Partial AutotelConfig
2316
+ * @throws Error if file cannot be read or parsed
2317
+ *
2318
+ * @example
2319
+ * import { loadYamlConfigFromFile } from 'autotel/yaml';
2320
+ * import { init } from 'autotel';
2321
+ *
2322
+ * const config = loadYamlConfigFromFile('./config/otel.yaml');
2323
+ * init({ ...config, debug: true });
1489
2324
  */
1490
- function validateEvent(eventName, attributes, config) {
1491
- const fullConfig = {
1492
- ...DEFAULT_CONFIG$1,
1493
- ...config
2325
+ function loadYamlConfigFromFile(filePath) {
2326
+ const resolved = node_path.default.resolve(filePath);
2327
+ return yamlToAutotelConfig(parseYamlConfig(node_fs.readFileSync(resolved, "utf8")));
2328
+ }
2329
+ /**
2330
+ * Check if a YAML config file exists (without loading it)
2331
+ *
2332
+ * @returns true if a config file would be found by loadYamlConfig()
2333
+ */
2334
+ function hasYamlConfig() {
2335
+ return findConfigFile() !== null;
2336
+ }
2337
+
2338
+ //#endregion
2339
+ //#region src/devtools.ts
2340
+ const defaultHost = "127.0.0.1";
2341
+ const defaultPort = 4318;
2342
+ function resolveDevtoolsConfig(config) {
2343
+ if (!config) return {
2344
+ enabled: false,
2345
+ endpoint: void 0,
2346
+ embedded: false,
2347
+ host: defaultHost,
2348
+ port: defaultPort,
2349
+ verbose: false
2350
+ };
2351
+ if (config === true) return {
2352
+ enabled: true,
2353
+ endpoint: `http://${defaultHost}:${defaultPort}`,
2354
+ embedded: false,
2355
+ host: defaultHost,
2356
+ port: defaultPort,
2357
+ verbose: false
1494
2358
  };
2359
+ const enabled = config.enabled ?? true;
2360
+ const host = config.host ?? defaultHost;
2361
+ const port = config.port ?? defaultPort;
2362
+ const endpoint = config.endpoint ?? `http://${host}:${port}`;
1495
2363
  return {
1496
- eventName: validateEventName(eventName, fullConfig),
1497
- attributes: validateAttributes(attributes, fullConfig)
2364
+ enabled,
2365
+ endpoint: enabled ? endpoint : void 0,
2366
+ embedded: enabled && (config.embedded ?? false),
2367
+ host,
2368
+ port,
2369
+ verbose: config.verbose ?? false
1498
2370
  };
1499
2371
  }
1500
2372
 
1501
2373
  //#endregion
1502
- //#region src/track.ts
2374
+ //#region src/process-handlers.ts
2375
+ let removeOwnedHandlers = [];
1503
2376
  /**
1504
- * Global track() function for business events
1505
- *
1506
- * Simple, no instantiation needed, auto-attaches trace context
2377
+ * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
2378
+ * a call to `installProcessHandlers`, which clears that list before installing
2379
+ * its own signal listeners.
1507
2380
  */
1508
- let eventsQueue = null;
2381
+ let removeExitFlush;
1509
2382
  /**
1510
- * Build autotel event context for trace correlation
1511
- *
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
2383
+ * Shared across every path that can end the process: signals, fatal errors and
2384
+ * a clean exit. They can overlap — a container stopping a job that has just
2385
+ * finished its work sends SIGTERM while the exit flush is still draining — and
2386
+ * a second shutdown would tear down queues the first is still using.
1516
2387
  */
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
1550
- });
1551
- if (traceUrl) autotelContext.trace_url = traceUrl;
1552
- }
1553
- return autotelContext;
2388
+ let shutdownInFlight;
2389
+ /**
2390
+ * The clean-exit flush, while it runs. Doubles as the latch that keeps a
2391
+ * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
2392
+ * waits on rather than tearing down queues mid-drain.
2393
+ */
2394
+ let exitFlushInFlight;
2395
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
2396
+ function runShutdownOnce(shutdown, timeoutMs) {
2397
+ if (shutdownInFlight) return shutdownInFlight;
2398
+ let timeoutHandle;
2399
+ const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
2400
+ const timeout = new Promise((resolve) => {
2401
+ timeoutHandle = setTimeout(resolve, timeoutMs);
2402
+ timeoutHandle.unref();
2403
+ });
2404
+ shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
2405
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2406
+ });
2407
+ return shutdownInFlight;
2408
+ }
2409
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
2410
+ function signalExitCode(signal) {
2411
+ return 128 + node_os.constants.signals[signal];
1554
2412
  }
1555
2413
  /**
1556
- * Initialize events queue lazily
2414
+ * Surface a fatal error before shutting down.
2415
+ *
2416
+ * Registering an `uncaughtException` / `unhandledRejection` listener overrides
2417
+ * Node's default of printing the stack to stderr, so without this a crash under
2418
+ * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
2419
+ * so we print to stderr directly to guarantee the crash stays visible.
1557
2420
  */
1558
- function getOrCreateQueue() {
1559
- if (!isInitialized()) {
1560
- warnIfNotInitialized("track()");
1561
- return null;
2421
+ function reportFatalError(error, event) {
2422
+ const err = error instanceof Error ? error : new Error(String(error));
2423
+ console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
2424
+ }
2425
+ function installProcessHandlers(config, shutdown) {
2426
+ uninstallProcessHandlers();
2427
+ const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
2428
+ let exitScheduled = false;
2429
+ let resolvedExitCode = 0;
2430
+ let fatalLatched = false;
2431
+ const shutdownAndExit = (exitCode, fatal) => {
2432
+ if (fatal) {
2433
+ if (!fatalLatched) {
2434
+ resolvedExitCode = exitCode;
2435
+ fatalLatched = true;
2436
+ }
2437
+ } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
2438
+ if (exitScheduled) return;
2439
+ exitScheduled = true;
2440
+ runShutdownOnce(shutdown, timeoutMs).then(() => {
2441
+ process.exit(resolvedExitCode);
2442
+ });
2443
+ };
2444
+ for (const signal of config.signals ?? DEFAULT_SIGNALS) {
2445
+ const listener = () => {
2446
+ shutdownAndExit(signalExitCode(signal), false);
2447
+ };
2448
+ process.on(signal, listener);
2449
+ removeOwnedHandlers.push(() => {
2450
+ process.removeListener(signal, listener);
2451
+ });
1562
2452
  }
1563
- if (!eventsQueue) {
1564
- const config = getConfig();
1565
- if (!config?.subscribers || config.subscribers.length === 0) return null;
1566
- eventsQueue = new EventQueue(config.subscribers);
2453
+ if (config.fatalErrors ?? true) {
2454
+ const uncaughtExceptionListener = (error) => {
2455
+ reportFatalError(error, "uncaughtException");
2456
+ shutdownAndExit(1, true);
2457
+ };
2458
+ const unhandledRejectionListener = (reason) => {
2459
+ reportFatalError(reason, "unhandledRejection");
2460
+ shutdownAndExit(1, true);
2461
+ };
2462
+ process.on("uncaughtException", uncaughtExceptionListener);
2463
+ process.on("unhandledRejection", unhandledRejectionListener);
2464
+ removeOwnedHandlers.push(() => {
2465
+ process.removeListener("uncaughtException", uncaughtExceptionListener);
2466
+ }, () => {
2467
+ process.removeListener("unhandledRejection", unhandledRejectionListener);
2468
+ });
1567
2469
  }
1568
- return eventsQueue;
1569
2470
  }
1570
2471
  /**
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
- * }
2472
+ * Flush telemetry when the process runs to completion.
1590
2473
  *
1591
- * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
1592
- * ```
2474
+ * The signal and fatal-error handlers above cover a process that is stopped or
2475
+ * that crashes. Neither fires when a script simply finishes: the event loop
2476
+ * drains and Node exits, taking whatever the batch span processor was still
2477
+ * holding with it. `beforeExit` is the only hook for that case, and it is the
2478
+ * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
1593
2479
  *
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
2480
+ * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
2481
+ * only the final one, so tearing the SDK down here would silently kill
2482
+ * telemetry in a process that goes on to do more work.
1627
2483
  */
1628
- function getEventQueue() {
1629
- return eventsQueue;
2484
+ function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
2485
+ removeExitFlush?.();
2486
+ exitFlushInFlight = void 0;
2487
+ const listener = (code) => {
2488
+ if (exitFlushInFlight || shutdownInFlight) return;
2489
+ const deadline = setTimeout(() => {
2490
+ process.exit(process.exitCode ?? code);
2491
+ }, timeoutMs);
2492
+ exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
2493
+ clearTimeout(deadline);
2494
+ });
2495
+ };
2496
+ process.on("beforeExit", listener);
2497
+ removeExitFlush = () => {
2498
+ process.removeListener("beforeExit", listener);
2499
+ removeExitFlush = void 0;
2500
+ };
1630
2501
  }
1631
- /**
1632
- * Reset events queue (for shutdown/cleanup)
1633
- * @internal
1634
- */
1635
- function resetEventQueue() {
1636
- eventsQueue = null;
2502
+ function uninstallProcessHandlers() {
2503
+ for (const removeHandler of removeOwnedHandlers) removeHandler();
2504
+ removeOwnedHandlers = [];
2505
+ removeExitFlush?.();
2506
+ shutdownInFlight = void 0;
2507
+ exitFlushInFlight = void 0;
1637
2508
  }
1638
2509
 
1639
2510
  //#endregion
@@ -2472,6 +3343,15 @@ function errorCode(cause) {
2472
3343
  return require_values.asString(require_values.readProperty(cause, "code"));
2473
3344
  }
2474
3345
  /**
3346
+ * A bare "Flush timeout" left you with a stack trace into bundled internals and
3347
+ * nothing to act on. Name how long we waited and where we were sending, since
3348
+ * the usual cause is that nothing is listening at that endpoint.
3349
+ */
3350
+ function flushTimeoutMessage(timeout) {
3351
+ const endpoint = getConfig()?.endpoint;
3352
+ return `Flush timeout after ${timeout}ms${endpoint === void 0 ? "" : ` sending to ${endpoint}`}. No collector acknowledged the export; check that one is running and reachable.`;
3353
+ }
3354
+ /**
2475
3355
  * True when the error (or every error it wraps) is an unreachable-endpoint
2476
3356
  * failure. Traverses `AggregateError.errors` and the `cause` chain, since the
2477
3357
  * SDK often wraps the underlying network error.
@@ -2528,7 +3408,7 @@ async function flush(options) {
2528
3408
  await Promise.race([doFlush().finally(() => {
2529
3409
  if (timeoutHandle) clearTimeout(timeoutHandle);
2530
3410
  }), new Promise((_, reject) => {
2531
- timeoutHandle = setTimeout(() => reject(/* @__PURE__ */ new Error("Flush timeout")), timeout);
3411
+ timeoutHandle = setTimeout(() => reject(new Error(flushTimeoutMessage(timeout))), timeout);
2532
3412
  timeoutHandle.unref();
2533
3413
  })]);
2534
3414
  } catch (error) {
@@ -2990,7 +3870,7 @@ function toOtelSampler(sampler) {
2990
3870
  const shouldTrace = sampler.shouldSample(samplingContext);
2991
3871
  const rate = sampler.sampleRate?.(samplingContext);
2992
3872
  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 };
3873
+ if (shouldTrace && rate !== void 0 && rate > 1) result.attributes = { [AUTOTEL_SAMPLING_RATE]: rate };
2994
3874
  return result;
2995
3875
  },
2996
3876
  toString() {
@@ -3090,7 +3970,7 @@ function isLoggerLocked() {
3090
3970
  function init(cfg) {
3091
3971
  if (locked) return;
3092
3972
  const envConfig = resolveConfigFromEnv();
3093
- const yamlConfig = require_yaml_config.loadYamlConfig() ?? {};
3973
+ const yamlConfig = loadYamlConfig() ?? {};
3094
3974
  const mergedConfig = {
3095
3975
  ...envConfig,
3096
3976
  ...yamlConfig,
@@ -3157,14 +4037,14 @@ function init(cfg) {
3157
4037
  const configuredLogRecordProcessors = mergedConfig.logRecordProcessors && mergedConfig.logRecordProcessors.length > 0 ? mergedConfig.logRecordProcessors : mergedConfig.logRecordProcessor ? [mergedConfig.logRecordProcessor] : void 0;
3158
4038
  let spanProcessors = [];
3159
4039
  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)));
4040
+ else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(exporter)));
3161
4041
  else for (const destination of otlpDestinations) {
3162
4042
  if (!destinationSupportsSignal(destination, "traces")) continue;
3163
4043
  const traceExporter = createTraceExporter(destination.protocol, {
3164
4044
  url: formatEndpointUrl(destination.endpoint, "traces", destination.protocol),
3165
4045
  headers: destination.headers
3166
4046
  });
3167
- spanProcessors.push(new require_tail_sampling_processor.TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(traceExporter)));
4047
+ spanProcessors.push(new TailSamplingSpanProcessor(new _opentelemetry_sdk_trace_base.BatchSpanProcessor(traceExporter)));
3168
4048
  }
3169
4049
  if (mergedConfig.baggage || mergedConfig.baggage === "") {
3170
4050
  const prefix = typeof mergedConfig.baggage === "string" ? mergedConfig.baggage ? `${mergedConfig.baggage}.` : "" : "baggage.";
@@ -3255,9 +4135,9 @@ function init(cfg) {
3255
4135
  logger.warn({}, `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`);
3256
4136
  }
3257
4137
  }
3258
- const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? require_sampling.resolveSamplingPreset(mergedConfig.sampling) : void 0);
4138
+ const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? resolveSamplingPreset(mergedConfig.sampling) : void 0);
3259
4139
  if (autotelSampler) mergedConfig.sampler = autotelSampler;
3260
- const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(require_sampling.samplingPresets.production());
4140
+ const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(samplingPresets.production());
3261
4141
  const sdkOptions = {
3262
4142
  resource,
3263
4143
  serviceName: mergedConfig.service,
@@ -3352,6 +4232,42 @@ function getSdk() {
3352
4232
  }
3353
4233
 
3354
4234
  //#endregion
4235
+ Object.defineProperty(exports, 'AUTOTEL_DEBUG_BAGGAGE_KEY', {
4236
+ enumerable: true,
4237
+ get: function () {
4238
+ return AUTOTEL_DEBUG_BAGGAGE_KEY;
4239
+ }
4240
+ });
4241
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_RATE', {
4242
+ enumerable: true,
4243
+ get: function () {
4244
+ return AUTOTEL_SAMPLING_RATE;
4245
+ }
4246
+ });
4247
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_EVALUATED', {
4248
+ enumerable: true,
4249
+ get: function () {
4250
+ return AUTOTEL_SAMPLING_TAIL_EVALUATED;
4251
+ }
4252
+ });
4253
+ Object.defineProperty(exports, 'AUTOTEL_SAMPLING_TAIL_KEEP', {
4254
+ enumerable: true,
4255
+ get: function () {
4256
+ return AUTOTEL_SAMPLING_TAIL_KEEP;
4257
+ }
4258
+ });
4259
+ Object.defineProperty(exports, 'AdaptiveSampler', {
4260
+ enumerable: true,
4261
+ get: function () {
4262
+ return AdaptiveSampler;
4263
+ }
4264
+ });
4265
+ Object.defineProperty(exports, 'AlwaysSampler', {
4266
+ enumerable: true,
4267
+ get: function () {
4268
+ return AlwaysSampler;
4269
+ }
4270
+ });
3355
4271
  Object.defineProperty(exports, 'BaggageSpanProcessor', {
3356
4272
  enumerable: true,
3357
4273
  get: function () {
@@ -3364,12 +4280,66 @@ Object.defineProperty(exports, 'CORRELATION_ID_BAGGAGE_KEY', {
3364
4280
  return CORRELATION_ID_BAGGAGE_KEY;
3365
4281
  }
3366
4282
  });
4283
+ Object.defineProperty(exports, 'CompositeSampler', {
4284
+ enumerable: true,
4285
+ get: function () {
4286
+ return CompositeSampler;
4287
+ }
4288
+ });
4289
+ Object.defineProperty(exports, 'DeterministicSampler', {
4290
+ enumerable: true,
4291
+ get: function () {
4292
+ return DeterministicSampler;
4293
+ }
4294
+ });
3367
4295
  Object.defineProperty(exports, 'Event', {
3368
4296
  enumerable: true,
3369
4297
  get: function () {
3370
4298
  return Event;
3371
4299
  }
3372
4300
  });
4301
+ Object.defineProperty(exports, 'FeatureFlagSampler', {
4302
+ enumerable: true,
4303
+ get: function () {
4304
+ return FeatureFlagSampler;
4305
+ }
4306
+ });
4307
+ Object.defineProperty(exports, 'KeyTargetRateSampler', {
4308
+ enumerable: true,
4309
+ get: function () {
4310
+ return KeyTargetRateSampler;
4311
+ }
4312
+ });
4313
+ Object.defineProperty(exports, 'NeverSampler', {
4314
+ enumerable: true,
4315
+ get: function () {
4316
+ return NeverSampler;
4317
+ }
4318
+ });
4319
+ Object.defineProperty(exports, 'RandomSampler', {
4320
+ enumerable: true,
4321
+ get: function () {
4322
+ return RandomSampler;
4323
+ }
4324
+ });
4325
+ Object.defineProperty(exports, 'TailSamplingSpanProcessor', {
4326
+ enumerable: true,
4327
+ get: function () {
4328
+ return TailSamplingSpanProcessor;
4329
+ }
4330
+ });
4331
+ Object.defineProperty(exports, 'UserIdSampler', {
4332
+ enumerable: true,
4333
+ get: function () {
4334
+ return UserIdSampler;
4335
+ }
4336
+ });
4337
+ Object.defineProperty(exports, 'createLinkFromHeaders', {
4338
+ enumerable: true,
4339
+ get: function () {
4340
+ return createLinkFromHeaders;
4341
+ }
4342
+ });
3373
4343
  Object.defineProperty(exports, 'createStringRedactor', {
3374
4344
  enumerable: true,
3375
4345
  get: function () {
@@ -3382,12 +4352,24 @@ Object.defineProperty(exports, 'createTraceContext', {
3382
4352
  return createTraceContext;
3383
4353
  }
3384
4354
  });
4355
+ Object.defineProperty(exports, 'debugCaptureRequested', {
4356
+ enumerable: true,
4357
+ get: function () {
4358
+ return debugCaptureRequested;
4359
+ }
4360
+ });
3385
4361
  Object.defineProperty(exports, 'defineBaggageSchema', {
3386
4362
  enumerable: true,
3387
4363
  get: function () {
3388
4364
  return defineBaggageSchema;
3389
4365
  }
3390
4366
  });
4367
+ Object.defineProperty(exports, 'extractLinksFromBatch', {
4368
+ enumerable: true,
4369
+ get: function () {
4370
+ return extractLinksFromBatch;
4371
+ }
4372
+ });
3391
4373
  Object.defineProperty(exports, 'flush', {
3392
4374
  enumerable: true,
3393
4375
  get: function () {
@@ -3472,12 +4454,30 @@ Object.defineProperty(exports, 'hasExplicitSpanStatus', {
3472
4454
  return hasExplicitSpanStatus;
3473
4455
  }
3474
4456
  });
4457
+ Object.defineProperty(exports, 'hasYamlConfig', {
4458
+ enumerable: true,
4459
+ get: function () {
4460
+ return hasYamlConfig;
4461
+ }
4462
+ });
4463
+ Object.defineProperty(exports, 'hashUnitInterval', {
4464
+ enumerable: true,
4465
+ get: function () {
4466
+ return hashUnitInterval;
4467
+ }
4468
+ });
3475
4469
  Object.defineProperty(exports, 'init', {
3476
4470
  enumerable: true,
3477
4471
  get: function () {
3478
4472
  return init;
3479
4473
  }
3480
4474
  });
4475
+ Object.defineProperty(exports, 'isForceKept', {
4476
+ enumerable: true,
4477
+ get: function () {
4478
+ return isForceKept;
4479
+ }
4480
+ });
3481
4481
  Object.defineProperty(exports, 'isInitialized', {
3482
4482
  enumerable: true,
3483
4483
  get: function () {
@@ -3490,18 +4490,42 @@ Object.defineProperty(exports, 'isLoggerLocked', {
3490
4490
  return isLoggerLocked;
3491
4491
  }
3492
4492
  });
4493
+ Object.defineProperty(exports, 'loadYamlConfig', {
4494
+ enumerable: true,
4495
+ get: function () {
4496
+ return loadYamlConfig;
4497
+ }
4498
+ });
4499
+ Object.defineProperty(exports, 'loadYamlConfigFromFile', {
4500
+ enumerable: true,
4501
+ get: function () {
4502
+ return loadYamlConfigFromFile;
4503
+ }
4504
+ });
3493
4505
  Object.defineProperty(exports, 'lockLogger', {
3494
4506
  enumerable: true,
3495
4507
  get: function () {
3496
4508
  return lockLogger;
3497
4509
  }
3498
4510
  });
4511
+ Object.defineProperty(exports, 'markForceKept', {
4512
+ enumerable: true,
4513
+ get: function () {
4514
+ return markForceKept;
4515
+ }
4516
+ });
3499
4517
  Object.defineProperty(exports, 'resetEvents', {
3500
4518
  enumerable: true,
3501
4519
  get: function () {
3502
4520
  return resetEvents;
3503
4521
  }
3504
4522
  });
4523
+ Object.defineProperty(exports, 'resolveSamplingPreset', {
4524
+ enumerable: true,
4525
+ get: function () {
4526
+ return resolveSamplingPreset;
4527
+ }
4528
+ });
3505
4529
  Object.defineProperty(exports, 'runInOperationContext', {
3506
4530
  enumerable: true,
3507
4531
  get: function () {
@@ -3514,6 +4538,12 @@ Object.defineProperty(exports, 'runWithCorrelationId', {
3514
4538
  return runWithCorrelationId;
3515
4539
  }
3516
4540
  });
4541
+ Object.defineProperty(exports, 'samplingPresets', {
4542
+ enumerable: true,
4543
+ get: function () {
4544
+ return samplingPresets;
4545
+ }
4546
+ });
3517
4547
  Object.defineProperty(exports, 'setCorrelationId', {
3518
4548
  enumerable: true,
3519
4549
  get: function () {