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