autotel 5.0.0 → 6.1.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 (94) hide show
  1. package/README.md +14 -1
  2. package/dist/auto.cjs +1 -1
  3. package/dist/auto.js +1 -1
  4. package/dist/correlation-id.cjs +9 -9
  5. package/dist/correlation-id.js +1 -1
  6. package/dist/decorators.cjs +2 -2
  7. package/dist/decorators.cjs.map +1 -1
  8. package/dist/decorators.js +1 -1
  9. package/dist/drain-pipeline.cjs +10 -1
  10. package/dist/drain-pipeline.cjs.map +1 -1
  11. package/dist/drain-pipeline.d.cts +5 -0
  12. package/dist/drain-pipeline.d.cts.map +1 -1
  13. package/dist/drain-pipeline.d.ts +5 -0
  14. package/dist/drain-pipeline.d.ts.map +1 -1
  15. package/dist/drain-pipeline.js +10 -1
  16. package/dist/drain-pipeline.js.map +1 -1
  17. package/dist/event.cjs +4 -4
  18. package/dist/event.js +1 -1
  19. package/dist/{functional-Bit6noRu.js → functional-BnDUxnSl.js} +2 -4
  20. package/dist/{functional-Bit6noRu.js.map → functional-BnDUxnSl.js.map} +1 -1
  21. package/dist/{functional-Cj7PSItS.cjs → functional-DcLBuCPz.cjs} +12 -14
  22. package/dist/{functional-Cj7PSItS.cjs.map → functional-DcLBuCPz.cjs.map} +1 -1
  23. package/dist/functional.cjs +1 -1
  24. package/dist/functional.js +1 -1
  25. package/dist/http.cjs +2 -2
  26. package/dist/http.cjs.map +1 -1
  27. package/dist/http.js +1 -1
  28. package/dist/index.cjs +33 -166
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +5 -2
  31. package/dist/index.d.cts.map +1 -1
  32. package/dist/index.d.ts +5 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +14 -147
  35. package/dist/index.js.map +1 -1
  36. package/dist/init-C3RApHKJ.cjs +3371 -0
  37. package/dist/init-C3RApHKJ.cjs.map +1 -0
  38. package/dist/{init-CTyqG9qR.d.ts → init-D6n2diBy.d.ts} +21 -2
  39. package/dist/init-D6n2diBy.d.ts.map +1 -0
  40. package/dist/init-R-gSEXkJ.js +3190 -0
  41. package/dist/init-R-gSEXkJ.js.map +1 -0
  42. package/dist/{init-a4yPDYqC.d.cts → init-XzXzrarP.d.cts} +21 -2
  43. package/dist/init-XzXzrarP.d.cts.map +1 -0
  44. package/dist/instrumentation.cjs +1 -1
  45. package/dist/instrumentation.js +1 -1
  46. package/dist/messaging.cjs +1 -1
  47. package/dist/messaging.js +1 -1
  48. package/dist/semantic-helpers.cjs +1 -1
  49. package/dist/semantic-helpers.js +1 -1
  50. package/dist/webhook.cjs +1 -1
  51. package/dist/webhook.js +1 -1
  52. package/dist/workflow-distributed.cjs +1 -1
  53. package/dist/workflow-distributed.js +1 -1
  54. package/dist/workflow.cjs +1 -1
  55. package/dist/workflow.js +1 -1
  56. package/dist/yaml-config.d.cts +1 -1
  57. package/dist/yaml-config.d.ts +1 -1
  58. package/package.json +2 -3
  59. package/dist/event-CwIpFdoY.cjs +0 -778
  60. package/dist/event-CwIpFdoY.cjs.map +0 -1
  61. package/dist/event-pLMF5b3d.js +0 -761
  62. package/dist/event-pLMF5b3d.js.map +0 -1
  63. package/dist/init-B2uvZReD.cjs +0 -1120
  64. package/dist/init-B2uvZReD.cjs.map +0 -1
  65. package/dist/init-CTyqG9qR.d.ts.map +0 -1
  66. package/dist/init-CdEWjhvl.js +0 -1043
  67. package/dist/init-CdEWjhvl.js.map +0 -1
  68. package/dist/init-a4yPDYqC.d.cts.map +0 -1
  69. package/dist/operation-context-CKBoA4Qy.js +0 -59
  70. package/dist/operation-context-CKBoA4Qy.js.map +0 -1
  71. package/dist/operation-context-hzKbTKnt.cjs +0 -72
  72. package/dist/operation-context-hzKbTKnt.cjs.map +0 -1
  73. package/dist/track-BBcxEJEC.js +0 -1110
  74. package/dist/track-BBcxEJEC.js.map +0 -1
  75. package/dist/track-BI6dmZ2X.cjs +0 -1207
  76. package/dist/track-BI6dmZ2X.cjs.map +0 -1
  77. package/skills/analyze-traces/SKILL.md +0 -209
  78. package/skills/autotel-core/SKILL.md +0 -166
  79. package/skills/autotel-events/SKILL.md +0 -84
  80. package/skills/autotel-frameworks/SKILL.md +0 -125
  81. package/skills/autotel-instrumentation/SKILL.md +0 -150
  82. package/skills/autotel-request-logging/SKILL.md +0 -99
  83. package/skills/autotel-structured-errors/SKILL.md +0 -125
  84. package/skills/build-audit-trails/SKILL.md +0 -460
  85. package/skills/build-audit-trails/references/audit-queries.md +0 -73
  86. package/skills/build-audit-trails/references/framework-wiring.md +0 -196
  87. package/skills/debug-missing-spans/SKILL.md +0 -250
  88. package/skills/migrate-to-autotel/SKILL.md +0 -269
  89. package/skills/review-otel-patterns/SKILL.md +0 -540
  90. package/skills/review-otel-patterns/references/code-review.md +0 -76
  91. package/skills/review-otel-patterns/references/processor-pipeline.md +0 -205
  92. package/skills/review-otel-patterns/references/structured-errors.md +0 -102
  93. package/skills/review-otel-patterns/references/wide-spans.md +0 -85
  94. package/skills/tune-sampling/SKILL.md +0 -290
@@ -0,0 +1,3190 @@
1
+ import { getForceFlushableProvider } from "./tracer-provider.js";
2
+ import { getConfig as getConfig$1 } from "./config.js";
3
+ import { AUTOTEL_SAMPLING_RATE, resolveSamplingPreset, samplingPresets } from "./sampling.js";
4
+ import { n as safeRequire, t as requireModule } from "./node-require-vROmTeJ8.js";
5
+ import { TailSamplingSpanProcessor } from "./tail-sampling-processor.js";
6
+ import { FilteringSpanProcessor } from "./filtering-span-processor.js";
7
+ import { SpanNameNormalizingProcessor } from "./span-name-normalizer.js";
8
+ import { AttributeRedactingProcessor, REDACTOR_PRESETS, normalizeAttributeRedactorConfig } from "./attribute-redacting-processor.js";
9
+ import { t as PrettyConsoleExporter } from "./pretty-console-exporter-DqKl_q9z.js";
10
+ import { loadYamlConfig } from "./yaml-config.js";
11
+ import { t as CanonicalLogLineProcessor } from "./canonical-log-line-processor-mKABvGP0.js";
12
+ import { r as recordStructuredError } from "./structured-error-9--cxBay.js";
13
+ import { resetMetrics } from "./metric.js";
14
+ import { NodeSDK } from "@opentelemetry/sdk-node";
15
+ import { AlwaysOffSampler, AlwaysOnSampler, BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, SamplingDecision, SimpleSpanProcessor, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
16
+ import { resourceFromAttributes } from "@opentelemetry/resources";
17
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
18
+ import { context, propagation, trace } from "@opentelemetry/api";
19
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
20
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
21
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
22
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
23
+ import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
24
+ import { constants } from "node:os";
25
+ import * as nodeAsyncHooks from "node:async_hooks";
26
+
27
+ //#region src/posthog-logs.ts
28
+ var RedactingLogRecordProcessor = class {
29
+ wrapped;
30
+ redact;
31
+ constructor(wrapped, redact) {
32
+ this.wrapped = wrapped;
33
+ this.redact = redact;
34
+ }
35
+ onEmit(logRecord, context) {
36
+ if (logRecord.body && typeof logRecord.body === "string") logRecord.body = this.redact(logRecord.body);
37
+ if (logRecord.attributes) {
38
+ for (const [key, value] of Object.entries(logRecord.attributes)) if (typeof value === "string") logRecord.attributes[key] = this.redact(value);
39
+ else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => typeof item === "string" ? this.redact(item) : item);
40
+ }
41
+ this.wrapped.onEmit(logRecord, context);
42
+ }
43
+ shutdown() {
44
+ return this.wrapped.shutdown();
45
+ }
46
+ forceFlush() {
47
+ return this.wrapped.forceFlush();
48
+ }
49
+ };
50
+ /**
51
+ * Build log record processors for PostHog OTLP logs integration.
52
+ *
53
+ * Resolution order:
54
+ * 1. config.url if provided
55
+ * 2. POSTHOG_LOGS_URL env var
56
+ * 3. Empty array (disabled)
57
+ */
58
+ function buildPostHogLogProcessors(config, stringRedactor) {
59
+ const url = config?.url || process.env.POSTHOG_LOGS_URL;
60
+ if (!url) return [];
61
+ const sdkLogs = safeRequire("@opentelemetry/sdk-logs");
62
+ const exporterModule = safeRequire("@opentelemetry/exporter-logs-otlp-http");
63
+ if (!sdkLogs || !exporterModule) return [];
64
+ const exporter = new exporterModule.OTLPLogExporter({ url });
65
+ let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
66
+ if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
67
+ return [processor];
68
+ }
69
+
70
+ //#endregion
71
+ //#region src/baggage-span-processor.ts
72
+ /**
73
+ * Span processor that automatically copies baggage entries to span attributes
74
+ *
75
+ * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
76
+ * without manually calling ctx.setAttribute() for each baggage entry.
77
+ *
78
+ * @example Enable in init()
79
+ * ```typescript
80
+ * init({
81
+ * service: 'my-app',
82
+ * baggage: true // Uses default 'baggage.' prefix
83
+ * });
84
+ *
85
+ * // Now baggage automatically appears as span attributes
86
+ * await withBaggage({
87
+ * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
88
+ * fn: async () => {
89
+ * // Span has baggage.tenant.id and baggage.user.id attributes!
90
+ * }
91
+ * });
92
+ * ```
93
+ *
94
+ * @example Custom prefix
95
+ * ```typescript
96
+ * init({
97
+ * service: 'my-app',
98
+ * baggage: 'ctx' // Uses 'ctx.' prefix
99
+ * });
100
+ * // Creates attributes: ctx.tenant.id, ctx.user.id
101
+ * ```
102
+ */
103
+ var BaggageSpanProcessor = class {
104
+ prefix;
105
+ constructor(options = {}) {
106
+ this.prefix = options.prefix ?? "baggage.";
107
+ }
108
+ onStart(span, parentContext) {
109
+ let baggage = propagation.getBaggage(parentContext);
110
+ if (!baggage) baggage = propagation.getBaggage(context.active());
111
+ if (!baggage) try {
112
+ const { getActiveContextWithBaggage } = requireModule("./trace-context");
113
+ const storedContext = getActiveContextWithBaggage();
114
+ baggage = propagation.getBaggage(storedContext);
115
+ } catch {}
116
+ if (!baggage) return;
117
+ for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
118
+ }
119
+ onEnd(_span) {}
120
+ async shutdown() {}
121
+ async forceFlush() {}
122
+ };
123
+
124
+ //#endregion
125
+ //#region src/redact-values.ts
126
+ /** Standalone string redaction for use outside the span processor pipeline. */
127
+ function createStringRedactor(config) {
128
+ const resolved = typeof config === "string" ? REDACTOR_PRESETS[config] : config;
129
+ const valuePatterns = resolved.valuePatterns ?? [];
130
+ const defaultReplacement = resolved.replacement ?? "[REDACTED]";
131
+ return (value) => {
132
+ let result = value;
133
+ for (const { pattern, replacement, mask } of valuePatterns) {
134
+ pattern.lastIndex = 0;
135
+ result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
136
+ }
137
+ return result;
138
+ };
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/env-config.ts
143
+ /**
144
+ * Validate URL format
145
+ */
146
+ function isValidUrl(urlString) {
147
+ try {
148
+ const url = new URL(urlString);
149
+ return url.protocol === "http:" || url.protocol === "https:";
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+ /**
155
+ * Resolve OpenTelemetry environment variables from process.env
156
+ */
157
+ function resolveOtelEnv() {
158
+ const env = {};
159
+ if (process.env.OTEL_SERVICE_NAME) {
160
+ const value = process.env.OTEL_SERVICE_NAME.trim();
161
+ if (value) env.OTEL_SERVICE_NAME = value;
162
+ }
163
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
164
+ const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
165
+ if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
166
+ }
167
+ if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
168
+ const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
169
+ if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
170
+ }
171
+ if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
172
+ const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
173
+ if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
174
+ }
175
+ if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
176
+ const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
177
+ if (value === "http" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
178
+ }
179
+ if (process.env.OTEL_TRACES_SAMPLER) {
180
+ const value = process.env.OTEL_TRACES_SAMPLER.trim();
181
+ if (value) env.OTEL_TRACES_SAMPLER = value;
182
+ }
183
+ if (process.env.OTEL_TRACES_SAMPLER_ARG) {
184
+ const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
185
+ if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
186
+ }
187
+ return env;
188
+ }
189
+ function parseRatioSamplerArg(samplerName, samplerArg) {
190
+ if (samplerArg === void 0) return 1;
191
+ const ratio = Number(samplerArg);
192
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
193
+ console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
194
+ return 1;
195
+ }
196
+ return ratio;
197
+ }
198
+ function warnOnUnusedSamplerArg(samplerName, samplerArg) {
199
+ if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
200
+ }
201
+ function createSamplerFromEnv(env) {
202
+ const samplerName = env.OTEL_TRACES_SAMPLER;
203
+ if (!samplerName) return;
204
+ switch (samplerName) {
205
+ case "always_on":
206
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
207
+ return new AlwaysOnSampler();
208
+ case "always_off":
209
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
210
+ return new AlwaysOffSampler();
211
+ case "traceidratio": return new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
212
+ case "parentbased_always_on":
213
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
214
+ return new ParentBasedSampler({ root: new AlwaysOnSampler() });
215
+ case "parentbased_always_off":
216
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
217
+ return new ParentBasedSampler({ root: new AlwaysOffSampler() });
218
+ case "parentbased_traceidratio": return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
219
+ case "jaeger_remote":
220
+ case "parentbased_jaeger_remote":
221
+ case "xray":
222
+ console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
223
+ return;
224
+ default:
225
+ console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
226
+ return;
227
+ }
228
+ }
229
+ /**
230
+ * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
231
+ * Example: "service.version=1.0.0,deployment.environment=production"
232
+ */
233
+ function parseResourceAttributes(input) {
234
+ if (!input || input.trim() === "") return {};
235
+ const attributes = {};
236
+ const pairs = input.split(",");
237
+ for (const pair of pairs) {
238
+ const trimmedPair = pair.trim();
239
+ if (!trimmedPair) continue;
240
+ const equalIndex = trimmedPair.indexOf("=");
241
+ if (equalIndex === -1) continue;
242
+ const key = trimmedPair.slice(0, equalIndex).trim();
243
+ const value = trimmedPair.slice(equalIndex + 1).trim();
244
+ if (key && value) attributes[key] = value;
245
+ }
246
+ return attributes;
247
+ }
248
+ /**
249
+ * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
250
+ * Example: "api-key=secret123,x-custom-header=value"
251
+ */
252
+ function parseOtlpHeaders(input) {
253
+ if (!input || input.trim() === "") return {};
254
+ const headers = {};
255
+ const pairs = input.split(",");
256
+ for (const pair of pairs) {
257
+ const trimmedPair = pair.trim();
258
+ if (!trimmedPair) continue;
259
+ const equalIndex = trimmedPair.indexOf("=");
260
+ if (equalIndex === -1) continue;
261
+ const key = trimmedPair.slice(0, equalIndex).trim();
262
+ const value = trimmedPair.slice(equalIndex + 1).trim();
263
+ if (key && value) headers[key] = value;
264
+ }
265
+ return headers;
266
+ }
267
+ /**
268
+ * Convert resolved environment variables to config
269
+ */
270
+ function envToConfig(env) {
271
+ const config = {};
272
+ if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
273
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
274
+ if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
275
+ if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
276
+ const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
277
+ if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
278
+ const sampler = createSamplerFromEnv(env);
279
+ if (sampler) config.otelSampler = sampler;
280
+ return config;
281
+ }
282
+ /**
283
+ * Main function to resolve config from environment variables
284
+ */
285
+ function resolveConfigFromEnv() {
286
+ return envToConfig(resolveOtelEnv());
287
+ }
288
+
289
+ //#endregion
290
+ //#region src/devtools.ts
291
+ const defaultHost = "127.0.0.1";
292
+ const defaultPort = 4318;
293
+ function resolveDevtoolsConfig(config) {
294
+ if (!config) return {
295
+ enabled: false,
296
+ endpoint: void 0,
297
+ embedded: false,
298
+ host: defaultHost,
299
+ port: defaultPort,
300
+ verbose: false
301
+ };
302
+ if (config === true) return {
303
+ enabled: true,
304
+ endpoint: `http://${defaultHost}:${defaultPort}`,
305
+ embedded: false,
306
+ host: defaultHost,
307
+ port: defaultPort,
308
+ verbose: false
309
+ };
310
+ const enabled = config.enabled ?? true;
311
+ const host = config.host ?? defaultHost;
312
+ const port = config.port ?? defaultPort;
313
+ const endpoint = config.endpoint ?? `http://${host}:${port}`;
314
+ return {
315
+ enabled,
316
+ endpoint: enabled ? endpoint : void 0,
317
+ embedded: enabled && (config.embedded ?? false),
318
+ host,
319
+ port,
320
+ verbose: config.verbose ?? false
321
+ };
322
+ }
323
+
324
+ //#endregion
325
+ //#region src/process-handlers.ts
326
+ let removeOwnedHandlers = [];
327
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
328
+ function signalExitCode(signal) {
329
+ return 128 + constants.signals[signal];
330
+ }
331
+ /**
332
+ * Surface a fatal error before shutting down.
333
+ *
334
+ * Registering an `uncaughtException` / `unhandledRejection` listener overrides
335
+ * Node's default of printing the stack to stderr, so without this a crash under
336
+ * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
337
+ * so we print to stderr directly to guarantee the crash stays visible.
338
+ */
339
+ function reportFatalError(error, event) {
340
+ const err = error instanceof Error ? error : new Error(String(error));
341
+ console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
342
+ }
343
+ function installProcessHandlers(config, shutdown) {
344
+ uninstallProcessHandlers();
345
+ let shutdownInFlight;
346
+ const runShutdownOnce = () => {
347
+ if (shutdownInFlight) return shutdownInFlight;
348
+ const timeoutMs = config.shutdownTimeoutMs ?? 2e3;
349
+ let timeoutHandle;
350
+ const shutdownAttempt = Promise.resolve().then(shutdown).catch(() => void 0);
351
+ const timeout = new Promise((resolve) => {
352
+ timeoutHandle = setTimeout(resolve, timeoutMs);
353
+ timeoutHandle.unref();
354
+ });
355
+ shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
356
+ if (timeoutHandle) clearTimeout(timeoutHandle);
357
+ });
358
+ return shutdownInFlight;
359
+ };
360
+ let exitScheduled = false;
361
+ let resolvedExitCode = 0;
362
+ let fatalLatched = false;
363
+ const shutdownAndExit = (exitCode, fatal) => {
364
+ if (fatal) {
365
+ if (!fatalLatched) {
366
+ resolvedExitCode = exitCode;
367
+ fatalLatched = true;
368
+ }
369
+ } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
370
+ if (exitScheduled) return;
371
+ exitScheduled = true;
372
+ runShutdownOnce().then(() => {
373
+ process.exit(resolvedExitCode);
374
+ });
375
+ };
376
+ for (const signal of config.signals ?? DEFAULT_SIGNALS) {
377
+ const listener = () => {
378
+ shutdownAndExit(signalExitCode(signal), false);
379
+ };
380
+ process.on(signal, listener);
381
+ removeOwnedHandlers.push(() => {
382
+ process.removeListener(signal, listener);
383
+ });
384
+ }
385
+ if (config.fatalErrors ?? true) {
386
+ const uncaughtExceptionListener = (error) => {
387
+ reportFatalError(error, "uncaughtException");
388
+ shutdownAndExit(1, true);
389
+ };
390
+ const unhandledRejectionListener = (reason) => {
391
+ reportFatalError(reason, "unhandledRejection");
392
+ shutdownAndExit(1, true);
393
+ };
394
+ process.on("uncaughtException", uncaughtExceptionListener);
395
+ process.on("unhandledRejection", unhandledRejectionListener);
396
+ removeOwnedHandlers.push(() => {
397
+ process.removeListener("uncaughtException", uncaughtExceptionListener);
398
+ }, () => {
399
+ process.removeListener("unhandledRejection", unhandledRejectionListener);
400
+ });
401
+ }
402
+ }
403
+ function uninstallProcessHandlers() {
404
+ for (const removeHandler of removeOwnedHandlers) removeHandler();
405
+ removeOwnedHandlers = [];
406
+ }
407
+
408
+ //#endregion
409
+ //#region src/rate-limiter.ts
410
+ /**
411
+ * Token bucket rate limiter
412
+ *
413
+ * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
414
+ * Thread-safe for async operations.
415
+ */
416
+ var TokenBucketRateLimiter = class {
417
+ tokens;
418
+ maxTokens;
419
+ refillRate;
420
+ lastRefill;
421
+ constructor(config) {
422
+ this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
423
+ this.tokens = this.maxTokens;
424
+ this.refillRate = config.maxEventsPerSecond / 1e3;
425
+ this.lastRefill = Date.now();
426
+ }
427
+ /**
428
+ * Try to consume a token (allow an event)
429
+ * Returns true if allowed, false if rate limit exceeded
430
+ */
431
+ tryConsume(count = 1) {
432
+ this.refill();
433
+ if (this.tokens >= count) {
434
+ this.tokens -= count;
435
+ return true;
436
+ }
437
+ return false;
438
+ }
439
+ /**
440
+ * Wait until a token is available (async rate limiting)
441
+ * Returns a promise that resolves when the event can be processed
442
+ */
443
+ async waitForToken(count = 1) {
444
+ this.refill();
445
+ if (this.tokens >= count) {
446
+ this.tokens -= count;
447
+ return;
448
+ }
449
+ const tokensNeeded = count - this.tokens;
450
+ const waitMs = Math.ceil(tokensNeeded / this.refillRate);
451
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
452
+ return this.waitForToken(count);
453
+ }
454
+ /**
455
+ * Refill tokens based on elapsed time
456
+ */
457
+ refill() {
458
+ const now = Date.now();
459
+ const tokensToAdd = (now - this.lastRefill) * this.refillRate;
460
+ this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
461
+ this.lastRefill = now;
462
+ }
463
+ /**
464
+ * Get current available tokens (for testing/debugging)
465
+ */
466
+ getAvailableTokens() {
467
+ this.refill();
468
+ return Math.floor(this.tokens);
469
+ }
470
+ /**
471
+ * Reset the rate limiter (for testing)
472
+ */
473
+ reset() {
474
+ this.tokens = this.maxTokens;
475
+ this.lastRefill = Date.now();
476
+ }
477
+ };
478
+
479
+ //#endregion
480
+ //#region src/trace-context.ts
481
+ /**
482
+ * AsyncLocalStorage for storing the active context with baggage
483
+ * This allows setters to update the context and have it persist
484
+ */
485
+ const contextStorage = new nodeAsyncHooks.AsyncLocalStorage();
486
+ /**
487
+ * Get the context storage instance (for initialization in functional.ts)
488
+ */
489
+ function getContextStorage() {
490
+ return contextStorage;
491
+ }
492
+ /**
493
+ * Get the active context, checking our stored context first
494
+ * This ensures baggage setters work with OpenTelemetry's propagation
495
+ */
496
+ function getActiveContextWithBaggage() {
497
+ return contextStorage.getStore()?.value ?? context.active();
498
+ }
499
+ /**
500
+ * Set a value in AsyncLocalStorage, preferring enterWith() when available
501
+ * (Node.js) and falling back to run() for environments that only support
502
+ * run() (e.g. Cloudflare Workers).
503
+ *
504
+ * On runtimes without enterWith() we mutate the existing run() scope when one
505
+ * exists. This is what allows baggage/correlation updates to remain visible
506
+ * for the rest of the traced callback in Workers.
507
+ */
508
+ function enterOrRun(storage, value) {
509
+ const existingStore = storage.getStore();
510
+ if (existingStore) {
511
+ existingStore.value = value;
512
+ return;
513
+ }
514
+ const boxedValue = { value };
515
+ try {
516
+ storage.enterWith(boxedValue);
517
+ } catch {
518
+ storage.run(boxedValue, () => {});
519
+ }
520
+ }
521
+ function updateActiveContext(newContext) {
522
+ enterOrRun(contextStorage, newContext);
523
+ const manager = context._getContextManager?.();
524
+ if (!manager) return;
525
+ const asyncLocal = manager._asyncLocalStorage ?? void 0;
526
+ if (asyncLocal?.enterWith) {
527
+ asyncLocal.enterWith(newContext);
528
+ return;
529
+ }
530
+ if (typeof manager.with === "function") manager.with(newContext, () => {});
531
+ }
532
+ /**
533
+ * Create a TraceContext from an OpenTelemetry Span
534
+ *
535
+ * This utility extracts trace context information from a span
536
+ * and provides span manipulation methods and baggage operations in a consistent format.
537
+ *
538
+ * Note: Baggage methods always operate on the currently active context,
539
+ * which may differ from the context when createTraceContext was called.
540
+ */
541
+ function createTraceContext(span) {
542
+ const spanContext = span.spanContext();
543
+ if (!contextStorage.getStore()?.value) {
544
+ const activeContext = context.active();
545
+ enterOrRun(contextStorage, activeContext);
546
+ }
547
+ const traceCtx = {
548
+ traceId: spanContext.traceId,
549
+ spanId: spanContext.spanId,
550
+ correlationId: spanContext.traceId.slice(0, 16),
551
+ setAttribute: span.setAttribute.bind(span),
552
+ setAttributes: span.setAttributes.bind(span),
553
+ setStatus: span.setStatus.bind(span),
554
+ recordException: span.recordException.bind(span),
555
+ addEvent: span.addEvent.bind(span),
556
+ addLink: span.addLink.bind(span),
557
+ addLinks: span.addLinks.bind(span),
558
+ updateName: span.updateName.bind(span),
559
+ isRecording: span.isRecording.bind(span),
560
+ recordError: (error) => {
561
+ const err = error instanceof Error ? error : new Error(String(error));
562
+ recordStructuredError(traceCtx, err);
563
+ },
564
+ track: (event, data) => {
565
+ track(event, data);
566
+ },
567
+ getBaggage(key) {
568
+ const activeCtx = context.active();
569
+ let baggage = propagation.getBaggage(activeCtx);
570
+ if (!baggage) {
571
+ const storedContext = contextStorage.getStore()?.value;
572
+ if (storedContext) baggage = propagation.getBaggage(storedContext);
573
+ }
574
+ return baggage?.getEntry(key)?.value;
575
+ },
576
+ setBaggage(key, value) {
577
+ const activeCtx = context.active();
578
+ const currentContext = contextStorage.getStore()?.value ?? activeCtx;
579
+ const updated = (propagation.getBaggage(currentContext) ?? propagation.createBaggage()).setEntry(key, { value });
580
+ updateActiveContext(propagation.setBaggage(currentContext, updated));
581
+ return value;
582
+ },
583
+ deleteBaggage(key) {
584
+ const activeCtx = context.active();
585
+ const currentContext = contextStorage.getStore()?.value ?? activeCtx;
586
+ const baggage = propagation.getBaggage(currentContext);
587
+ if (baggage) {
588
+ const updated = baggage.removeEntry(key);
589
+ updateActiveContext(propagation.setBaggage(currentContext, updated));
590
+ }
591
+ },
592
+ getAllBaggage() {
593
+ const activeCtx = context.active();
594
+ let baggage = propagation.getBaggage(activeCtx);
595
+ if (!baggage) {
596
+ const storedContext = contextStorage.getStore()?.value;
597
+ if (storedContext) baggage = propagation.getBaggage(storedContext);
598
+ }
599
+ if (!baggage) return /* @__PURE__ */ new Map();
600
+ const entries = /* @__PURE__ */ new Map();
601
+ for (const [key, entry] of baggage.getAllEntries()) entries.set(key, entry);
602
+ return entries;
603
+ },
604
+ getTypedBaggage: ((namespace) => {
605
+ const activeCtx = context.active();
606
+ let baggage = propagation.getBaggage(activeCtx);
607
+ if (!baggage) {
608
+ const storedContext = contextStorage.getStore()?.value;
609
+ if (storedContext) baggage = propagation.getBaggage(storedContext);
610
+ }
611
+ if (!baggage) return;
612
+ const prefix = namespace ? `${namespace}.` : "";
613
+ const result = {};
614
+ for (const [key, entry] of baggage.getAllEntries()) if (namespace && key.startsWith(prefix)) {
615
+ const fieldName = key.slice(prefix.length);
616
+ result[fieldName] = entry.value;
617
+ } else if (!namespace) result[key] = entry.value;
618
+ return Object.keys(result).length > 0 ? result : void 0;
619
+ }),
620
+ setTypedBaggage: ((namespace, value) => {
621
+ const activeCtx = context.active();
622
+ const currentContext = contextStorage.getStore()?.value ?? activeCtx;
623
+ let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
624
+ const prefix = namespace ? `${namespace}.` : "";
625
+ for (const [key, val] of Object.entries(value)) if (val !== void 0) {
626
+ const baggageKey = `${prefix}${key}`;
627
+ baggage = baggage.setEntry(baggageKey, { value: String(val) });
628
+ }
629
+ updateActiveContext(propagation.setBaggage(currentContext, baggage));
630
+ })
631
+ };
632
+ return traceCtx;
633
+ }
634
+ /**
635
+ * Define a typed baggage schema for type-safe baggage operations
636
+ *
637
+ * This helper provides a type-safe API for working with baggage entries.
638
+ * The namespace parameter is optional and prefixes all keys to avoid collisions.
639
+ *
640
+ * @template T - The baggage schema type (all fields are treated as optional)
641
+ * @param namespace - Optional namespace to prefix baggage keys
642
+ *
643
+ * @example Basic usage
644
+ * ```typescript
645
+ * type TenantBaggage = { tenantId: string; region?: string };
646
+ * const tenantBaggage = defineBaggageSchema<TenantBaggage>('tenant');
647
+ *
648
+ * export const handler = trace<TenantBaggage>((ctx) => async () => {
649
+ * // Get typed baggage
650
+ * const tenant = tenantBaggage.get(ctx);
651
+ * if (tenant?.tenantId) {
652
+ * console.log('Tenant:', tenant.tenantId);
653
+ * }
654
+ *
655
+ * // Set typed baggage
656
+ * tenantBaggage.set(ctx, { tenantId: 't1', region: 'us-east-1' });
657
+ * });
658
+ * ```
659
+ *
660
+ * @example With withBaggage helper
661
+ * ```typescript
662
+ * const tenantBaggage = defineBaggageSchema<TenantBaggage>('tenant');
663
+ *
664
+ * export const handler = trace<TenantBaggage>((ctx) => async () => {
665
+ * return await tenantBaggage.with(ctx, { tenantId: 't1' }, async () => {
666
+ * // Baggage is available here and in child spans
667
+ * const tenant = tenantBaggage.get(ctx);
668
+ * });
669
+ * });
670
+ * ```
671
+ */
672
+ function defineBaggageSchema(namespace) {
673
+ return {
674
+ /**
675
+ * Get typed baggage from context
676
+ * @param ctx - Trace context
677
+ * @returns Partial baggage object or undefined if no baggage is set
678
+ */
679
+ get: (ctx) => {
680
+ if (!ctx.getTypedBaggage) return void 0;
681
+ return ctx.getTypedBaggage(namespace);
682
+ },
683
+ /**
684
+ * Set typed baggage in context
685
+ *
686
+ * Note: For proper scoping across async boundaries, use the `with` method instead
687
+ *
688
+ * @param ctx - Trace context
689
+ * @param value - Partial baggage object to set
690
+ */
691
+ set: (ctx, value) => {
692
+ if (!ctx.setTypedBaggage) return;
693
+ ctx.setTypedBaggage(namespace, value);
694
+ },
695
+ /**
696
+ * Run a function with typed baggage properly scoped
697
+ *
698
+ * This is the recommended way to set baggage as it ensures proper
699
+ * scoping across async boundaries.
700
+ *
701
+ * @param ctx - Trace context (can be omitted, will use active context)
702
+ * @param value - Partial baggage object to set
703
+ * @param fn - Function to execute with the baggage
704
+ */
705
+ with: (ctxOrValue, valueOrFn, maybeFn) => {
706
+ const value = maybeFn ? valueOrFn : ctxOrValue;
707
+ const fn = maybeFn || valueOrFn;
708
+ const prefix = namespace ? `${namespace}.` : "";
709
+ const flatBaggage = {};
710
+ for (const [key, val] of Object.entries(value)) if (val !== void 0) flatBaggage[`${prefix}${key}`] = String(val);
711
+ const currentContext = context.active();
712
+ let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
713
+ for (const [key, val] of Object.entries(flatBaggage)) baggage = baggage.setEntry(key, { value: val });
714
+ const newContext = propagation.setBaggage(currentContext, baggage);
715
+ return context.with(newContext, fn);
716
+ }
717
+ };
718
+ }
719
+
720
+ //#endregion
721
+ //#region src/correlation-id.ts
722
+ /**
723
+ * Correlation ID utilities for event-driven observability
724
+ *
725
+ * Provides a stable join key across events, logs, and spans even when traces fragment.
726
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe.
727
+ *
728
+ * Lifecycle:
729
+ * 1. Generated at boundary root (HTTP server span, message process span, cron job span)
730
+ * 2. Reused within context (nested work shares it via AsyncLocalStorage)
731
+ * 3. Propagated via baggage (optional, default OFF to avoid header bloat)
732
+ *
733
+ * @example Basic usage
734
+ * ```typescript
735
+ * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
736
+ *
737
+ * // Generate a new correlation ID
738
+ * const id = generateCorrelationId();
739
+ * // Returns: 'a1b2c3d4e5f67890'
740
+ *
741
+ * // Get current correlation ID from context
742
+ * const currentId = getCorrelationId();
743
+ * ```
744
+ */
745
+ /**
746
+ * AsyncLocalStorage for storing correlation ID
747
+ * This allows correlation IDs to persist across async boundaries
748
+ */
749
+ const correlationStorage = new nodeAsyncHooks.AsyncLocalStorage();
750
+ /**
751
+ * Baggage key for correlation ID propagation
752
+ */
753
+ const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
754
+ /**
755
+ * Generate a new correlation ID
756
+ *
757
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe
758
+ *
759
+ * @returns A new correlation ID
760
+ *
761
+ * @example
762
+ * ```typescript
763
+ * const id = generateCorrelationId();
764
+ * // Returns: 'a1b2c3d4e5f67890'
765
+ * ```
766
+ */
767
+ function generateCorrelationId() {
768
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
769
+ crypto.getRandomValues(bytes);
770
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
771
+ }
772
+ /**
773
+ * Get the current correlation ID from context
774
+ *
775
+ * Resolution order:
776
+ * 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
777
+ * 2. Baggage (if propagated from upstream)
778
+ * 3. Active span's trace ID (first 16 chars as fallback)
779
+ * 4. undefined (if not in any context)
780
+ *
781
+ * @returns Current correlation ID or undefined
782
+ *
783
+ * @example
784
+ * ```typescript
785
+ * const id = getCorrelationId();
786
+ * if (id) {
787
+ * console.log('Correlation ID:', id);
788
+ * }
789
+ * ```
790
+ */
791
+ function getCorrelationId() {
792
+ const storedId = correlationStorage.getStore()?.value;
793
+ if (storedId) return storedId;
794
+ const activeContext = context.active();
795
+ const baggageEntry = propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
796
+ if (baggageEntry?.value) return baggageEntry.value;
797
+ const span = trace.getActiveSpan();
798
+ if (span) return span.spanContext().traceId.slice(0, 16);
799
+ }
800
+ /**
801
+ * Get or create a correlation ID
802
+ *
803
+ * If a correlation ID exists in the current context, returns it.
804
+ * Otherwise, generates a new one.
805
+ *
806
+ * @returns Existing or new correlation ID
807
+ *
808
+ * @example
809
+ * ```typescript
810
+ * const id = getOrCreateCorrelationId();
811
+ * // Always returns a valid correlation ID
812
+ * ```
813
+ */
814
+ function getOrCreateCorrelationId() {
815
+ return getCorrelationId() ?? generateCorrelationId();
816
+ }
817
+ /**
818
+ * Run a function with a specific correlation ID in context
819
+ *
820
+ * The correlation ID will be available via getCorrelationId() throughout
821
+ * the execution of the function and any async operations it spawns.
822
+ *
823
+ * @param correlationId - Correlation ID to use
824
+ * @param fn - Function to execute
825
+ * @returns The return value of the function
826
+ *
827
+ * @example
828
+ * ```typescript
829
+ * await runWithCorrelationId('abc123', async () => {
830
+ * // getCorrelationId() returns 'abc123' here
831
+ * await processRequest();
832
+ * });
833
+ * ```
834
+ */
835
+ function runWithCorrelationId(correlationId, fn) {
836
+ return correlationStorage.run({ value: correlationId }, fn);
837
+ }
838
+ /**
839
+ * Set correlation ID in the current context (mutates context)
840
+ *
841
+ * Note: This updates the AsyncLocalStorage context. For proper scoping
842
+ * across async boundaries, prefer runWithCorrelationId() instead.
843
+ *
844
+ * @param correlationId - Correlation ID to set
845
+ *
846
+ * @example
847
+ * ```typescript
848
+ * setCorrelationId('abc123');
849
+ * // Now getCorrelationId() returns 'abc123'
850
+ * ```
851
+ */
852
+ function setCorrelationId(correlationId) {
853
+ enterOrRun(correlationStorage, correlationId);
854
+ }
855
+ /**
856
+ * Set correlation ID in baggage for propagation
857
+ *
858
+ * This adds the correlation ID to the W3C baggage header, allowing it
859
+ * to be propagated to downstream services.
860
+ *
861
+ * Note: Only use this when you explicitly want cross-service propagation.
862
+ * Default is OFF to avoid header bloat.
863
+ *
864
+ * @param correlationId - Correlation ID to propagate
865
+ * @returns New context with baggage set
866
+ *
867
+ * @example
868
+ * ```typescript
869
+ * const newContext = setCorrelationIdInBaggage('abc123');
870
+ * context.with(newContext, () => {
871
+ * // Baggage will be propagated in outgoing requests
872
+ * });
873
+ * ```
874
+ */
875
+ function setCorrelationIdInBaggage(correlationId) {
876
+ const activeContext = context.active();
877
+ let baggage = propagation.getBaggage(activeContext) ?? propagation.createBaggage();
878
+ baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
879
+ return propagation.setBaggage(activeContext, baggage);
880
+ }
881
+ /**
882
+ * Get the correlation storage instance (for internal use in init/shutdown)
883
+ */
884
+ function getCorrelationStorage() {
885
+ return correlationStorage;
886
+ }
887
+
888
+ //#endregion
889
+ //#region src/event-queue.ts
890
+ const DEFAULT_CONFIG$2 = {
891
+ maxSize: 5e4,
892
+ batchSize: 100,
893
+ flushInterval: 1e4,
894
+ maxRetries: 3,
895
+ rateLimit: {
896
+ maxEventsPerSecond: 100,
897
+ burstCapacity: 200
898
+ }
899
+ };
900
+ /**
901
+ * Get subscriber name for metrics (stable, low-cardinality)
902
+ *
903
+ * Priority:
904
+ * 1. Explicit config: subscriber.name
905
+ * 2. Class static property (if available)
906
+ * 3. Fallback: lowercase class name without "Subscriber" suffix
907
+ */
908
+ function getSubscriberName(subscriber) {
909
+ if (subscriber.name) return subscriber.name.toLowerCase();
910
+ return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
911
+ }
912
+ /**
913
+ * Events queue with batching and backpressure
914
+ *
915
+ * Features:
916
+ * - Batches events for efficient sending
917
+ * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
918
+ * - Exponential backoff retry
919
+ * - Rate limiting to prevent overwhelming subscribers
920
+ * - Graceful flush on shutdown
921
+ */
922
+ var EventQueue = class {
923
+ queue = [];
924
+ flushTimer = null;
925
+ config;
926
+ subscribers;
927
+ rateLimiter;
928
+ flushPromise = null;
929
+ isShuttingDown = false;
930
+ metrics = null;
931
+ observableCleanups = [];
932
+ subscriberHealthy = /* @__PURE__ */ new Map();
933
+ constructor(subscribers, config) {
934
+ this.subscribers = subscribers;
935
+ this.config = {
936
+ ...DEFAULT_CONFIG$2,
937
+ ...config
938
+ };
939
+ this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
940
+ for (const subscriber of subscribers) {
941
+ const name = getSubscriberName(subscriber);
942
+ this.subscriberHealthy.set(name, true);
943
+ }
944
+ this.initMetrics();
945
+ }
946
+ /**
947
+ * Initialize OTel metrics for queue observability
948
+ */
949
+ initMetrics() {
950
+ const meter = getConfig$1().meter;
951
+ const queueSize = meter.createObservableGauge("autotel.event_delivery.queue.size", {
952
+ description: "Current number of events in the delivery queue",
953
+ unit: "count"
954
+ });
955
+ const queueSizeCallback = (observableResult) => {
956
+ observableResult.observe(this.queue.length);
957
+ };
958
+ queueSize.addCallback(queueSizeCallback);
959
+ this.observableCleanups.push(() => queueSize.removeCallback(queueSizeCallback));
960
+ const oldestAge = meter.createObservableGauge("autotel.event_delivery.queue.oldest_age_ms", {
961
+ description: "Age of the oldest event in the queue in milliseconds",
962
+ unit: "ms"
963
+ });
964
+ const oldestAgeCallback = (observableResult) => {
965
+ if (this.queue.length > 0) {
966
+ const oldest = this.queue[0];
967
+ const ageMs = Date.now() - oldest.timestamp;
968
+ observableResult.observe(ageMs);
969
+ } else observableResult.observe(0);
970
+ };
971
+ oldestAge.addCallback(oldestAgeCallback);
972
+ this.observableCleanups.push(() => oldestAge.removeCallback(oldestAgeCallback));
973
+ const delivered = meter.createCounter("autotel.event_delivery.queue.delivered", {
974
+ description: "Number of events successfully delivered to subscribers",
975
+ unit: "count"
976
+ });
977
+ const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
978
+ description: "Number of events that failed delivery after all retry attempts",
979
+ unit: "count"
980
+ });
981
+ const dropped = meter.createCounter("autotel.event_delivery.queue.dropped", {
982
+ description: "Number of events dropped from the queue",
983
+ unit: "count"
984
+ });
985
+ const latency = meter.createHistogram("autotel.event_delivery.queue.latency_ms", {
986
+ description: "Event delivery latency from enqueue to successful send",
987
+ unit: "ms"
988
+ });
989
+ const subscriberHealth = meter.createObservableGauge("autotel.event_delivery.subscriber.health", {
990
+ description: "Subscriber health status (1=healthy, 0=unhealthy)",
991
+ unit: "1"
992
+ });
993
+ const subscriberHealthCallback = (observableResult) => {
994
+ for (const [subscriberName, isHealthy] of this.subscriberHealthy) observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName });
995
+ };
996
+ subscriberHealth.addCallback(subscriberHealthCallback);
997
+ this.observableCleanups.push(() => subscriberHealth.removeCallback(subscriberHealthCallback));
998
+ this.metrics = {
999
+ queueSize,
1000
+ oldestAge,
1001
+ delivered,
1002
+ failed,
1003
+ dropped,
1004
+ latency,
1005
+ subscriberHealth
1006
+ };
1007
+ }
1008
+ /**
1009
+ * Record a dropped event with reason and emit debug breadcrumb
1010
+ */
1011
+ recordDropped(reason, event, subscriberName) {
1012
+ const attrs = { reason };
1013
+ if (subscriberName) attrs.subscriber = subscriberName;
1014
+ this.metrics?.dropped.add(1, attrs);
1015
+ const logLevel = reason === "payload_invalid" ? "error" : "warn";
1016
+ const logger = getLogger();
1017
+ if (logLevel === "error") logger.error({
1018
+ eventName: event?.name,
1019
+ subscriber: subscriberName,
1020
+ reason,
1021
+ correlationId: event?._correlationId,
1022
+ traceId: event?._traceId
1023
+ }, `[autotel] Event dropped: ${reason}`);
1024
+ else logger.warn({
1025
+ eventName: event?.name,
1026
+ subscriber: subscriberName,
1027
+ reason,
1028
+ correlationId: event?._correlationId,
1029
+ traceId: event?._traceId
1030
+ }, `[autotel] Event dropped: ${reason}`);
1031
+ }
1032
+ /**
1033
+ * Record permanent delivery failure (after all retries exhausted)
1034
+ * Increments failed counter and logs error
1035
+ */
1036
+ recordFailed(event, subscriberName, error) {
1037
+ this.metrics?.failed.add(1, { subscriber: subscriberName });
1038
+ this.subscriberHealthy.set(subscriberName, false);
1039
+ getLogger().error({
1040
+ eventName: event.name,
1041
+ subscriber: subscriberName,
1042
+ correlationId: event._correlationId,
1043
+ traceId: event._traceId,
1044
+ err: error
1045
+ }, `[autotel] Event delivery failed after all retries`);
1046
+ }
1047
+ /**
1048
+ * Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
1049
+ * Used during retry attempts - only recordFailed should increment the counter
1050
+ */
1051
+ markSubscriberUnhealthy(subscriberName) {
1052
+ this.subscriberHealthy.set(subscriberName, false);
1053
+ }
1054
+ /**
1055
+ * Record successful delivery
1056
+ */
1057
+ recordDelivered(event, subscriberName, startTime) {
1058
+ const latencyMs = Date.now() - startTime;
1059
+ this.metrics?.delivered.add(1, { subscriber: subscriberName });
1060
+ this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
1061
+ this.subscriberHealthy.set(subscriberName, true);
1062
+ }
1063
+ /**
1064
+ * Enqueue an event for sending
1065
+ *
1066
+ * Backpressure policy:
1067
+ * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
1068
+ */
1069
+ enqueue(event) {
1070
+ if (this.isShuttingDown) {
1071
+ this.recordDropped("shutdown", event);
1072
+ return;
1073
+ }
1074
+ if (this.queue.length >= this.config.maxSize) {
1075
+ const droppedEvent = this.queue.shift();
1076
+ this.recordDropped("rate_limit", droppedEvent);
1077
+ 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.`);
1078
+ }
1079
+ const enrichedEvent = {
1080
+ ...event,
1081
+ _correlationId: event._correlationId || getOrCreateCorrelationId()
1082
+ };
1083
+ this.queue.push(enrichedEvent);
1084
+ this.scheduleBatchFlush();
1085
+ }
1086
+ /**
1087
+ * Schedule a batch flush if not already scheduled
1088
+ */
1089
+ scheduleBatchFlush() {
1090
+ if (this.flushTimer || this.flushPromise) return;
1091
+ this.flushTimer = setTimeout(() => {
1092
+ this.flushTimer = null;
1093
+ this.flushBatch();
1094
+ }, this.config.flushInterval);
1095
+ }
1096
+ /**
1097
+ * Flush a batch of events
1098
+ * Uses promise-based concurrency control to prevent race conditions
1099
+ */
1100
+ async flushBatch() {
1101
+ if (this.queue.length === 0) return;
1102
+ if (this.flushPromise) {
1103
+ await this.flushPromise;
1104
+ return;
1105
+ }
1106
+ this.flushPromise = this.doFlushBatch();
1107
+ try {
1108
+ await this.flushPromise;
1109
+ } finally {
1110
+ this.flushPromise = null;
1111
+ if (this.queue.length > 0) this.scheduleBatchFlush();
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Internal flush implementation
1116
+ */
1117
+ async doFlushBatch() {
1118
+ const batch = this.queue.splice(0, this.config.batchSize);
1119
+ await this.sendWithRetry(batch, this.config.maxRetries);
1120
+ }
1121
+ /**
1122
+ * Send events with exponential backoff retry
1123
+ * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
1124
+ * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
1125
+ */
1126
+ async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
1127
+ const failedDeliveries = await this.sendToSubscribers(events, subscribersByEventIndex);
1128
+ if (failedDeliveries.length > 0) if (retriesLeft > 0) {
1129
+ const failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
1130
+ const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
1131
+ const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
1132
+ for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
1133
+ const set = /* @__PURE__ */ new Set();
1134
+ for (const { eventIndex, subscriberName } of failedDeliveries) if (eventIndex === origIndex) set.add(subscriberName);
1135
+ failedSubscribersByRetryIndex.set(j, set);
1136
+ }
1137
+ const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
1138
+ await new Promise((resolve) => setTimeout(resolve, delay));
1139
+ return this.sendWithRetry(eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex);
1140
+ } else {
1141
+ for (const { eventIndex, subscriberName, error } of failedDeliveries) {
1142
+ const event = events[eventIndex];
1143
+ if (event) this.recordFailed(event, subscriberName, error);
1144
+ }
1145
+ const failedSubscriberNames = [...new Set(failedDeliveries.map((f) => f.subscriberName))];
1146
+ getLogger().error({
1147
+ failedSubscribers: failedSubscriberNames,
1148
+ retriesAttempted: this.config.maxRetries
1149
+ }, "[autotel] Failed to send events after retries");
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Send events to configured subscribers with rate limiting and metrics.
1154
+ * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
1155
+ * Returns per-event, per-subscriber failures (empty if all succeeded).
1156
+ */
1157
+ async sendToSubscribers(events, subscribersByEventIndex) {
1158
+ const failedDeliveries = [];
1159
+ const sendOne = async (event, eventIndex) => {
1160
+ const subscriberNames = subscribersByEventIndex?.get(eventIndex);
1161
+ const failures = await this.sendEventToSubscribers(event, subscriberNames ?? void 0);
1162
+ for (const failure of failures) failedDeliveries.push({
1163
+ eventIndex,
1164
+ subscriberName: failure.subscriberName,
1165
+ error: failure.error
1166
+ });
1167
+ };
1168
+ if (!this.rateLimiter) {
1169
+ for (const [i, event] of events.entries()) if (event) await sendOne(event, i);
1170
+ return failedDeliveries;
1171
+ }
1172
+ for (const [i, event] of events.entries()) {
1173
+ await this.rateLimiter.waitForToken();
1174
+ if (event) await sendOne(event, i);
1175
+ }
1176
+ return failedDeliveries;
1177
+ }
1178
+ /**
1179
+ * Send a single event to subscribers.
1180
+ * - When subscriberNames is undefined (initial attempt): send to all subscribers.
1181
+ * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
1182
+ * Returns list of subscribers that failed (empty if all succeeded).
1183
+ */
1184
+ async sendEventToSubscribers(event, subscriberNames) {
1185
+ const startTime = event.timestamp;
1186
+ const failures = [];
1187
+ const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter((s) => subscriberNames.has(getSubscriberName(s)));
1188
+ const results = await Promise.allSettled(subscribersToTry.map(async (subscriber) => {
1189
+ const subscriberName = getSubscriberName(subscriber);
1190
+ try {
1191
+ await subscriber.trackEvent(event.name, event.attributes, {
1192
+ autotel: event.autotel,
1193
+ schema: event.schema
1194
+ });
1195
+ this.recordDelivered(event, subscriberName, startTime);
1196
+ return {
1197
+ subscriberName,
1198
+ success: true
1199
+ };
1200
+ } catch (error) {
1201
+ this.markSubscriberUnhealthy(subscriberName);
1202
+ return {
1203
+ subscriberName,
1204
+ success: false,
1205
+ error: error instanceof Error ? error : void 0
1206
+ };
1207
+ }
1208
+ }));
1209
+ for (const result of results) if (result.status === "fulfilled" && !result.value.success) failures.push({
1210
+ subscriberName: result.value.subscriberName,
1211
+ error: result.value.error
1212
+ });
1213
+ return failures;
1214
+ }
1215
+ /**
1216
+ * Flush all remaining events. Queue remains usable after flush (e.g. for
1217
+ * auto-flush at root span end). Use shutdown() when tearing down the queue.
1218
+ */
1219
+ async flush() {
1220
+ if (this.flushTimer) {
1221
+ clearTimeout(this.flushTimer);
1222
+ this.flushTimer = null;
1223
+ }
1224
+ if (this.flushPromise) await this.flushPromise;
1225
+ while (this.queue.length > 0) await this.doFlushBatch();
1226
+ }
1227
+ /**
1228
+ * Flush remaining events and permanently disable the queue (reject new events).
1229
+ * Use for process/SDK shutdown; use flush() for periodic or span-end drain.
1230
+ */
1231
+ async shutdown() {
1232
+ this.isShuttingDown = true;
1233
+ await this.flush();
1234
+ }
1235
+ /**
1236
+ * Cleanup observable metric callbacks to prevent memory leaks
1237
+ * Call this when destroying the EventQueue instance
1238
+ */
1239
+ cleanup() {
1240
+ for (const cleanupFn of this.observableCleanups) try {
1241
+ cleanupFn();
1242
+ } catch {}
1243
+ this.observableCleanups = [];
1244
+ }
1245
+ /**
1246
+ * Get queue size (for testing/debugging)
1247
+ */
1248
+ size() {
1249
+ return this.queue.length;
1250
+ }
1251
+ /**
1252
+ * Get subscriber health status (for testing/debugging)
1253
+ */
1254
+ getSubscriberHealth() {
1255
+ return new Map(this.subscriberHealthy);
1256
+ }
1257
+ /**
1258
+ * Check if a specific subscriber is healthy
1259
+ */
1260
+ isSubscriberHealthy(subscriberName) {
1261
+ return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
1262
+ }
1263
+ /**
1264
+ * Manually mark a subscriber as healthy or unhealthy
1265
+ * (used for circuit breaker integration)
1266
+ */
1267
+ setSubscriberHealth(subscriberName, healthy) {
1268
+ this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
1269
+ }
1270
+ };
1271
+
1272
+ //#endregion
1273
+ //#region src/validation.ts
1274
+ const DEFAULT_CONFIG$1 = {
1275
+ maxEventNameLength: 100,
1276
+ maxAttributeKeyLength: 100,
1277
+ maxAttributeValueLength: 1e3,
1278
+ maxAttributeCount: 50,
1279
+ maxNestingDepth: 3,
1280
+ sensitivePatterns: [
1281
+ /password/i,
1282
+ /secret/i,
1283
+ /token/i,
1284
+ /api[_-]?key/i,
1285
+ /access[_-]?key/i,
1286
+ /private[_-]?key/i,
1287
+ /auth/i,
1288
+ /credential/i,
1289
+ /ssn/i,
1290
+ /credit[_-]?card/i
1291
+ ]
1292
+ };
1293
+ var ValidationError = class extends Error {
1294
+ constructor(message) {
1295
+ super(message);
1296
+ this.name = "ValidationError";
1297
+ }
1298
+ };
1299
+ /**
1300
+ * Validate and sanitize event name
1301
+ * Throws ValidationError if invalid
1302
+ */
1303
+ function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
1304
+ if (typeof eventName !== "string") throw new ValidationError(`Event name must be a string, got ${typeof eventName}`);
1305
+ const trimmed = eventName.trim();
1306
+ if (trimmed.length === 0) throw new ValidationError("Event name cannot be empty");
1307
+ if (trimmed.length > config.maxEventNameLength) throw new ValidationError(`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`);
1308
+ if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) throw new ValidationError(`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`);
1309
+ return trimmed;
1310
+ }
1311
+ /**
1312
+ * Validate and sanitize attributes
1313
+ * Returns sanitized attributes (sensitive data redacted)
1314
+ */
1315
+ function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
1316
+ if (attributes === void 0 || attributes === null) return;
1317
+ if (typeof attributes !== "object" || Array.isArray(attributes)) throw new ValidationError("Attributes must be an object");
1318
+ const keys = Object.keys(attributes);
1319
+ if (keys.length > config.maxAttributeCount) throw new ValidationError(`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`);
1320
+ const sanitized = {};
1321
+ for (const key of keys) {
1322
+ if (key.length > config.maxAttributeKeyLength) throw new ValidationError(`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`);
1323
+ const value = attributes[key];
1324
+ if (typeof value === "string" && config.sensitivePatterns.some((pattern) => pattern.test(key))) {
1325
+ sanitized[key] = "[REDACTED]";
1326
+ continue;
1327
+ }
1328
+ sanitized[key] = sanitizeValue(value, config, 1);
1329
+ }
1330
+ return sanitized;
1331
+ }
1332
+ /**
1333
+ * Sanitize attribute value (recursive)
1334
+ */
1335
+ function sanitizeValue(value, config, depth) {
1336
+ if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
1337
+ if (value === null || value === void 0) return value;
1338
+ if (typeof value === "string") {
1339
+ if (value.length > config.maxAttributeValueLength) return value.slice(0, config.maxAttributeValueLength) + "...";
1340
+ return value;
1341
+ }
1342
+ if (typeof value === "number" || typeof value === "boolean") return value;
1343
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
1344
+ if (typeof value === "object") try {
1345
+ JSON.stringify(value);
1346
+ const sanitized = {};
1347
+ for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) {
1348
+ const nested = value[key];
1349
+ sanitized[key] = typeof nested === "string" && config.sensitivePatterns.some((pattern) => pattern.test(key)) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
1350
+ }
1351
+ return sanitized;
1352
+ } catch {
1353
+ return "[CIRCULAR]";
1354
+ }
1355
+ return `[${typeof value}]`;
1356
+ }
1357
+ /**
1358
+ * Validate and sanitize an events event
1359
+ * Returns { eventName, attributes } with sanitized values
1360
+ */
1361
+ function validateEvent(eventName, attributes, config) {
1362
+ const fullConfig = {
1363
+ ...DEFAULT_CONFIG$1,
1364
+ ...config
1365
+ };
1366
+ return {
1367
+ eventName: validateEventName(eventName, fullConfig),
1368
+ attributes: validateAttributes(attributes, fullConfig)
1369
+ };
1370
+ }
1371
+
1372
+ //#endregion
1373
+ //#region src/track.ts
1374
+ /**
1375
+ * Global track() function for business events
1376
+ *
1377
+ * Simple, no instantiation needed, auto-attaches trace context
1378
+ */
1379
+ let eventsQueue = null;
1380
+ /**
1381
+ * Build autotel event context for trace correlation
1382
+ *
1383
+ * Works in multiple contexts:
1384
+ * 1. Inside a span → use current span's trace_id + span_id
1385
+ * 2. Outside span → use correlation_id only
1386
+ * 3. With trace URL config → include clickable trace URL
1387
+ */
1388
+ function buildAutotelContext(span) {
1389
+ const eventsConfig = getEventsConfig();
1390
+ const config = getConfig();
1391
+ const correlationId = getOrCreateCorrelationId();
1392
+ if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
1393
+ const autotelContext = { correlation_id: correlationId };
1394
+ const spanContext = span?.spanContext();
1395
+ if (spanContext) {
1396
+ autotelContext.trace_id = spanContext.traceId;
1397
+ autotelContext.span_id = spanContext.spanId;
1398
+ autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
1399
+ const traceState = spanContext.traceState;
1400
+ if (traceState) try {
1401
+ if (typeof traceState.serialize === "function") {
1402
+ const traceStateStr = traceState.serialize();
1403
+ if (traceStateStr) autotelContext.trace_state = traceStateStr;
1404
+ }
1405
+ } catch {}
1406
+ if (eventsConfig.traceUrl && config) {
1407
+ const traceUrl = eventsConfig.traceUrl({
1408
+ traceId: spanContext.traceId,
1409
+ spanId: spanContext.spanId,
1410
+ correlationId,
1411
+ serviceName: config.service,
1412
+ environment: config.environment
1413
+ });
1414
+ if (traceUrl) autotelContext.trace_url = traceUrl;
1415
+ }
1416
+ } else if (eventsConfig.traceUrl && config) {
1417
+ const traceUrl = eventsConfig.traceUrl({
1418
+ correlationId,
1419
+ serviceName: config.service,
1420
+ environment: config.environment
1421
+ });
1422
+ if (traceUrl) autotelContext.trace_url = traceUrl;
1423
+ }
1424
+ return autotelContext;
1425
+ }
1426
+ /**
1427
+ * Initialize events queue lazily
1428
+ */
1429
+ function getOrCreateQueue() {
1430
+ if (!isInitialized()) {
1431
+ warnIfNotInitialized("track()");
1432
+ return null;
1433
+ }
1434
+ if (!eventsQueue) {
1435
+ const config = getConfig();
1436
+ if (!config?.subscribers || config.subscribers.length === 0) return null;
1437
+ eventsQueue = new EventQueue(config.subscribers);
1438
+ }
1439
+ return eventsQueue;
1440
+ }
1441
+ /**
1442
+ * Track a business events event
1443
+ *
1444
+ * Features:
1445
+ * - Auto-attaches traceId and spanId if in active span
1446
+ * - Batched sending with retry
1447
+ * - Type-safe with optional generic
1448
+ * - No-op if init() not called or no subscribers configured
1449
+ *
1450
+ * @example Basic usage
1451
+ * ```typescript
1452
+ * track('user.signup', { userId: '123', plan: 'pro' })
1453
+ * ```
1454
+ *
1455
+ * @example With type safety
1456
+ * ```typescript
1457
+ * interface EventDatas {
1458
+ * 'user.signup': { userId: string; plan: string }
1459
+ * 'plan.upgraded': { userId: string; revenue: number }
1460
+ * }
1461
+ *
1462
+ * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
1463
+ * ```
1464
+ *
1465
+ * @example Trace correlation (automatic)
1466
+ * ```typescript
1467
+ * @Instrumented()
1468
+ * class UserService {
1469
+ * async createUser(data: CreateUserData) {
1470
+ * // This track call automatically includes traceId + spanId
1471
+ * track('user.signup', { userId: data.id })
1472
+ * }
1473
+ * }
1474
+ * ```
1475
+ */
1476
+ function track(event, data, options) {
1477
+ const queue = getOrCreateQueue();
1478
+ if (!queue) return;
1479
+ const validated = validateEvent(event, data, getValidationConfig() || void 0);
1480
+ const span = trace.getActiveSpan();
1481
+ const enrichedData = span ? {
1482
+ ...validated.attributes,
1483
+ traceId: span.spanContext().traceId,
1484
+ spanId: span.spanContext().spanId
1485
+ } : validated.attributes;
1486
+ const autotelContext = buildAutotelContext(span);
1487
+ queue.enqueue({
1488
+ name: validated.eventName,
1489
+ attributes: enrichedData,
1490
+ timestamp: Date.now(),
1491
+ autotel: autotelContext,
1492
+ schema: options?.schema
1493
+ });
1494
+ }
1495
+ /**
1496
+ * Get events queue (for flush/shutdown)
1497
+ * @internal
1498
+ */
1499
+ function getEventQueue() {
1500
+ return eventsQueue;
1501
+ }
1502
+ /**
1503
+ * Reset events queue (for shutdown/cleanup)
1504
+ * @internal
1505
+ */
1506
+ function resetEventQueue() {
1507
+ eventsQueue = null;
1508
+ }
1509
+
1510
+ //#endregion
1511
+ //#region src/circuit-breaker.ts
1512
+ const DEFAULT_CONFIG = {
1513
+ failureThreshold: 5,
1514
+ resetTimeout: 3e4,
1515
+ windowSize: 6e4
1516
+ };
1517
+ const CircuitState = {
1518
+ CLOSED: "CLOSED",
1519
+ OPEN: "OPEN",
1520
+ HALF_OPEN: "HALF_OPEN"
1521
+ };
1522
+ /**
1523
+ * Circuit breaker implementation
1524
+ *
1525
+ * Tracks failures and automatically opens the circuit to prevent
1526
+ * overwhelming failing subscribers.
1527
+ */
1528
+ var CircuitBreaker = class {
1529
+ state = CircuitState.CLOSED;
1530
+ failures = [];
1531
+ lastFailureTime = 0;
1532
+ config;
1533
+ name;
1534
+ constructor(name, config) {
1535
+ this.name = name;
1536
+ this.config = {
1537
+ ...DEFAULT_CONFIG,
1538
+ ...config
1539
+ };
1540
+ }
1541
+ /**
1542
+ * Execute a function with circuit breaker protection
1543
+ * Throws CircuitOpenError if circuit is open
1544
+ */
1545
+ async execute(fn) {
1546
+ if (this.state === CircuitState.OPEN) {
1547
+ const now = Date.now();
1548
+ if (now - this.lastFailureTime >= this.config.resetTimeout) this.state = CircuitState.HALF_OPEN;
1549
+ else throw new CircuitOpenError(`Circuit breaker is OPEN for ${this.name}. Will retry in ${Math.ceil((this.config.resetTimeout - (now - this.lastFailureTime)) / 1e3)}s`);
1550
+ }
1551
+ try {
1552
+ const result = await fn();
1553
+ if (this.state === CircuitState.HALF_OPEN) this.reset();
1554
+ return result;
1555
+ } catch (error) {
1556
+ this.recordFailure(error);
1557
+ throw error;
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Record a failure and potentially open the circuit
1562
+ */
1563
+ recordFailure(error) {
1564
+ const now = Date.now();
1565
+ this.failures = this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
1566
+ this.failures.push({
1567
+ timestamp: now,
1568
+ error: error instanceof Error ? error.message : String(error)
1569
+ });
1570
+ this.lastFailureTime = now;
1571
+ if (this.failures.length >= this.config.failureThreshold) {
1572
+ if (this.state === CircuitState.HALF_OPEN) this.state = CircuitState.OPEN;
1573
+ else if (this.state === CircuitState.CLOSED) this.state = CircuitState.OPEN;
1574
+ }
1575
+ }
1576
+ /**
1577
+ * Reset the circuit breaker (on success)
1578
+ */
1579
+ reset() {
1580
+ this.state = CircuitState.CLOSED;
1581
+ this.failures = [];
1582
+ this.lastFailureTime = 0;
1583
+ }
1584
+ /**
1585
+ * Get current state (for monitoring)
1586
+ */
1587
+ getState() {
1588
+ return this.state;
1589
+ }
1590
+ /**
1591
+ * Get failure count in current window
1592
+ */
1593
+ getFailureCount() {
1594
+ const now = Date.now();
1595
+ this.failures = this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
1596
+ return this.failures.length;
1597
+ }
1598
+ /**
1599
+ * Get recent failures (for debugging)
1600
+ */
1601
+ getRecentFailures() {
1602
+ const now = Date.now();
1603
+ return this.failures.filter((f) => now - f.timestamp < this.config.windowSize);
1604
+ }
1605
+ /**
1606
+ * Manually reset the circuit breaker (for testing or manual intervention)
1607
+ */
1608
+ forceReset() {
1609
+ this.reset();
1610
+ }
1611
+ /**
1612
+ * Manually open the circuit (for testing or manual intervention)
1613
+ */
1614
+ forceOpen() {
1615
+ this.state = CircuitState.OPEN;
1616
+ this.lastFailureTime = Date.now();
1617
+ }
1618
+ };
1619
+ /**
1620
+ * Error thrown when circuit is open
1621
+ */
1622
+ var CircuitOpenError = class extends Error {
1623
+ constructor(message) {
1624
+ super(message);
1625
+ this.name = "CircuitOpenError";
1626
+ }
1627
+ };
1628
+
1629
+ //#endregion
1630
+ //#region src/operation-context.ts
1631
+ /**
1632
+ * Operation context tracking using AsyncLocalStorage
1633
+ *
1634
+ * This module provides a way to track operation names across async boundaries
1635
+ * so they can be automatically captured in events events.
1636
+ *
1637
+ * We cannot read span attributes from OpenTelemetry's API (it's write-only),
1638
+ * so we maintain our own async context storage.
1639
+ */
1640
+ /**
1641
+ * AsyncLocalStorage instance for tracking operation context
1642
+ */
1643
+ const operationStorage = new nodeAsyncHooks.AsyncLocalStorage();
1644
+ /**
1645
+ * Get the current operation context (if any)
1646
+ *
1647
+ * @returns The current operation context, or undefined if not in an operation
1648
+ *
1649
+ * @example
1650
+ * ```typescript
1651
+ * const ctx = getOperationContext();
1652
+ * if (ctx) {
1653
+ * console.log('Current operation:', ctx.name);
1654
+ * }
1655
+ * ```
1656
+ */
1657
+ function getOperationContext() {
1658
+ return operationStorage.getStore();
1659
+ }
1660
+ /**
1661
+ * Run a function within an operation context
1662
+ *
1663
+ * This sets the operation name for the duration of the function execution,
1664
+ * including all async operations spawned from it.
1665
+ *
1666
+ * @param name - The operation name to set
1667
+ * @param fn - The function to execute within the context
1668
+ * @returns The result of the function
1669
+ *
1670
+ * @example
1671
+ * ```typescript
1672
+ * const result = await runInOperationContext('user.create', async () => {
1673
+ * // Any events.trackEvent() calls here will automatically capture
1674
+ * // 'operation.name': 'user.create'
1675
+ * await createUser();
1676
+ * return 'success';
1677
+ * });
1678
+ * ```
1679
+ */
1680
+ function runInOperationContext(name, fn) {
1681
+ return operationStorage.run({ name }, fn);
1682
+ }
1683
+
1684
+ //#endregion
1685
+ //#region src/events-config.ts
1686
+ /**
1687
+ * Hash a string value for PII protection
1688
+ *
1689
+ * Uses a simple, fast hash function suitable for correlation.
1690
+ * NOT cryptographically secure - use for PII masking, not security.
1691
+ */
1692
+ function hashValue(value) {
1693
+ let hash = 0;
1694
+ for (let i = 0; i < value.length; i++) {
1695
+ const char = value.charCodeAt(i);
1696
+ hash = (hash << 5) - hash + char;
1697
+ hash = hash & hash;
1698
+ }
1699
+ return (hash >>> 0).toString(16).padStart(8, "0");
1700
+ }
1701
+
1702
+ //#endregion
1703
+ //#region src/event.ts
1704
+ /**
1705
+ * Events API for product events platforms
1706
+ *
1707
+ * Track user behavior, business events, and critical actions.
1708
+ * Sends to product events platforms (PostHog, Mixpanel, Amplitude) via subscribers.
1709
+ * For business people who think in events/funnels.
1710
+ *
1711
+ * For OpenTelemetry metrics (Prometheus/Grafana), use the Metrics class instead.
1712
+ *
1713
+ * @example Recommended: Configure subscribers in init(), use track() function
1714
+ * ```typescript
1715
+ * import { init, track } from 'autotel';
1716
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
1717
+ *
1718
+ * init({
1719
+ * service: 'my-app',
1720
+ * subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
1721
+ * });
1722
+ *
1723
+ * // Track events - uses subscribers from init()
1724
+ * track('application.submitted', { jobId: '123', userId: '456' });
1725
+ * ```
1726
+ *
1727
+ * @example Create Event instance (inherits subscribers from init)
1728
+ * ```typescript
1729
+ * import { Event } from 'autotel/event';
1730
+ *
1731
+ * // Uses subscribers configured in init()
1732
+ * const event = new Event('job-application');
1733
+ * event.trackEvent('application.submitted', { jobId: '123' });
1734
+ * ```
1735
+ *
1736
+ * @example Override subscribers for specific Event instance
1737
+ * ```typescript
1738
+ * import { Event } from 'autotel/event';
1739
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
1740
+ *
1741
+ * // Override: use different subscribers for this instance
1742
+ * const event = new Event('job-application', {
1743
+ * subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
1744
+ * });
1745
+ *
1746
+ * event.trackEvent('application.submitted', { jobId: '123' });
1747
+ * ```
1748
+ */
1749
+ var Event = class {
1750
+ serviceName;
1751
+ logger;
1752
+ collector;
1753
+ subscribers;
1754
+ hasSubscribers;
1755
+ circuitBreakers;
1756
+ /**
1757
+ * Create a new Event instance
1758
+ *
1759
+ * **Note**: Most users should use `init()` + `track()` instead of creating Event instances directly.
1760
+ *
1761
+ * **Subscriber Resolution**:
1762
+ * - If `subscribers` provided in options → uses those (instance override)
1763
+ * - If `subscribers` not provided → falls back to subscribers from `init()` (global config)
1764
+ * - If neither → no subscribers (events logged only)
1765
+ *
1766
+ * @param serviceName - Service name for identifying events
1767
+ * @param options - Optional configuration (logger, collector, subscribers)
1768
+ *
1769
+ * @example Recommended: Use track() with init()
1770
+ * ```typescript
1771
+ * import { init, track } from 'autotel';
1772
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
1773
+ *
1774
+ * init({
1775
+ * service: 'checkout',
1776
+ * subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
1777
+ * });
1778
+ *
1779
+ * track('purchase.completed', { amount: 99.99 });
1780
+ * ```
1781
+ *
1782
+ * @example Inherit subscribers from init()
1783
+ * ```typescript
1784
+ * // Uses subscribers configured in init()
1785
+ * const event = new Event('checkout');
1786
+ * event.trackEvent('purchase.completed', { amount: 99.99 });
1787
+ * ```
1788
+ *
1789
+ * @example Override subscribers for this instance
1790
+ * ```typescript
1791
+ * import { Event } from 'autotel/event';
1792
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
1793
+ *
1794
+ * // Override: use different subscribers for this instance only
1795
+ * const event = new Event('checkout', {
1796
+ * subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
1797
+ * });
1798
+ * ```
1799
+ */
1800
+ constructor(serviceName, options = {}) {
1801
+ this.serviceName = serviceName;
1802
+ this.logger = options.logger;
1803
+ this.collector = options.collector;
1804
+ this.subscribers = options.subscribers === void 0 ? getConfig()?.subscribers || [] : options.subscribers;
1805
+ this.hasSubscribers = this.subscribers.length > 0;
1806
+ this.circuitBreakers = /* @__PURE__ */ new Map();
1807
+ for (const subscriber of this.subscribers) {
1808
+ const subscriberName = subscriber.name || "Unknown";
1809
+ this.circuitBreakers.set(subscriber, new CircuitBreaker(subscriberName, {
1810
+ failureThreshold: 5,
1811
+ resetTimeout: 3e4,
1812
+ windowSize: 6e4
1813
+ }));
1814
+ }
1815
+ }
1816
+ /**
1817
+ * Automatically enrich attributes with all available telemetry context
1818
+ *
1819
+ * Auto-captures:
1820
+ * - Resource attributes: service.version, deployment.environment
1821
+ * - Trace context: traceId, spanId, correlationId
1822
+ * - Operation context: operation.name
1823
+ */
1824
+ enrichWithTelemetryContext(attributes = {}) {
1825
+ const enriched = {
1826
+ service: this.serviceName,
1827
+ ...attributes
1828
+ };
1829
+ const config = getConfig();
1830
+ if (config) {
1831
+ if (config.version) enriched["service.version"] = config.version;
1832
+ if (config.environment) enriched["deployment.environment"] = config.environment;
1833
+ }
1834
+ const spanContext = trace.getActiveSpan()?.spanContext();
1835
+ if (spanContext) {
1836
+ enriched.traceId = spanContext.traceId;
1837
+ enriched.spanId = spanContext.spanId;
1838
+ enriched.correlationId = spanContext.traceId.slice(0, 16);
1839
+ }
1840
+ const operationContext = getOperationContext();
1841
+ if (operationContext) enriched["operation.name"] = operationContext.name;
1842
+ return enriched;
1843
+ }
1844
+ /**
1845
+ * Build autotel event context for trace correlation
1846
+ *
1847
+ * Works in 4 contexts:
1848
+ * 1. Inside a span → use current span's trace_id + span_id
1849
+ * 2. Outside span but in AsyncLocalStorage context → use trace_id + correlation_id
1850
+ * 3. Totally standalone → use correlation_id + service/env/version
1851
+ * 4. Batch/fan-in (multiple linked parents) → use count + hash or full array
1852
+ *
1853
+ * @returns AutotelEventContext or undefined if trace context is disabled
1854
+ */
1855
+ buildAutotelContext() {
1856
+ const eventsConfig = getEventsConfig();
1857
+ if (!eventsConfig?.includeTraceContext) return { correlation_id: getOrCreateCorrelationId() };
1858
+ const config = getConfig();
1859
+ const spanContext = trace.getActiveSpan()?.spanContext();
1860
+ const correlationId = getOrCreateCorrelationId();
1861
+ const autotelContext = { correlation_id: correlationId };
1862
+ if (spanContext) {
1863
+ autotelContext.trace_id = spanContext.traceId;
1864
+ autotelContext.span_id = spanContext.spanId;
1865
+ autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
1866
+ const traceState = spanContext.traceState;
1867
+ if (traceState) {
1868
+ let traceStateStr = "";
1869
+ try {
1870
+ if (typeof traceState.serialize === "function") traceStateStr = traceState.serialize();
1871
+ } catch {}
1872
+ if (traceStateStr) autotelContext.trace_state = traceStateStr;
1873
+ }
1874
+ if (eventsConfig.traceUrl) {
1875
+ const traceUrl = eventsConfig.traceUrl({
1876
+ traceId: spanContext.traceId,
1877
+ spanId: spanContext.spanId,
1878
+ correlationId,
1879
+ serviceName: config?.service || this.serviceName,
1880
+ environment: config?.environment
1881
+ });
1882
+ if (traceUrl) autotelContext.trace_url = traceUrl;
1883
+ }
1884
+ } else if (eventsConfig.traceUrl && config) {
1885
+ const traceUrl = eventsConfig.traceUrl({
1886
+ correlationId,
1887
+ serviceName: config.service,
1888
+ environment: config.environment
1889
+ });
1890
+ if (traceUrl) autotelContext.trace_url = traceUrl;
1891
+ }
1892
+ return autotelContext;
1893
+ }
1894
+ /**
1895
+ * Enrich event attributes from baggage with guardrails
1896
+ *
1897
+ * @param attributes - Current event attributes
1898
+ * @returns Enriched attributes with baggage values
1899
+ */
1900
+ enrichFromBaggage(attributes) {
1901
+ const enrichConfig = getEventsConfig()?.enrichFromBaggage;
1902
+ if (!enrichConfig) return attributes;
1903
+ const enriched = { ...attributes };
1904
+ const activeContext = context.active();
1905
+ const baggage = propagation.getBaggage(activeContext);
1906
+ if (!baggage) return enriched;
1907
+ let keyCount = 0;
1908
+ let byteCount = 0;
1909
+ const maxKeys = enrichConfig.maxKeys ?? 10;
1910
+ const maxBytes = enrichConfig.maxBytes ?? 1024;
1911
+ const prefix = enrichConfig.prefix ?? "";
1912
+ for (const [key, entry] of baggage.getAllEntries()) {
1913
+ if (!this.isBaggageKeyAllowed(key, enrichConfig)) continue;
1914
+ if (keyCount >= maxKeys) break;
1915
+ const value = entry.value;
1916
+ const transform = enrichConfig.transform?.[key];
1917
+ let transformedValue;
1918
+ if (transform === "hash") transformedValue = hashValue(value);
1919
+ else if (transform === "plain" || !transform) transformedValue = value;
1920
+ else if (typeof transform === "function") transformedValue = transform(value);
1921
+ else transformedValue = value;
1922
+ const valueBytes = new TextEncoder().encode(transformedValue).length;
1923
+ if (byteCount + valueBytes > maxBytes) continue;
1924
+ const enrichedKey = `${prefix}${key}`;
1925
+ enriched[enrichedKey] = transformedValue;
1926
+ keyCount++;
1927
+ byteCount += valueBytes;
1928
+ }
1929
+ return enriched;
1930
+ }
1931
+ /**
1932
+ * Check if a baggage key is allowed based on config
1933
+ */
1934
+ isBaggageKeyAllowed(key, config) {
1935
+ if (config.deny) {
1936
+ for (const pattern of config.deny) if (this.matchesBaggagePattern(key, pattern)) return false;
1937
+ }
1938
+ for (const pattern of config.allow) if (this.matchesBaggagePattern(key, pattern)) return true;
1939
+ return false;
1940
+ }
1941
+ /**
1942
+ * Check if a key matches a baggage pattern
1943
+ * Supports exact matches and wildcard patterns (e.g., 'tenant.*')
1944
+ */
1945
+ matchesBaggagePattern(key, pattern) {
1946
+ if (pattern.endsWith(".*")) {
1947
+ const prefix = pattern.slice(0, -2);
1948
+ return key.startsWith(prefix + ".");
1949
+ }
1950
+ return key === pattern;
1951
+ }
1952
+ /**
1953
+ * Track a business event
1954
+ *
1955
+ * Use this for tracking user actions, business events, product usage:
1956
+ * - "user.signup"
1957
+ * - "order.completed"
1958
+ * - "feature.used"
1959
+ *
1960
+ * Events are sent to configured subscribers (PostHog, Mixpanel, etc.).
1961
+ *
1962
+ * @example
1963
+ * ```typescript
1964
+ * // Track user signup
1965
+ * events.trackEvent('user.signup', {
1966
+ * userId: '123',
1967
+ * plan: 'pro'
1968
+ * })
1969
+ *
1970
+ * // Track order
1971
+ * events.trackEvent('order.completed', {
1972
+ * orderId: 'ord_123',
1973
+ * amount: 99.99
1974
+ * })
1975
+ * ```
1976
+ */
1977
+ trackEvent(eventName, attributes) {
1978
+ const validated = validateEvent(eventName, attributes, getValidationConfig() || void 0);
1979
+ const enrichedAttributes = this.enrichWithTelemetryContext(validated.attributes);
1980
+ this.logger?.info({
1981
+ event: validated.eventName,
1982
+ attributes: enrichedAttributes
1983
+ }, "Event tracked");
1984
+ this.collector?.recordEvent({
1985
+ event: validated.eventName,
1986
+ attributes: enrichedAttributes,
1987
+ service: this.serviceName,
1988
+ timestamp: Date.now()
1989
+ });
1990
+ if (this.hasSubscribers) {
1991
+ const autotelContext = this.buildAutotelContext();
1992
+ const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
1993
+ this.notifySubscribers((subscriber) => subscriber.trackEvent(validated.eventName, finalAttributes, { autotel: autotelContext }));
1994
+ }
1995
+ }
1996
+ /**
1997
+ * Notify all subscribers concurrently without blocking
1998
+ * Uses circuit breakers to protect against failing subscribers
1999
+ * Uses Promise.allSettled to prevent subscriber errors from affecting other subscribers
2000
+ */
2001
+ async notifySubscribers(fn) {
2002
+ const promises = this.subscribers.map(async (subscriber) => {
2003
+ const circuitBreaker = this.circuitBreakers.get(subscriber);
2004
+ if (!circuitBreaker) return;
2005
+ try {
2006
+ await circuitBreaker.execute(() => fn(subscriber));
2007
+ } catch (error) {
2008
+ if (error instanceof CircuitOpenError) {
2009
+ getLogger().warn({ subscriberName: subscriber.name || "Unknown" }, `[Events] ${error.message}`);
2010
+ return;
2011
+ }
2012
+ getLogger().error({
2013
+ err: error instanceof Error ? error : void 0,
2014
+ subscriberName: subscriber.name || "Unknown"
2015
+ }, `[Events] Subscriber ${subscriber.name || "Unknown"} failed`);
2016
+ }
2017
+ });
2018
+ await Promise.allSettled(promises);
2019
+ }
2020
+ /**
2021
+ * Track conversion funnel steps
2022
+ *
2023
+ * Monitor where users drop off in multi-step processes.
2024
+ *
2025
+ * @example
2026
+ * ```typescript
2027
+ * // Track signup funnel
2028
+ * events.trackFunnelStep('signup', 'started', { userId: '123' })
2029
+ * events.trackFunnelStep('signup', 'email_verified', { userId: '123' })
2030
+ * events.trackFunnelStep('signup', 'completed', { userId: '123' })
2031
+ *
2032
+ * // Track checkout flow
2033
+ * events.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })
2034
+ * events.trackFunnelStep('checkout', 'payment_info', { cartValue: 99.99 })
2035
+ * events.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })
2036
+ * ```
2037
+ */
2038
+ trackFunnelStep(funnelName, status, attributes) {
2039
+ const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
2040
+ this.logger?.info({
2041
+ funnel: funnelName,
2042
+ status,
2043
+ attributes: enrichedAttributes
2044
+ }, "Funnel step tracked");
2045
+ this.collector?.recordFunnelStep({
2046
+ funnel: funnelName,
2047
+ status,
2048
+ attributes: enrichedAttributes,
2049
+ service: this.serviceName,
2050
+ timestamp: Date.now()
2051
+ });
2052
+ if (this.hasSubscribers) {
2053
+ const autotelContext = this.buildAutotelContext();
2054
+ const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
2055
+ this.notifySubscribers((subscriber) => subscriber.trackFunnelStep(funnelName, status, finalAttributes, { autotel: autotelContext }));
2056
+ }
2057
+ }
2058
+ /**
2059
+ * Track outcomes (success/failure/partial)
2060
+ *
2061
+ * Monitor success rates of critical operations.
2062
+ *
2063
+ * @example
2064
+ * ```typescript
2065
+ * // Track email delivery
2066
+ * events.trackOutcome('email.delivery', 'success', {
2067
+ * recipientType: 'user',
2068
+ * emailType: 'welcome'
2069
+ * })
2070
+ *
2071
+ * events.trackOutcome('email.delivery', 'failure', {
2072
+ * recipientType: 'user',
2073
+ * errorCode: 'invalid_email'
2074
+ * })
2075
+ *
2076
+ * // Track payment processing
2077
+ * events.trackOutcome('payment.process', 'success', { amount: 99.99 })
2078
+ * events.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })
2079
+ * ```
2080
+ */
2081
+ trackOutcome(operationName, status, attributes) {
2082
+ const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
2083
+ this.logger?.info({
2084
+ operation: operationName,
2085
+ status,
2086
+ attributes: enrichedAttributes
2087
+ }, "Outcome tracked");
2088
+ this.collector?.recordOutcome({
2089
+ operation: operationName,
2090
+ status,
2091
+ attributes: enrichedAttributes,
2092
+ service: this.serviceName,
2093
+ timestamp: Date.now()
2094
+ });
2095
+ if (this.hasSubscribers) {
2096
+ const autotelContext = this.buildAutotelContext();
2097
+ const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
2098
+ this.notifySubscribers((subscriber) => subscriber.trackOutcome(operationName, status, finalAttributes, { autotel: autotelContext }));
2099
+ }
2100
+ }
2101
+ /**
2102
+ * Track value metrics
2103
+ *
2104
+ * Record numerical values like revenue, transaction amounts,
2105
+ * item counts, processing times, engagement scores, etc.
2106
+ *
2107
+ * @example
2108
+ * ```typescript
2109
+ * // Track revenue
2110
+ * events.trackValue('order.revenue', 149.99, {
2111
+ * currency: 'USD',
2112
+ * productCategory: 'electronics'
2113
+ * })
2114
+ *
2115
+ * // Track items per cart
2116
+ * events.trackValue('cart.item_count', 5, {
2117
+ * userId: '123'
2118
+ * })
2119
+ *
2120
+ * // Track processing time
2121
+ * events.trackValue('api.response_time', 250, {
2122
+ * unit: 'ms',
2123
+ * endpoint: '/api/checkout'
2124
+ * })
2125
+ * ```
2126
+ */
2127
+ trackValue(metricName, value, attributes) {
2128
+ const enrichedAttributes = this.enrichWithTelemetryContext({
2129
+ metric: metricName,
2130
+ ...attributes
2131
+ });
2132
+ this.logger?.debug({
2133
+ metric: metricName,
2134
+ value,
2135
+ attributes: enrichedAttributes
2136
+ }, "Value tracked");
2137
+ this.collector?.recordValue({
2138
+ metric: metricName,
2139
+ value,
2140
+ attributes: enrichedAttributes,
2141
+ service: this.serviceName,
2142
+ timestamp: Date.now()
2143
+ });
2144
+ if (this.hasSubscribers) {
2145
+ const autotelContext = this.buildAutotelContext();
2146
+ const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
2147
+ this.notifySubscribers((subscriber) => subscriber.trackValue(metricName, value, finalAttributes, { autotel: autotelContext }));
2148
+ }
2149
+ }
2150
+ /**
2151
+ * Flush all subscribers and wait for pending events
2152
+ *
2153
+ * Call this before shutdown to ensure all events are delivered.
2154
+ *
2155
+ * @example
2156
+ * ```typescript
2157
+ * const event =new Event('app', { subscribers: [...] });
2158
+ *
2159
+ * // Before shutdown
2160
+ * await events.flush();
2161
+ * ```
2162
+ */
2163
+ async flush() {
2164
+ if (!this.hasSubscribers) return;
2165
+ const shutdownPromises = this.subscribers.map(async (subscriber) => {
2166
+ if (subscriber.shutdown) try {
2167
+ await subscriber.shutdown();
2168
+ } catch (error) {
2169
+ getLogger().error({
2170
+ err: error instanceof Error ? error : void 0,
2171
+ subscriberName: subscriber.name || "Unknown"
2172
+ }, `[Events] Failed to shutdown subscriber ${subscriber.name || "Unknown"}`);
2173
+ }
2174
+ });
2175
+ await Promise.allSettled(shutdownPromises);
2176
+ }
2177
+ /**
2178
+ * Shutdown the Event instance and all subscribers
2179
+ *
2180
+ * Unlike `flush()`, this method:
2181
+ * - Shuts down all subscribers
2182
+ * - Prevents further event tracking (hasSubscribers becomes false)
2183
+ * - Should only be called once at application shutdown
2184
+ *
2185
+ * @example
2186
+ * ```typescript
2187
+ * // In Next.js API route with after()
2188
+ * import { after } from 'next/server';
2189
+ *
2190
+ * export async function POST(req: Request) {
2191
+ * const event = new Event('checkout', { subscribers: [...] });
2192
+ * event.trackEvent('order.completed', { orderId: '123' });
2193
+ *
2194
+ * after(async () => {
2195
+ * await event.shutdown();
2196
+ * });
2197
+ *
2198
+ * return Response.json({ success: true });
2199
+ * }
2200
+ * ```
2201
+ */
2202
+ async shutdown() {
2203
+ if (!this.hasSubscribers) return;
2204
+ await Promise.allSettled(this.subscribers.map(async (subscriber) => {
2205
+ if (subscriber.shutdown) try {
2206
+ await subscriber.shutdown();
2207
+ } catch (error) {
2208
+ getLogger().error({
2209
+ err: error instanceof Error ? error : void 0,
2210
+ subscriberName: subscriber.name || "Unknown"
2211
+ }, `[Events] Failed to shutdown subscriber ${subscriber.name || "Unknown"}`);
2212
+ }
2213
+ }));
2214
+ this.hasSubscribers = false;
2215
+ }
2216
+ /**
2217
+ * Track funnel progression with custom step names
2218
+ *
2219
+ * Unlike trackFunnelStep which uses FunnelStatus enum values,
2220
+ * this method allows any string as the step name for flexible funnel tracking.
2221
+ *
2222
+ * @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
2223
+ * @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
2224
+ * @param stepNumber - Optional numeric position in the funnel
2225
+ * @param attributes - Optional event attributes
2226
+ *
2227
+ * @example
2228
+ * ```typescript
2229
+ * // Track custom checkout steps
2230
+ * event.trackFunnelProgression('checkout', 'cart_viewed', 1);
2231
+ * event.trackFunnelProgression('checkout', 'shipping_selected', 2);
2232
+ * event.trackFunnelProgression('checkout', 'payment_entered', 3);
2233
+ * event.trackFunnelProgression('checkout', 'order_confirmed', 4);
2234
+ * ```
2235
+ */
2236
+ trackFunnelProgression(funnelName, stepName, stepNumber, attributes) {
2237
+ const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
2238
+ this.logger?.info({
2239
+ funnel: funnelName,
2240
+ stepName,
2241
+ stepNumber,
2242
+ attributes: enrichedAttributes
2243
+ }, "Funnel progression tracked");
2244
+ this.collector?.recordFunnelStep({
2245
+ funnel: funnelName,
2246
+ status: stepName,
2247
+ attributes: {
2248
+ ...enrichedAttributes,
2249
+ step_name: stepName,
2250
+ ...stepNumber === void 0 ? {} : { step_number: stepNumber }
2251
+ },
2252
+ service: this.serviceName,
2253
+ timestamp: Date.now()
2254
+ });
2255
+ if (this.hasSubscribers) {
2256
+ const autotelContext = this.buildAutotelContext();
2257
+ const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
2258
+ this.notifySubscribers(async (subscriber) => {
2259
+ await (subscriber.trackFunnelProgression ? subscriber.trackFunnelProgression(funnelName, stepName, stepNumber, finalAttributes, { autotel: autotelContext }) : subscriber.trackFunnelStep(funnelName, stepName, {
2260
+ ...finalAttributes,
2261
+ step_name: stepName,
2262
+ ...stepNumber === void 0 ? {} : { step_number: stepNumber }
2263
+ }, { autotel: autotelContext }));
2264
+ });
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Track multiple events in a batch
2269
+ *
2270
+ * Useful for bulk event tracking with consistent timestamps.
2271
+ * Events are sent to subscribers individually but processed together.
2272
+ *
2273
+ * @param events - Array of events to track
2274
+ *
2275
+ * @example
2276
+ * ```typescript
2277
+ * event.trackBatch([
2278
+ * { name: 'item.viewed', attributes: { itemId: '1' } },
2279
+ * { name: 'item.viewed', attributes: { itemId: '2' } },
2280
+ * { name: 'cart.updated', attributes: { itemCount: 2 } },
2281
+ * ]);
2282
+ * ```
2283
+ */
2284
+ trackBatch(events) {
2285
+ for (const event of events) {
2286
+ const filteredAttributes = event.attributes ? Object.fromEntries(Object.entries(event.attributes).filter(([, v]) => v !== void 0 && v !== null)) : void 0;
2287
+ this.trackEvent(event.name, filteredAttributes);
2288
+ }
2289
+ }
2290
+ };
2291
+ /**
2292
+ * Global events instances (singleton pattern)
2293
+ */
2294
+ const eventsInstances = /* @__PURE__ */ new Map();
2295
+ /**
2296
+ * Get or create an Events instance for a service
2297
+ *
2298
+ * @param serviceName - Service name for identifying events
2299
+ * @param logger - Optional logger
2300
+ * @returns Events instance
2301
+ *
2302
+ * @example
2303
+ * ```typescript
2304
+ * const event =getEvents('job-application')
2305
+ * events.trackEvent('application.submitted', { jobId: '123' })
2306
+ * ```
2307
+ */
2308
+ function getEvents(serviceName, logger) {
2309
+ if (!eventsInstances.has(serviceName)) eventsInstances.set(serviceName, new Event(serviceName, { logger }));
2310
+ return eventsInstances.get(serviceName);
2311
+ }
2312
+ /**
2313
+ * Reset all events instances (mainly for testing)
2314
+ */
2315
+ function resetEvents() {
2316
+ eventsInstances.clear();
2317
+ }
2318
+
2319
+ //#endregion
2320
+ //#region src/shutdown.ts
2321
+ /**
2322
+ * Graceful shutdown with flush and cleanup
2323
+ */
2324
+ /**
2325
+ * Error codes that mean "the OTLP endpoint wasn't reachable" — expected and
2326
+ * harmless when no collector is configured. Deliberately limited to
2327
+ * connection-establishment failures (refused / DNS), not post-connection
2328
+ * errors like ECONNRESET or ETIMEDOUT, which can indicate a real problem
2329
+ * talking to a configured backend and should surface.
2330
+ */
2331
+ const UNREACHABLE_ENDPOINT_CODES = /* @__PURE__ */ new Set([
2332
+ "ECONNREFUSED",
2333
+ "ENOTFOUND",
2334
+ "EAI_AGAIN"
2335
+ ]);
2336
+ function errorCode(error) {
2337
+ if (typeof error === "object" && error !== null && "code" in error) {
2338
+ const code = error.code;
2339
+ if (typeof code === "string") return code;
2340
+ }
2341
+ }
2342
+ /**
2343
+ * True when the error (or every error it wraps) is an unreachable-endpoint
2344
+ * failure. Traverses `AggregateError.errors` and the `cause` chain, since the
2345
+ * SDK often wraps the underlying network error.
2346
+ */
2347
+ function isUnreachableEndpointError(error, depth = 0) {
2348
+ if (depth > 5 || error === null || typeof error !== "object") return false;
2349
+ if (error instanceof AggregateError) return error.errors.length > 0 && error.errors.every((e) => isUnreachableEndpointError(e, depth + 1));
2350
+ const code = errorCode(error);
2351
+ if (code && UNREACHABLE_ENDPOINT_CODES.has(code)) return true;
2352
+ const cause = error.cause;
2353
+ return cause !== void 0 && cause !== error ? isUnreachableEndpointError(cause, depth + 1) : false;
2354
+ }
2355
+ /**
2356
+ * Flush all pending telemetry
2357
+ *
2358
+ * Flushes both events events and OpenTelemetry spans to their destinations.
2359
+ * Includes timeout protection to prevent hanging in serverless environments.
2360
+ *
2361
+ * Safe to call multiple times.
2362
+ *
2363
+ * @param options - Optional configuration
2364
+ * @param options.timeout - Timeout in milliseconds (default: 2000ms)
2365
+ * @param options.forShutdown - If true, permanently disables the events queue after flush (used internally by shutdown())
2366
+ *
2367
+ * @example Manual flush in serverless
2368
+ * ```typescript
2369
+ * import { flush } from 'autotel';
2370
+ *
2371
+ * export const handler = async (event) => {
2372
+ * // ... process event
2373
+ * await flush(); // Flush before function returns
2374
+ * return result;
2375
+ * };
2376
+ * ```
2377
+ *
2378
+ * @example With custom timeout
2379
+ * ```typescript
2380
+ * await flush({ timeout: 5000 }); // 5 second timeout
2381
+ * ```
2382
+ */
2383
+ async function flush(options) {
2384
+ const timeout = options?.timeout ?? 2e3;
2385
+ const forShutdown = options?.forShutdown ?? false;
2386
+ const doFlush = async () => {
2387
+ const eventsQueue = getEventQueue();
2388
+ if (eventsQueue) await (forShutdown ? eventsQueue.shutdown() : eventsQueue.flush());
2389
+ try {
2390
+ const tracerProvider = getForceFlushableProvider(getSdk());
2391
+ if (tracerProvider) await tracerProvider.forceFlush();
2392
+ } catch {}
2393
+ };
2394
+ let timeoutHandle;
2395
+ try {
2396
+ await Promise.race([doFlush().finally(() => {
2397
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2398
+ }), new Promise((_, reject) => {
2399
+ timeoutHandle = setTimeout(() => reject(/* @__PURE__ */ new Error("Flush timeout")), timeout);
2400
+ timeoutHandle.unref();
2401
+ })]);
2402
+ } catch (error) {
2403
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2404
+ getLogger().error({ err: error instanceof Error ? error : new Error(String(error)) }, "[autotel] Flush error");
2405
+ throw error;
2406
+ }
2407
+ }
2408
+ /**
2409
+ * Shutdown telemetry and cleanup resources
2410
+ *
2411
+ * - Flushes all pending data
2412
+ * - Shuts down OpenTelemetry SDK
2413
+ * - Cleans up resources
2414
+ *
2415
+ * Call this before process exit.
2416
+ *
2417
+ * Always performs cleanup even if flush fails, preventing resource leaks
2418
+ * in serverless handlers or tests.
2419
+ *
2420
+ * @example Express server
2421
+ * ```typescript
2422
+ * const server = app.listen(3000)
2423
+ *
2424
+ * process.on('SIGTERM', async () => {
2425
+ * await server.close()
2426
+ * await shutdown()
2427
+ * process.exit(0)
2428
+ * })
2429
+ * ```
2430
+ */
2431
+ async function shutdown() {
2432
+ const logger = getLogger();
2433
+ let shutdownError = null;
2434
+ try {
2435
+ await flush({ forShutdown: true });
2436
+ } catch (error) {
2437
+ const err = error instanceof Error ? error : new Error(String(error));
2438
+ shutdownError = err;
2439
+ logger.error({ err }, "[autotel] Flush failed during shutdown, continuing cleanup");
2440
+ }
2441
+ try {
2442
+ const sdk = getSdk();
2443
+ if (sdk) await sdk.shutdown();
2444
+ } catch (error) {
2445
+ const err = error instanceof Error ? error : new Error(String(error));
2446
+ if (!isUnreachableEndpointError(error)) {
2447
+ if (!shutdownError) shutdownError = err;
2448
+ logger.error({ err }, "[autotel] SDK shutdown failed");
2449
+ }
2450
+ } finally {
2451
+ uninstallProcessHandlers();
2452
+ await _closeEmbeddedDevtools();
2453
+ const eventsQueue = getEventQueue();
2454
+ if (eventsQueue && typeof eventsQueue.cleanup === "function") eventsQueue.cleanup();
2455
+ resetEvents();
2456
+ resetMetrics();
2457
+ resetEventQueue();
2458
+ }
2459
+ if (shutdownError) throw shutdownError;
2460
+ }
2461
+
2462
+ //#endregion
2463
+ //#region src/init.ts
2464
+ /**
2465
+ * Simplified initialization for autotel
2466
+ *
2467
+ * Single init() function with sensible defaults.
2468
+ * Replaces initInstrumentation() and separate events config.
2469
+ */
2470
+ /**
2471
+ * Silent logger (no-op) - used as default when user doesn't provide one.
2472
+ * Internal autotel logs are silent by default to avoid spam.
2473
+ * Users can import { autotelLogger } from 'autotel/logger' to create their own.
2474
+ */
2475
+ const silentLogger = {
2476
+ info: () => {},
2477
+ warn: () => {},
2478
+ error: () => {},
2479
+ debug: () => {}
2480
+ };
2481
+ /**
2482
+ * Adapts an Autotel Sampler to the OTel SDK Sampler interface.
2483
+ */
2484
+ function toOtelSampler(sampler) {
2485
+ return {
2486
+ shouldSample(_context, _traceId, spanName, _spanKind, _attributes, links) {
2487
+ const samplingContext = {
2488
+ operationName: spanName,
2489
+ args: [],
2490
+ links
2491
+ };
2492
+ const shouldTrace = sampler.shouldSample(samplingContext);
2493
+ const rate = sampler.sampleRate?.(samplingContext);
2494
+ return {
2495
+ decision: shouldTrace ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,
2496
+ ...shouldTrace && rate !== void 0 && rate > 1 ? { attributes: { [AUTOTEL_SAMPLING_RATE]: rate } } : {}
2497
+ };
2498
+ },
2499
+ toString() {
2500
+ return `AutotelSamplerAdapter`;
2501
+ }
2502
+ };
2503
+ }
2504
+ let OTLPTraceExporterGRPC;
2505
+ let OTLPMetricExporterGRPC;
2506
+ let OTLPLogExporterGRPC;
2507
+ /**
2508
+ * Helper: Lazy-load gRPC trace exporter
2509
+ */
2510
+ function loadGRPCTraceExporter() {
2511
+ if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
2512
+ try {
2513
+ OTLPTraceExporterGRPC = requireModule("@opentelemetry/exporter-trace-otlp-grpc").OTLPTraceExporter;
2514
+ return OTLPTraceExporterGRPC;
2515
+ } catch {
2516
+ throw new Error("gRPC trace exporter not found. Install @opentelemetry/exporter-trace-otlp-grpc");
2517
+ }
2518
+ }
2519
+ /**
2520
+ * Helper: Lazy-load gRPC metric exporter
2521
+ */
2522
+ function loadGRPCMetricExporter() {
2523
+ if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
2524
+ try {
2525
+ OTLPMetricExporterGRPC = requireModule("@opentelemetry/exporter-metrics-otlp-grpc").OTLPMetricExporter;
2526
+ return OTLPMetricExporterGRPC;
2527
+ } catch {
2528
+ throw new Error("gRPC metric exporter not found. Install @opentelemetry/exporter-metrics-otlp-grpc");
2529
+ }
2530
+ }
2531
+ /**
2532
+ * Helper: Create trace exporter based on protocol
2533
+ */
2534
+ function createTraceExporter(protocol, config) {
2535
+ if (protocol === "grpc") return new (loadGRPCTraceExporter())(config);
2536
+ return new OTLPTraceExporter(config);
2537
+ }
2538
+ /**
2539
+ * Helper: Create metric exporter based on protocol
2540
+ */
2541
+ function createMetricExporter(protocol, config) {
2542
+ if (protocol === "grpc") return new (loadGRPCMetricExporter())(config);
2543
+ return new OTLPMetricExporter(config);
2544
+ }
2545
+ /**
2546
+ * Helper: Lazy-load gRPC log exporter
2547
+ */
2548
+ function loadGRPCLogExporter() {
2549
+ if (OTLPLogExporterGRPC) return OTLPLogExporterGRPC;
2550
+ try {
2551
+ OTLPLogExporterGRPC = requireModule("@opentelemetry/exporter-logs-otlp-grpc").OTLPLogExporter;
2552
+ return OTLPLogExporterGRPC;
2553
+ } catch {
2554
+ throw new Error("gRPC log exporter not found. Install @opentelemetry/exporter-logs-otlp-grpc");
2555
+ }
2556
+ }
2557
+ /**
2558
+ * Helper: Create log exporter based on protocol
2559
+ */
2560
+ function createLogExporter(protocol, config) {
2561
+ if (protocol === "grpc") return new (loadGRPCLogExporter())(config);
2562
+ return new OTLPLogExporter(config);
2563
+ }
2564
+ /**
2565
+ * Helper: Resolve protocol from config and environment
2566
+ */
2567
+ function resolveProtocol(configProtocol) {
2568
+ if (configProtocol === "grpc" || configProtocol === "http") return configProtocol;
2569
+ const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
2570
+ if (envProtocol === "grpc") return "grpc";
2571
+ if (envProtocol === "http/protobuf" || envProtocol === "http") return "http";
2572
+ return "http";
2573
+ }
2574
+ /**
2575
+ * Helper: Adjust endpoint URL for protocol
2576
+ * gRPC exporters don't need the /v1/traces or /v1/metrics path
2577
+ * HTTP exporters need the full path
2578
+ */
2579
+ function formatEndpointUrl(endpoint, signal, protocol) {
2580
+ if (protocol === "grpc") return endpoint.replace(/\/(v1\/)?(traces|metrics|logs)$/, "");
2581
+ if (!endpoint.endsWith(`/v1/${signal}`)) return `${endpoint}/v1/${signal}`;
2582
+ return endpoint;
2583
+ }
2584
+ let initialized = false;
2585
+ let locked = false;
2586
+ let config = null;
2587
+ let sdk = null;
2588
+ let warnedOnce = false;
2589
+ let logger = silentLogger;
2590
+ let validationConfig = null;
2591
+ let eventsConfig = null;
2592
+ let _stringRedactor = null;
2593
+ function acceptsStringRedactor(subscriber) {
2594
+ return typeof subscriber === "object" && subscriber !== null && "setStringRedactor" in subscriber && typeof subscriber.setStringRedactor === "function";
2595
+ }
2596
+ let _optionalRequire = safeRequire;
2597
+ let _devtoolsClose = null;
2598
+ const LOG_LEVELS = {
2599
+ debug: 0,
2600
+ info: 1,
2601
+ warn: 2,
2602
+ error: 3
2603
+ };
2604
+ /**
2605
+ * Lock the logger to prevent further `init()` calls.
2606
+ * Use this when framework plugins set up instrumentation and you want
2607
+ * to prevent accidental re-initialization from user code.
2608
+ */
2609
+ function lockLogger() {
2610
+ locked = true;
2611
+ }
2612
+ /**
2613
+ * Check if the logger has been locked.
2614
+ */
2615
+ function isLoggerLocked() {
2616
+ return locked;
2617
+ }
2618
+ function createSilentLogger() {
2619
+ return {
2620
+ info: () => {},
2621
+ warn: () => {},
2622
+ error: () => {},
2623
+ debug: () => {}
2624
+ };
2625
+ }
2626
+ function wrapLogger(base, silent, minLevel) {
2627
+ if (silent) return createSilentLogger();
2628
+ const threshold = LOG_LEVELS[minLevel];
2629
+ const wrap = (fn, level) => {
2630
+ if (LOG_LEVELS[level] < threshold) return (() => {});
2631
+ return ((...args) => fn(...args));
2632
+ };
2633
+ return {
2634
+ debug: wrap(base.debug, "debug"),
2635
+ info: wrap(base.info, "info"),
2636
+ warn: wrap(base.warn, "warn"),
2637
+ error: wrap(base.error, "error")
2638
+ };
2639
+ }
2640
+ /**
2641
+ * Resolve the effective attribute redactor. Explicit config wins (`false`
2642
+ * disables). Otherwise the `AUTOTEL_REDACT_PII` env var controls it, and as a
2643
+ * final default PII redaction is auto-enabled in production.
2644
+ */
2645
+ function resolveAttributeRedactor(explicit, environment) {
2646
+ if (explicit === false) return void 0;
2647
+ if (explicit !== void 0) return explicit;
2648
+ const flag = process.env.AUTOTEL_REDACT_PII?.trim().toLowerCase();
2649
+ if (flag) {
2650
+ if ([
2651
+ "off",
2652
+ "false",
2653
+ "0",
2654
+ "none",
2655
+ "disabled"
2656
+ ].includes(flag)) return;
2657
+ if (flag === "default" || flag === "strict" || flag === "pci-dss") return flag;
2658
+ if ([
2659
+ "on",
2660
+ "true",
2661
+ "1",
2662
+ "enabled"
2663
+ ].includes(flag)) return "default";
2664
+ }
2665
+ return environment === "production" ? "default" : void 0;
2666
+ }
2667
+ function detectEnvironmentAttributes() {
2668
+ const attrs = {};
2669
+ const commitSha = process.env.COMMIT_SHA || process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CF_PAGES_COMMIT_SHA || process.env.AWS_CODEPIPELINE_EXECUTION_ID;
2670
+ if (commitSha) attrs["service.commit.sha"] = commitSha;
2671
+ const region = process.env.VERCEL_REGION || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || process.env.FLY_REGION || process.env.CF_REGION || process.env.GOOGLE_CLOUD_REGION;
2672
+ if (region) attrs["service.region"] = region;
2673
+ const version = process.env.APP_VERSION || process.env.HEROKU_RELEASE_VERSION || process.env.VERCEL_GIT_COMMIT_REF;
2674
+ if (version) attrs["service.deploy.version"] = version;
2675
+ return attrs;
2676
+ }
2677
+ /**
2678
+ * Resolve metrics flag with env var override support
2679
+ */
2680
+ function resolveMetricsFlag(configFlag = "auto") {
2681
+ const envFlag = process.env.AUTOTEL_METRICS;
2682
+ if (envFlag === "on" || envFlag === "true") return true;
2683
+ if (envFlag === "off" || envFlag === "false") return false;
2684
+ if (configFlag === true) return true;
2685
+ if (configFlag === false) return false;
2686
+ return true;
2687
+ }
2688
+ /**
2689
+ * Resolve logs flag with env var override support.
2690
+ * Defaults to disabled (opt-in only) to avoid unexpected log export
2691
+ * and to preserve the upstream SDK's OTEL_LOGS_EXPORTER handling.
2692
+ */
2693
+ function resolveLogsFlag(configFlag = "auto") {
2694
+ const envFlag = process.env.AUTOTEL_LOGS;
2695
+ if (envFlag === "on" || envFlag === "true") return true;
2696
+ if (envFlag === "off" || envFlag === "false") return false;
2697
+ if (configFlag === true) return true;
2698
+ if (configFlag === false) return false;
2699
+ return false;
2700
+ }
2701
+ /**
2702
+ * Resolve debug flag with env var override support
2703
+ *
2704
+ * Supports:
2705
+ * - `'pretty'`: Colorized, hierarchical output (PrettyConsoleExporter)
2706
+ * - `true` / `'true'` / `'1'`: Raw JSON output (ConsoleSpanExporter)
2707
+ * - `false` / `'false'` / `'0'`: Disabled
2708
+ */
2709
+ function resolveDebugFlag(configFlag) {
2710
+ const envFlag = process.env.AUTOTEL_DEBUG;
2711
+ if (envFlag === "pretty") return "pretty";
2712
+ if (envFlag === "true" || envFlag === "1") return true;
2713
+ if (envFlag === "false" || envFlag === "0") return false;
2714
+ return configFlag ?? false;
2715
+ }
2716
+ function normalizeOtlpHeaders(headers) {
2717
+ if (!headers) return void 0;
2718
+ if (typeof headers !== "string") return headers;
2719
+ const parsed = {};
2720
+ for (const pair of headers.split(",")) {
2721
+ const [key, ...valueParts] = pair.split("=");
2722
+ if (!key || valueParts.length === 0) continue;
2723
+ parsed[key.trim()] = valueParts.join("=").trim();
2724
+ }
2725
+ return parsed;
2726
+ }
2727
+ function resolveOtlpDestinations(config, fallbackEndpoint) {
2728
+ return (config.destinations === void 0 ? fallbackEndpoint ? [{
2729
+ endpoint: fallbackEndpoint,
2730
+ headers: config.headers,
2731
+ protocol: config.protocol
2732
+ }] : [] : config.destinations).map((destination) => ({
2733
+ endpoint: destination.endpoint,
2734
+ protocol: resolveProtocol(destination.protocol ?? config.protocol),
2735
+ headers: normalizeOtlpHeaders(destination.headers ?? config.headers),
2736
+ signals: destination.signals ? new Set(destination.signals) : void 0
2737
+ }));
2738
+ }
2739
+ function destinationSupportsSignal(destination, signal) {
2740
+ return destination.signals ? destination.signals.has(signal) : true;
2741
+ }
2742
+ /**
2743
+ * Initialize autotel - Write Once, Observe Everywhere
2744
+ *
2745
+ * Follows OpenTelemetry standards: opinionated defaults with full flexibility
2746
+ * Idempotent: multiple calls are safe, last one wins
2747
+ *
2748
+ * @example Minimal setup (OTLP default)
2749
+ * ```typescript
2750
+ * init({ service: 'my-app' })
2751
+ * ```
2752
+ *
2753
+ * @example With events (observe in PostHog, Mixpanel, etc.)
2754
+ * ```typescript
2755
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
2756
+ *
2757
+ * init({
2758
+ * service: 'my-app',
2759
+ * subscribers: [new PostHogSubscriber({ apiKey: '...' })]
2760
+ * })
2761
+ * ```
2762
+ *
2763
+ * @example Observe in Jaeger
2764
+ * ```typescript
2765
+ * import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
2766
+ *
2767
+ * init({
2768
+ * service: 'my-app',
2769
+ * spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
2770
+ * })
2771
+ * ```
2772
+ *
2773
+ * @example Observe in Zipkin
2774
+ * ```typescript
2775
+ * import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
2776
+ *
2777
+ * init({
2778
+ * service: 'my-app',
2779
+ * spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
2780
+ * })
2781
+ * ```
2782
+ *
2783
+ * @example Observe in Datadog
2784
+ * ```typescript
2785
+ * import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
2786
+ *
2787
+ * init({
2788
+ * service: 'my-app',
2789
+ * spanProcessor: new DatadogSpanProcessor({ ... })
2790
+ * })
2791
+ * ```
2792
+ *
2793
+ * @example Console output (dev)
2794
+ * ```typescript
2795
+ * import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
2796
+ *
2797
+ * init({
2798
+ * service: 'my-app',
2799
+ * spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
2800
+ * })
2801
+ * ```
2802
+ */
2803
+ function init(cfg) {
2804
+ if (locked) return;
2805
+ const envConfig = resolveConfigFromEnv();
2806
+ const yamlConfig = loadYamlConfig() ?? {};
2807
+ const mergedConfig = {
2808
+ ...envConfig,
2809
+ ...yamlConfig,
2810
+ ...cfg,
2811
+ resourceAttributes: {
2812
+ ...envConfig.resourceAttributes,
2813
+ ...yamlConfig.resourceAttributes,
2814
+ ...detectEnvironmentAttributes(),
2815
+ ...cfg.resourceAttributes
2816
+ },
2817
+ headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers
2818
+ };
2819
+ const resolvedRedactor = resolveAttributeRedactor(mergedConfig.attributeRedactor, mergedConfig.environment || process.env.NODE_ENV || "development");
2820
+ if (resolvedRedactor === void 0) mergedConfig.attributeRedactor = void 0;
2821
+ else {
2822
+ const normalizedRedactor = normalizeAttributeRedactorConfig(resolvedRedactor);
2823
+ if (!normalizedRedactor) throw new Error("Invalid attributeRedactor config");
2824
+ mergedConfig.attributeRedactor = normalizedRedactor;
2825
+ }
2826
+ const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
2827
+ if (devtoolsConfig.enabled && mergedConfig.logs === void 0) mergedConfig.logs = true;
2828
+ const silent = mergedConfig.silent ?? false;
2829
+ const minLevel = mergedConfig.minLevel ?? "info";
2830
+ logger = wrapLogger(mergedConfig.logger || silentLogger, silent, minLevel);
2831
+ if (initialized) logger.warn({}, "[autotel] init() called again - last config wins. This may cause unexpected behavior.");
2832
+ config = mergedConfig;
2833
+ validationConfig = mergedConfig.validation || null;
2834
+ eventsConfig = mergedConfig.events || null;
2835
+ let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
2836
+ const version = mergedConfig.version || detectVersion();
2837
+ const environment = mergedConfig.environment || process.env.NODE_ENV || "development";
2838
+ const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
2839
+ const logsEnabled = resolveLogsFlag(mergedConfig.logs);
2840
+ if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
2841
+ const devtoolsModule = _optionalRequire("autotel-devtools");
2842
+ if (devtoolsModule?.createDevtools) {
2843
+ const devtoolsInstance = devtoolsModule.createDevtools({
2844
+ port: devtoolsConfig.port,
2845
+ host: devtoolsConfig.host,
2846
+ verbose: devtoolsConfig.verbose
2847
+ });
2848
+ _devtoolsClose = devtoolsInstance.close;
2849
+ endpoint = `http://${devtoolsConfig.host}:${devtoolsInstance.port}`;
2850
+ logger.info({}, `[autotel] autotel-devtools embedded server started at ${endpoint}`);
2851
+ } else logger.warn({}, "[autotel] devtools.embedded requested but autotel-devtools is not installed. Falling back to endpoint-only mode.");
2852
+ }
2853
+ const hostname = detectHostname();
2854
+ let resource = resourceFromAttributes({
2855
+ [ATTR_SERVICE_NAME]: mergedConfig.service,
2856
+ [ATTR_SERVICE_VERSION]: version,
2857
+ "deployment.environment": environment,
2858
+ "deployment.environment.name": environment
2859
+ });
2860
+ if (hostname) resource = resource.merge(resourceFromAttributes({
2861
+ "host.name": hostname,
2862
+ "datadog.host.name": hostname
2863
+ }));
2864
+ if (mergedConfig.resource) resource = resource.merge(mergedConfig.resource);
2865
+ if (mergedConfig.resourceAttributes) resource = resource.merge(resourceFromAttributes(mergedConfig.resourceAttributes));
2866
+ const otlpDestinations = resolveOtlpDestinations(mergedConfig, endpoint);
2867
+ const configuredSpanProcessors = mergedConfig.spanProcessors && mergedConfig.spanProcessors.length > 0 ? mergedConfig.spanProcessors : mergedConfig.spanProcessor ? [mergedConfig.spanProcessor] : void 0;
2868
+ const configuredSpanExporters = mergedConfig.spanExporters && mergedConfig.spanExporters.length > 0 ? mergedConfig.spanExporters : mergedConfig.spanExporter ? [mergedConfig.spanExporter] : void 0;
2869
+ const configuredMetricReaders = mergedConfig.metricReaders && mergedConfig.metricReaders.length > 0 ? mergedConfig.metricReaders : mergedConfig.metricReader ? [mergedConfig.metricReader] : void 0;
2870
+ const configuredLogRecordProcessors = mergedConfig.logRecordProcessors && mergedConfig.logRecordProcessors.length > 0 ? mergedConfig.logRecordProcessors : mergedConfig.logRecordProcessor ? [mergedConfig.logRecordProcessor] : void 0;
2871
+ let spanProcessors = [];
2872
+ if (configuredSpanProcessors && configuredSpanProcessors.length > 0) spanProcessors.push(...configuredSpanProcessors);
2873
+ else if (configuredSpanExporters && configuredSpanExporters.length > 0) for (const exporter of configuredSpanExporters) spanProcessors.push(new TailSamplingSpanProcessor(new BatchSpanProcessor(exporter)));
2874
+ else for (const destination of otlpDestinations) {
2875
+ if (!destinationSupportsSignal(destination, "traces")) continue;
2876
+ const traceExporter = createTraceExporter(destination.protocol, {
2877
+ url: formatEndpointUrl(destination.endpoint, "traces", destination.protocol),
2878
+ headers: destination.headers
2879
+ });
2880
+ spanProcessors.push(new TailSamplingSpanProcessor(new BatchSpanProcessor(traceExporter)));
2881
+ }
2882
+ if (mergedConfig.baggage) {
2883
+ const prefix = typeof mergedConfig.baggage === "string" ? mergedConfig.baggage ? `${mergedConfig.baggage}.` : "" : "baggage.";
2884
+ spanProcessors.push(new BaggageSpanProcessor({ prefix }));
2885
+ }
2886
+ const debugMode = resolveDebugFlag(mergedConfig.debug);
2887
+ if (debugMode === "pretty") spanProcessors.push(new SimpleSpanProcessor(new PrettyConsoleExporter()));
2888
+ else if (debugMode === true) spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
2889
+ if (mergedConfig.canonicalLogLines?.enabled) {
2890
+ const canonicalOptions = {
2891
+ logger: mergedConfig.canonicalLogLines.logger || mergedConfig.logger,
2892
+ rootSpansOnly: mergedConfig.canonicalLogLines.rootSpansOnly,
2893
+ minLevel: mergedConfig.canonicalLogLines.minLevel,
2894
+ messageFormat: mergedConfig.canonicalLogLines.messageFormat,
2895
+ includeResourceAttributes: mergedConfig.canonicalLogLines.includeResourceAttributes,
2896
+ shouldEmit: mergedConfig.canonicalLogLines.shouldEmit,
2897
+ keep: mergedConfig.canonicalLogLines.keep,
2898
+ drain: mergedConfig.canonicalLogLines.drain,
2899
+ onDrainError: mergedConfig.canonicalLogLines.onDrainError,
2900
+ pretty: mergedConfig.canonicalLogLines.pretty
2901
+ };
2902
+ spanProcessors.push(new CanonicalLogLineProcessor(canonicalOptions));
2903
+ }
2904
+ if (mergedConfig.attributeRedactor && spanProcessors.length > 0) {
2905
+ const redactor = mergedConfig.attributeRedactor;
2906
+ spanProcessors = spanProcessors.map((processor) => new AttributeRedactingProcessor(processor, { redactor }));
2907
+ }
2908
+ if (mergedConfig.attributeRedactor) _stringRedactor = createStringRedactor(mergedConfig.attributeRedactor);
2909
+ if (_stringRedactor && mergedConfig.subscribers) {
2910
+ for (const subscriber of mergedConfig.subscribers) if (acceptsStringRedactor(subscriber)) subscriber.setStringRedactor(_stringRedactor);
2911
+ }
2912
+ if (mergedConfig.spanNameNormalizer && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new SpanNameNormalizingProcessor(processor, { normalizer: mergedConfig.spanNameNormalizer }));
2913
+ if (mergedConfig.spanFilter && spanProcessors.length > 0) spanProcessors = spanProcessors.map((processor) => new FilteringSpanProcessor(processor, { filter: mergedConfig.spanFilter }));
2914
+ const metricReaders = [];
2915
+ if (configuredMetricReaders && configuredMetricReaders.length > 0) metricReaders.push(...configuredMetricReaders);
2916
+ else if (metricsEnabled) for (const destination of otlpDestinations) {
2917
+ if (!destinationSupportsSignal(destination, "metrics")) continue;
2918
+ const metricExporter = createMetricExporter(destination.protocol, {
2919
+ url: formatEndpointUrl(destination.endpoint, "metrics", destination.protocol),
2920
+ headers: destination.headers
2921
+ });
2922
+ metricReaders.push(new PeriodicExportingMetricReader({ exporter: metricExporter }));
2923
+ }
2924
+ let logRecordProcessors;
2925
+ if (configuredLogRecordProcessors && configuredLogRecordProcessors.length > 0) logRecordProcessors = [...configuredLogRecordProcessors];
2926
+ if (logsEnabled) {
2927
+ for (const destination of otlpDestinations) {
2928
+ if (!destinationSupportsSignal(destination, "logs")) continue;
2929
+ let processor = new BatchLogRecordProcessor({ exporter: createLogExporter(destination.protocol, {
2930
+ url: formatEndpointUrl(destination.endpoint, "logs", destination.protocol),
2931
+ headers: destination.headers
2932
+ }) });
2933
+ if (_stringRedactor) processor = new RedactingLogRecordProcessor(processor, _stringRedactor);
2934
+ if (!logRecordProcessors) logRecordProcessors = [];
2935
+ logRecordProcessors.push(processor);
2936
+ }
2937
+ if (otlpDestinations.some((destination) => destinationSupportsSignal(destination, "logs"))) logger.info({}, "[autotel] OTLP log exporter configured");
2938
+ }
2939
+ const posthogProcessors = buildPostHogLogProcessors(mergedConfig.posthog, _stringRedactor);
2940
+ if (posthogProcessors.length > 0) {
2941
+ if (!logRecordProcessors) logRecordProcessors = [];
2942
+ logRecordProcessors.push(...posthogProcessors);
2943
+ logger.info({}, "[autotel] PostHog OTLP logs configured");
2944
+ }
2945
+ let finalInstrumentations = mergedConfig.instrumentations ? [...mergedConfig.instrumentations] : [];
2946
+ if (mergedConfig.autoInstrumentations !== void 0 && mergedConfig.autoInstrumentations !== false) {
2947
+ if (isESMMode()) logger.info({}, "[autotel] ESM mode detected. For auto-instrumentation to work:\n 1. Install @opentelemetry/auto-instrumentations-node as a direct dependency\n 2. Import autotel/register FIRST in your instrumentation file\n 3. Use getNodeAutoInstrumentations() directly instead of autoInstrumentations\n See: https://github.com/jagreehal/autotel#esm-setup");
2948
+ try {
2949
+ const manualInstrumentationNames = getInstrumentationNames(mergedConfig.instrumentations ?? []);
2950
+ if (manualInstrumentationNames.size > 0) {
2951
+ const manualNames = [...manualInstrumentationNames].join(", ");
2952
+ logger.info({}, `[autotel] Detected manual instrumentations (${manualNames}). These will take precedence over auto-instrumentations. Tip: Set autoInstrumentations:false if you want full manual control, or remove manual configs to use auto-instrumentations.`);
2953
+ }
2954
+ const autoInstrumentations = getAutoInstrumentations(mergedConfig.autoInstrumentations, manualInstrumentationNames);
2955
+ if (autoInstrumentations && autoInstrumentations.length > 0) finalInstrumentations = [...finalInstrumentations, ...autoInstrumentations];
2956
+ } catch (error) {
2957
+ logger.warn({}, `[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`);
2958
+ }
2959
+ }
2960
+ const autotelSampler = mergedConfig.sampler ?? (mergedConfig.sampling ? resolveSamplingPreset(mergedConfig.sampling) : void 0);
2961
+ if (autotelSampler) mergedConfig.sampler = autotelSampler;
2962
+ const sampler = autotelSampler ? toOtelSampler(autotelSampler) : envConfig.otelSampler ?? toOtelSampler(samplingPresets.production());
2963
+ const sdkOptions = {
2964
+ resource,
2965
+ serviceName: mergedConfig.service,
2966
+ sampler,
2967
+ instrumentations: finalInstrumentations
2968
+ };
2969
+ if (spanProcessors.length > 0) sdkOptions.spanProcessors = spanProcessors;
2970
+ if (metricReaders.length > 0) sdkOptions.metricReaders = metricReaders;
2971
+ if (logRecordProcessors && logRecordProcessors.length > 0) sdkOptions.logRecordProcessors = logRecordProcessors;
2972
+ sdk = mergedConfig.sdkFactory ? mergedConfig.sdkFactory(sdkOptions) : new NodeSDK(sdkOptions);
2973
+ if (!sdk) throw new Error("[autotel] sdkFactory must return a NodeSDK instance");
2974
+ sdk.start();
2975
+ if (mergedConfig.openllmetry?.enabled) {
2976
+ const traceloop = _optionalRequire("@traceloop/node-server-sdk");
2977
+ if (traceloop) {
2978
+ const initOptions = { ...mergedConfig.openllmetry.options };
2979
+ try {
2980
+ initOptions.tracerProvider = sdk.getTracerProvider();
2981
+ } catch {}
2982
+ if (configuredSpanExporters?.[0]) initOptions.exporter = configuredSpanExporters[0];
2983
+ if (typeof traceloop.initialize === "function") {
2984
+ traceloop.initialize(initOptions);
2985
+ logger.info({}, "[autotel] OpenLLMetry initialized successfully");
2986
+ } else logger.warn({}, "[autotel] OpenLLMetry initialize function not found. Check @traceloop/node-server-sdk version.");
2987
+ } else logger.warn({}, "[autotel] OpenLLMetry enabled but @traceloop/node-server-sdk is not installed. Install it as a peer dependency to use OpenLLMetry integration.");
2988
+ }
2989
+ initialized = true;
2990
+ const processHandlers = mergedConfig.processHandlers;
2991
+ if (processHandlers) installProcessHandlers(processHandlers === true ? {} : processHandlers, shutdown);
2992
+ else uninstallProcessHandlers();
2993
+ }
2994
+ /**
2995
+ * Extract instrumentation class names from instrumentation instances
2996
+ * Used to detect duplicates between manual and auto instrumentations
2997
+ */
2998
+ function getInstrumentationNames(instrumentations) {
2999
+ const names = /* @__PURE__ */ new Set();
3000
+ if (!instrumentations) return names;
3001
+ for (const instrumentation of instrumentations) if (instrumentation && typeof instrumentation === "object") names.add(instrumentation.constructor.name);
3002
+ return names;
3003
+ }
3004
+ /**
3005
+ * Map common instrumentation class names to their package names
3006
+ * Used to disable auto-instrumentations when user provides manual configs
3007
+ */
3008
+ const INSTRUMENTATION_CLASS_TO_PACKAGE = {
3009
+ HttpInstrumentation: "@opentelemetry/instrumentation-http",
3010
+ HttpsInstrumentation: "@opentelemetry/instrumentation-http",
3011
+ ExpressInstrumentation: "@opentelemetry/instrumentation-express",
3012
+ FastifyInstrumentation: "@opentelemetry/instrumentation-fastify",
3013
+ MongoDBInstrumentation: "@opentelemetry/instrumentation-mongodb",
3014
+ MongooseInstrumentation: "@opentelemetry/instrumentation-mongoose",
3015
+ PrismaInstrumentation: "@opentelemetry/instrumentation-prisma",
3016
+ PinoInstrumentation: "@opentelemetry/instrumentation-pino",
3017
+ WinstonInstrumentation: "@opentelemetry/instrumentation-winston",
3018
+ RedisInstrumentation: "@opentelemetry/instrumentation-redis",
3019
+ GraphQLInstrumentation: "@opentelemetry/instrumentation-graphql",
3020
+ GrpcInstrumentation: "@opentelemetry/instrumentation-grpc",
3021
+ IORedisInstrumentation: "@opentelemetry/instrumentation-ioredis",
3022
+ KnexInstrumentation: "@opentelemetry/instrumentation-knex",
3023
+ NestJsInstrumentation: "@opentelemetry/instrumentation-nestjs-core",
3024
+ PgInstrumentation: "@opentelemetry/instrumentation-pg",
3025
+ MySQLInstrumentation: "@opentelemetry/instrumentation-mysql",
3026
+ MySQL2Instrumentation: "@opentelemetry/instrumentation-mysql2"
3027
+ };
3028
+ /**
3029
+ * Detect if we're running in ESM mode
3030
+ */
3031
+ function isESMMode() {
3032
+ try {
3033
+ const fs = requireModule("node:fs");
3034
+ try {
3035
+ return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).type === "module";
3036
+ } catch {
3037
+ return false;
3038
+ }
3039
+ } catch {
3040
+ return false;
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Lazy-load auto-instrumentations (optional peer dependency)
3045
+ * Only loads when integrations config is truthy, avoiding ~40+ package imports at startup.
3046
+ */
3047
+ function loadNodeAutoInstrumentations() {
3048
+ try {
3049
+ return requireModule("@opentelemetry/auto-instrumentations-node").getNodeAutoInstrumentations;
3050
+ } catch {
3051
+ const isESM = isESMMode();
3052
+ const baseMessage = "@opentelemetry/auto-instrumentations-node not found.";
3053
+ if (isESM) throw new Error(`${baseMessage}\n\nESM Setup Required:
3054
+ 1. Install as a direct dependency: pnpm add @opentelemetry/auto-instrumentations-node
3055
+ 2. Create instrumentation.mjs with:
3056
+ import 'autotel/register'; // MUST be first!
3057
+ import { init } from 'autotel';
3058
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
3059
+ init({ service: "my-app", instrumentations: getNodeAutoInstrumentations() });
3060
+ 3. Run with: tsx --import ./instrumentation.mjs src/index.ts
3061
+
3062
+ See: https://github.com/jagreehal/autotel#esm-setup`);
3063
+ throw new Error(`${baseMessage} Install it: pnpm add @opentelemetry/auto-instrumentations-node`);
3064
+ }
3065
+ }
3066
+ /**
3067
+ * Injectable loader for testing. Set to override the default loader.
3068
+ * @internal
3069
+ */
3070
+ let _autoInstrumentationsLoader = null;
3071
+ /**
3072
+ * Get auto-instrumentations based on simple integration names
3073
+ * Excludes instrumentations that are manually provided to avoid conflicts
3074
+ */
3075
+ function getAutoInstrumentations(integrations, manualInstrumentationNames = /* @__PURE__ */ new Set()) {
3076
+ if (integrations === false) return [];
3077
+ const getNodeAutoInstrumentations = _autoInstrumentationsLoader ? _autoInstrumentationsLoader() : loadNodeAutoInstrumentations();
3078
+ const exclusionConfig = {};
3079
+ for (const className of manualInstrumentationNames) {
3080
+ const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
3081
+ if (packageName) exclusionConfig[packageName] = { enabled: false };
3082
+ }
3083
+ if (integrations === true) {
3084
+ if (Object.keys(exclusionConfig).length > 0) return getNodeAutoInstrumentations(exclusionConfig);
3085
+ return getNodeAutoInstrumentations();
3086
+ }
3087
+ if (Array.isArray(integrations)) {
3088
+ const config = { ...exclusionConfig };
3089
+ for (const name of integrations) {
3090
+ const packageName = `@opentelemetry/instrumentation-${name}`;
3091
+ if (!exclusionConfig[packageName]) config[packageName] = { enabled: true };
3092
+ }
3093
+ return getNodeAutoInstrumentations(config);
3094
+ }
3095
+ const config = {
3096
+ ...exclusionConfig,
3097
+ ...integrations
3098
+ };
3099
+ for (const packageName of Object.keys(exclusionConfig)) if (Object.keys(integrations).find((key) => packageName.includes(key))) config[packageName] = { enabled: false };
3100
+ return getNodeAutoInstrumentations(config);
3101
+ }
3102
+ /**
3103
+ * Check if autotel has been initialized
3104
+ */
3105
+ function isInitialized() {
3106
+ return initialized;
3107
+ }
3108
+ /**
3109
+ * Get current config (internal use)
3110
+ */
3111
+ function getConfig() {
3112
+ return config;
3113
+ }
3114
+ /**
3115
+ * Get current logger (internal use)
3116
+ */
3117
+ function getLogger() {
3118
+ return logger;
3119
+ }
3120
+ /**
3121
+ * Get validation config (internal use)
3122
+ */
3123
+ function getValidationConfig() {
3124
+ return validationConfig;
3125
+ }
3126
+ /**
3127
+ * Get events config (internal use)
3128
+ */
3129
+ function getEventsConfig() {
3130
+ return eventsConfig;
3131
+ }
3132
+ /**
3133
+ * Warn once if not initialized (same behavior in all environments)
3134
+ */
3135
+ function warnIfNotInitialized(context) {
3136
+ if (!initialized && !warnedOnce) {
3137
+ logger.warn({}, `[autotel] ${context} used before init() called. Call init({ service: "..." }) first. See: https://docs.autotel.dev/quickstart`);
3138
+ warnedOnce = true;
3139
+ }
3140
+ }
3141
+ /**
3142
+ * Auto-detect version from package.json
3143
+ */
3144
+ function detectVersion() {
3145
+ try {
3146
+ const fs = requireModule("node:fs");
3147
+ return JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`, "utf8")).version || "1.0.0";
3148
+ } catch {
3149
+ return "1.0.0";
3150
+ }
3151
+ }
3152
+ /**
3153
+ * Detect hostname for resource attributes.
3154
+ * Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
3155
+ *
3156
+ * Priority order:
3157
+ * 1. DD_HOSTNAME environment variable (Datadog convention)
3158
+ * 2. HOSTNAME environment variable (common Unix convention)
3159
+ * 3. os.hostname() (system hostname)
3160
+ *
3161
+ * @returns hostname string or undefined if detection fails
3162
+ */
3163
+ function detectHostname() {
3164
+ if (process.env.DD_HOSTNAME) return process.env.DD_HOSTNAME;
3165
+ if (process.env.HOSTNAME) return process.env.HOSTNAME;
3166
+ try {
3167
+ return requireModule("node:os").hostname();
3168
+ } catch {
3169
+ return;
3170
+ }
3171
+ }
3172
+ /**
3173
+ * @internal Close embedded devtools if running.
3174
+ */
3175
+ async function _closeEmbeddedDevtools() {
3176
+ if (_devtoolsClose) {
3177
+ await _devtoolsClose();
3178
+ _devtoolsClose = null;
3179
+ }
3180
+ }
3181
+ /**
3182
+ * Get SDK instance (for shutdown)
3183
+ */
3184
+ function getSdk() {
3185
+ return sdk;
3186
+ }
3187
+
3188
+ //#endregion
3189
+ export { BaggageSpanProcessor 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, createStringRedactor 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 };
3190
+ //# sourceMappingURL=init-R-gSEXkJ.js.map