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