@autter/runtime-node 1.3.4 → 1.5.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/dist/index.js CHANGED
@@ -1,3 +1,69 @@
1
+ import {
2
+ CODE_PATTERN,
3
+ DEFAULT_REQUEST_ID_HEADER,
4
+ RuntimeError,
5
+ captureExceptionAsLog,
6
+ compileIgnore,
7
+ configureRuntimeLogger,
8
+ consoleSink,
9
+ createRequestState,
10
+ createRuntimeLogger,
11
+ currentOperation,
12
+ defineRuntimeErrors,
13
+ emitSummary,
14
+ errorAttributes,
15
+ errorCodeOf,
16
+ errorInternal,
17
+ fileSink,
18
+ flushRuntimeLogs,
19
+ isRuntimeErrorLike,
20
+ local,
21
+ makeRedactor,
22
+ normalizeRoutePath,
23
+ noteOperationError,
24
+ notifyException,
25
+ otlpSink,
26
+ parseUserAgent,
27
+ recordOperationAiUsage,
28
+ redactAttributes,
29
+ redactText,
30
+ reportExplicitFailure,
31
+ requestOutcome,
32
+ resolveRequestId,
33
+ responseStatusOf,
34
+ runInBackground,
35
+ runtimeContext,
36
+ runtimeLogStats,
37
+ runtimeLogger,
38
+ shutdownRuntimeLogger,
39
+ toClientError,
40
+ withRuntimeOperation
41
+ } from "./chunk-3TJHUCZ6.js";
42
+ import {
43
+ runDoctor
44
+ } from "./chunk-ZS4WQJQR.js";
45
+ import {
46
+ COMPAT_MANIFEST,
47
+ CountingExporter,
48
+ INGESTER_VERSION_HEADER,
49
+ SDK_IDENTITY,
50
+ configureCompatCheck,
51
+ debugLog,
52
+ ensureCompatConfigured,
53
+ evaluateCompat,
54
+ featuresForBrowserEvents,
55
+ fetchIngesterCompat,
56
+ installAutterAutoFlush,
57
+ isDebugEnabled,
58
+ noteCompatFeature,
59
+ noteRelayRejection,
60
+ observeIngesterResponse,
61
+ registerFlushTarget,
62
+ setDebugMode,
63
+ telemetryStats,
64
+ unregisterFlushTargets
65
+ } from "./chunk-L7HWKMCZ.js";
66
+
1
67
  // src/relay.ts
2
68
  var IpWindow = class {
3
69
  constructor(limitPerMinute) {
@@ -36,7 +102,120 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
36
102
  "timing"
37
103
  ]);
38
104
  var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
39
- function sanitizeBrowserPayload(raw) {
105
+ var CONTEXT_MAX_DEPTH = 6;
106
+ var CONTEXT_MAX_NODES = 256;
107
+ var CONTEXT_MAX_STRING = 4e3;
108
+ var CONTEXT_MAX_ARRAY = 100;
109
+ var CONTEXT_MAX_KEYS = 100;
110
+ function byteLength(text) {
111
+ return new TextEncoder().encode(text).length;
112
+ }
113
+ function boundContext(value) {
114
+ let nodes = 0;
115
+ const seen = /* @__PURE__ */ new WeakSet();
116
+ const walk = (v, depth) => {
117
+ if (v === null) return null;
118
+ const t = typeof v;
119
+ if (t === "string") return v.slice(0, CONTEXT_MAX_STRING);
120
+ if (t === "number" || t === "boolean") return v;
121
+ if (t !== "object") return void 0;
122
+ if (depth >= CONTEXT_MAX_DEPTH || nodes >= CONTEXT_MAX_NODES) return void 0;
123
+ const obj = v;
124
+ if (seen.has(obj)) return void 0;
125
+ seen.add(obj);
126
+ let isArr = false;
127
+ try {
128
+ isArr = Array.isArray(v);
129
+ } catch {
130
+ return void 0;
131
+ }
132
+ if (isArr) {
133
+ const arr = v;
134
+ const out2 = [];
135
+ let len = 0;
136
+ try {
137
+ len = arr.length;
138
+ } catch {
139
+ return out2;
140
+ }
141
+ for (let i = 0; i < len && i < CONTEXT_MAX_ARRAY; i++) {
142
+ if (nodes >= CONTEXT_MAX_NODES) break;
143
+ nodes++;
144
+ let el;
145
+ try {
146
+ el = walk(arr[i], depth + 1);
147
+ } catch {
148
+ el = void 0;
149
+ }
150
+ if (el !== void 0) out2.push(el);
151
+ }
152
+ return out2;
153
+ }
154
+ let keys;
155
+ try {
156
+ keys = Object.keys(obj);
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ const out = {};
161
+ for (let i = 0; i < keys.length && i < CONTEXT_MAX_KEYS; i++) {
162
+ if (nodes >= CONTEXT_MAX_NODES) break;
163
+ const key = keys[i];
164
+ if (key === void 0) continue;
165
+ nodes++;
166
+ let child;
167
+ try {
168
+ child = walk(obj[key], depth + 1);
169
+ } catch {
170
+ child = void 0;
171
+ }
172
+ if (child !== void 0) out[key] = child;
173
+ }
174
+ return out;
175
+ };
176
+ let result;
177
+ try {
178
+ result = walk(value, 0);
179
+ } catch {
180
+ result = void 0;
181
+ }
182
+ return result && typeof result === "object" ? result : {};
183
+ }
184
+ async function readBodyBounded(request, maxBody) {
185
+ const body = request.body;
186
+ if (!body) {
187
+ const text = await request.text();
188
+ return byteLength(text) > maxBody ? { tooLarge: true } : { tooLarge: false, text };
189
+ }
190
+ const reader = body.getReader();
191
+ const chunks = [];
192
+ let total = 0;
193
+ for (; ; ) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ if (!value) continue;
197
+ total += value.byteLength;
198
+ if (total > maxBody) {
199
+ void Promise.resolve().then(() => reader.cancel()).catch(() => {
200
+ });
201
+ return { tooLarge: true };
202
+ }
203
+ chunks.push(value);
204
+ }
205
+ const buf = new Uint8Array(total);
206
+ let offset = 0;
207
+ for (const chunk of chunks) {
208
+ buf.set(chunk, offset);
209
+ offset += chunk.byteLength;
210
+ }
211
+ return { tooLarge: false, text: new TextDecoder().decode(buf) };
212
+ }
213
+ function relayContext(raw, redactor) {
214
+ const bounded = boundContext(raw);
215
+ return Array.isArray(bounded) ? {} : { context: redactor(bounded) };
216
+ }
217
+ var defaultRelayRedactor = makeRedactor(true);
218
+ function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
40
219
  if (typeof raw !== "object" || raw === null) return null;
41
220
  const p = raw;
42
221
  if (p.version !== 1) return null;
@@ -53,16 +232,16 @@ function sanitizeBrowserPayload(raw) {
53
232
  type: e.type,
54
233
  timestamp: e.timestamp,
55
234
  ...typeof e.severity === "string" && SEVERITIES.has(e.severity) ? { severity: e.severity } : {},
56
- message: typeof e.message === "string" ? e.message.slice(0, 4e3) : "",
57
- ...typeof e.name === "string" ? { name: e.name.slice(0, 200) } : {},
235
+ message: typeof e.message === "string" ? redactor.text(e.message.slice(0, 4512)).slice(0, 4e3) : "",
236
+ ...typeof e.name === "string" ? { name: redactor.text(e.name.slice(0, 712)).slice(0, 200) } : {},
58
237
  ...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) ? { durationMs: Math.max(0, Math.min(12e4, e.durationMs)) } : {},
59
- ...typeof e.stack === "string" ? { stack: e.stack.slice(0, 32e3) } : {},
238
+ ...typeof e.stack === "string" ? { stack: redactor.text(e.stack.slice(0, 32512)).slice(0, 32e3) } : {},
60
239
  ...typeof e.errorType === "string" ? { errorType: e.errorType.slice(0, 200) } : {},
61
240
  ...typeof e.filename === "string" ? { filename: e.filename.split("?")[0].slice(0, 1e3) } : {},
62
241
  ...typeof e.line === "number" ? { line: e.line } : {},
63
242
  ...typeof e.column === "number" ? { column: e.column } : {},
64
243
  ...typeof e.route === "string" ? { route: e.route.split("?")[0].slice(0, 1e3) } : {},
65
- ...typeof e.context === "object" && e.context !== null ? { context: e.context } : {}
244
+ ...typeof e.context === "object" && e.context !== null ? relayContext(e.context, redactor) : {}
66
245
  });
67
246
  }
68
247
  return {
@@ -71,11 +250,16 @@ function sanitizeBrowserPayload(raw) {
71
250
  service: p.service.slice(0, 200),
72
251
  environment: p.environment.slice(0, 100),
73
252
  ...typeof p.release === "string" ? { release: p.release.slice(0, 200) } : {},
253
+ // @autter/runtime-browser version, for the ingester's compat records.
254
+ ...typeof p.sdk === "string" && /^\d{1,6}\.\d{1,6}\.\d{1,6}[0-9A-Za-z.+-]{0,20}$/.test(p.sdk) ? { sdk: p.sdk } : {},
74
255
  events
75
256
  };
76
257
  }
77
258
  function forward(payload, opts) {
78
- const url = `${(opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "")}/v1/browser`;
259
+ const endpoint = (opts.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
260
+ const url = `${endpoint}/v1/browser`;
261
+ const compat = opts.compatCheck !== false;
262
+ if (compat) ensureCompatConfigured(endpoint);
79
263
  void fetch(url, {
80
264
  method: "POST",
81
265
  headers: {
@@ -84,6 +268,14 @@ function forward(payload, opts) {
84
268
  },
85
269
  body: JSON.stringify(payload),
86
270
  signal: AbortSignal.timeout(1e4)
271
+ }).then((response) => {
272
+ if (!compat) return;
273
+ const types = new Set(
274
+ (payload.events ?? []).map((e) => String(e.type))
275
+ );
276
+ observeIngesterResponse({ status: response.status, headers: response.headers });
277
+ for (const feature of featuresForBrowserEvents(types)) noteCompatFeature(feature);
278
+ noteRelayRejection(types, response.status, response.headers);
87
279
  }).catch((err) => {
88
280
  (opts.onError ?? ((e) => console.warn("autter relay forward failed:", e)))(
89
281
  err
@@ -92,6 +284,7 @@ function forward(payload, opts) {
92
284
  }
93
285
  function createBrowserRelayFetchHandler(opts) {
94
286
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
287
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
95
288
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
96
289
  return async (request) => {
97
290
  if (request.method !== "POST") {
@@ -105,21 +298,28 @@ function createBrowserRelayFetchHandler(opts) {
105
298
  });
106
299
  }
107
300
  }
108
- const text = await request.text();
109
- if (text.length > maxBody) {
301
+ let bounded;
302
+ try {
303
+ bounded = await readBodyBounded(request, maxBody);
304
+ } catch {
305
+ return new Response(JSON.stringify({ error: "invalid json" }), {
306
+ status: 400
307
+ });
308
+ }
309
+ if (bounded.tooLarge) {
110
310
  return new Response(JSON.stringify({ error: "payload too large" }), {
111
311
  status: 413
112
312
  });
113
313
  }
114
314
  let raw;
115
315
  try {
116
- raw = JSON.parse(text);
316
+ raw = JSON.parse(bounded.text);
117
317
  } catch {
118
318
  return new Response(JSON.stringify({ error: "invalid json" }), {
119
319
  status: 400
120
320
  });
121
321
  }
122
- const payload = sanitizeBrowserPayload(raw);
322
+ const payload = sanitizeBrowserPayload(raw, redactor);
123
323
  if (!payload) {
124
324
  return new Response(JSON.stringify({ error: "invalid payload" }), {
125
325
  status: 400
@@ -131,6 +331,7 @@ function createBrowserRelayFetchHandler(opts) {
131
331
  }
132
332
  function createBrowserRelayHandler(opts) {
133
333
  const maxBody = opts.maxBodyBytes ?? DEFAULT_MAX_BODY;
334
+ const redactor = opts.redact === void 0 || opts.redact === true ? defaultRelayRedactor : makeRedactor(opts.redact);
134
335
  const limiter = opts.perIpRateLimit === false ? null : new IpWindow(opts.perIpRateLimit ?? 120);
135
336
  function respond(res, status, body) {
136
337
  res.statusCode = status;
@@ -142,7 +343,7 @@ function createBrowserRelayHandler(opts) {
142
343
  }
143
344
  }
144
345
  function handleParsed(raw, res) {
145
- const payload = sanitizeBrowserPayload(raw);
346
+ const payload = sanitizeBrowserPayload(raw, redactor);
146
347
  if (!payload) {
147
348
  respond(res, 400, { error: "invalid payload" });
148
349
  return;
@@ -206,8 +407,8 @@ function createBrowserRelayHandler(opts) {
206
407
  import { ServerResponse } from "http";
207
408
  import { randomUUID } from "crypto";
208
409
  import {
209
- context,
210
- trace,
410
+ context as context2,
411
+ trace as trace2,
211
412
  SpanKind,
212
413
  SpanStatusCode,
213
414
  TraceFlags
@@ -219,7 +420,7 @@ import {
219
420
  } from "@opentelemetry/exporter-metrics-otlp-http";
220
421
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
221
422
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
222
- import { Resource } from "@opentelemetry/resources";
423
+ import { Resource as Resource2 } from "@opentelemetry/resources";
223
424
  import { ExplicitBucketHistogramAggregation, PeriodicExportingMetricReader, View } from "@opentelemetry/sdk-metrics";
224
425
  import { NodeSDK } from "@opentelemetry/sdk-node";
225
426
  import {
@@ -235,50 +436,83 @@ import {
235
436
  ATTR_SERVICE_VERSION
236
437
  } from "@opentelemetry/semantic-conventions";
237
438
 
238
- // src/lifecycle.ts
439
+ // src/redact-exporter.ts
239
440
  import { ExportResultCode } from "@opentelemetry/core";
240
- var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
241
- function setDebugMode(enabled) {
242
- debugEnabled = enabled;
243
- }
244
- function isDebugEnabled() {
245
- return debugEnabled;
441
+ import { Resource } from "@opentelemetry/resources";
442
+ import {
443
+ AggregationTemporality
444
+ } from "@opentelemetry/sdk-metrics";
445
+ var scrubbedResources = /* @__PURE__ */ new WeakMap();
446
+ function redactResource(resource, redactor) {
447
+ const cached = scrubbedResources.get(resource);
448
+ if (cached) return cached;
449
+ const scrubbed = new Resource(redactor.exported(resource.attributes));
450
+ scrubbedResources.set(resource, scrubbed);
451
+ return scrubbed;
246
452
  }
247
- function debugLog(...args) {
248
- if (debugEnabled) {
249
- console.error("[autter]", ...args);
250
- }
453
+ function redactSpan(span, redactor) {
454
+ const status = span.status.message ? { ...span.status, message: redactor.text(span.status.message) } : span.status;
455
+ return {
456
+ name: redactor.text(span.name),
457
+ kind: span.kind,
458
+ spanContext: () => span.spanContext(),
459
+ parentSpanId: span.parentSpanId,
460
+ startTime: span.startTime,
461
+ endTime: span.endTime,
462
+ status,
463
+ attributes: redactor.exported(span.attributes),
464
+ links: span.links.map(
465
+ (link) => link.attributes ? { ...link, attributes: redactor.exported(link.attributes) } : link
466
+ ),
467
+ events: span.events.map((event) => ({
468
+ ...event,
469
+ name: redactor.text(event.name),
470
+ ...event.attributes ? { attributes: redactor.exported(event.attributes) } : {}
471
+ })),
472
+ duration: span.duration,
473
+ ended: span.ended,
474
+ resource: redactResource(span.resource, redactor),
475
+ instrumentationLibrary: span.instrumentationLibrary,
476
+ droppedAttributesCount: span.droppedAttributesCount,
477
+ droppedEventsCount: span.droppedEventsCount,
478
+ droppedLinksCount: span.droppedLinksCount
479
+ };
251
480
  }
252
- var TelemetryStats = class {
253
- /** Captures queued via our APIs and not yet seen leaving an exporter. */
254
- pendingCaptures = 0;
255
- markCaptured(count = 1) {
256
- this.pendingCaptures += count;
257
- }
258
- /** An exporter batch left the process successfully. */
259
- onExported(spanCount) {
260
- this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
261
- if (debugEnabled && spanCount > 0) {
262
- debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
263
- }
264
- }
265
- markAllFlushed() {
266
- this.pendingCaptures = 0;
267
- }
268
- };
269
- var telemetryStats = new TelemetryStats();
270
- var CountingExporter = class {
271
- constructor(inner) {
481
+ var RedactingSpanExporter = class {
482
+ constructor(inner, redactor) {
272
483
  this.inner = inner;
484
+ this.redactor = redactor;
273
485
  }
274
486
  inner;
487
+ redactor;
275
488
  export(spans, resultCallback) {
276
- this.inner.export(spans, (result) => {
277
- if (result.code === ExportResultCode.SUCCESS) {
278
- telemetryStats.onExported(spans.length);
489
+ const redactor = this.redactor();
490
+ let scrubbed = spans;
491
+ let failed = 0;
492
+ if (redactor.enabled) {
493
+ scrubbed = [];
494
+ for (const span of spans) {
495
+ try {
496
+ scrubbed.push(redactSpan(span, redactor));
497
+ } catch {
498
+ failed++;
499
+ }
279
500
  }
280
- resultCallback(result);
281
- });
501
+ }
502
+ if (!failed) {
503
+ this.inner.export(scrubbed, resultCallback);
504
+ return;
505
+ }
506
+ const error = new Error(`autter-runtime: ${failed} span(s) could not be redacted and were not exported`);
507
+ const fail = () => resultCallback({ code: ExportResultCode.FAILED, error });
508
+ if (!scrubbed.length) {
509
+ fail();
510
+ return;
511
+ }
512
+ this.inner.export(
513
+ scrubbed,
514
+ (result) => result.code === ExportResultCode.SUCCESS ? fail() : resultCallback(result)
515
+ );
282
516
  }
283
517
  shutdown() {
284
518
  return this.inner.shutdown();
@@ -287,394 +521,38 @@ var CountingExporter = class {
287
521
  return this.inner.forceFlush?.() ?? Promise.resolve();
288
522
  }
289
523
  };
290
- var flushTargets = /* @__PURE__ */ new Map();
291
- function registerFlushTarget(key, target) {
292
- flushTargets.set(key, target);
293
- }
294
- function unregisterFlushTargets() {
295
- flushTargets.clear();
296
- }
297
- function activeFlushTargets() {
298
- return [...flushTargets.values()];
299
- }
300
- var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
301
- var SIGNAL_EXIT_CODES = {
302
- SIGINT: 130,
303
- SIGTERM: 143
304
- };
305
- function installAutterAutoFlush(options = {}) {
306
- const logger = options.logger ?? console;
307
- const log = options.log !== false;
308
- const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
309
- const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
310
- const signals = options.signals ?? DEFAULT_SIGNALS;
311
- const flushOnUncaughtException = options.flushOnUncaughtException !== false;
312
- let disposed = false;
313
- let inFlight = null;
314
- const doFlush = async () => {
315
- const targets = options.targets ?? activeFlushTargets();
316
- if (targets.length === 0) return true;
317
- let timer;
318
- const timedOut = new Promise((resolve) => {
319
- timer = setTimeout(() => resolve(false), timeoutMs);
320
- });
321
- const drained = Promise.allSettled(
322
- targets.map(
323
- (target) => Promise.resolve().then(() => target.forceFlush())
324
- )
325
- ).then(
326
- (results) => results.every((result) => result.status === "fulfilled")
327
- );
328
- let ok;
329
- try {
330
- ok = await Promise.race([drained, timedOut]);
331
- } finally {
332
- clearTimeout(timer);
333
- }
334
- if (ok) {
335
- telemetryStats.markAllFlushed();
336
- if (log) {
337
- logger.log("[autter] telemetry flushed");
338
- }
339
- } else {
340
- logger.error(
341
- `[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
342
- );
343
- }
344
- return ok;
345
- };
346
- const flush = (reason = "exit") => {
347
- if (disposed) return Promise.resolve(false);
348
- if (!inFlight) {
349
- inFlight = doFlush().finally(() => {
350
- inFlight = null;
351
- });
352
- }
353
- if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
354
- return inFlight;
355
- };
356
- const signalCounts = /* @__PURE__ */ new Map();
357
- let handlingUncaughtException = false;
358
- const onUncaughtException = (error) => {
359
- if (process.listenerCount("uncaughtException") > 1) return;
360
- if (handlingUncaughtException) return;
361
- handlingUncaughtException = true;
362
- logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
363
- const keepAlive = setTimeout(() => {
364
- }, timeoutMs + 50);
365
- void flush("uncaughtException").finally(() => {
366
- clearTimeout(keepAlive);
367
- process.exit(1);
368
- });
369
- };
370
- const onSignal = (signal) => {
371
- const count = (signalCounts.get(signal) ?? 0) + 1;
372
- signalCounts.set(signal, count);
373
- const exitCode = SIGNAL_EXIT_CODES[signal] ?? 1;
374
- if (count > 1) {
375
- process.exit(exitCode);
376
- }
377
- const otherHandlers = process.listenerCount(signal) - 1;
378
- if (otherHandlers > 0) {
379
- void flush(signal);
380
- return;
381
- }
382
- void flush(signal).finally(() => {
383
- process.exit(exitCode);
384
- });
385
- };
386
- const MAX_BEFORE_EXIT_FLUSHES = 10;
387
- let beforeExitFlushes = 0;
388
- const onBeforeExit = () => {
389
- if (disposed) return;
390
- const targets = options.targets ?? activeFlushTargets();
391
- const worthIt = beforeExitFlushes === 0 ? targets.length > 0 : telemetryStats.pendingCaptures > 0;
392
- if (!worthIt || beforeExitFlushes >= MAX_BEFORE_EXIT_FLUSHES) return;
393
- beforeExitFlushes++;
394
- const keepAlive = setTimeout(() => {
395
- }, timeoutMs);
396
- void flush("beforeExit").finally(() => clearTimeout(keepAlive));
397
- };
398
- const onExit = () => {
399
- if (!warnOnUnflushedExit || disposed) return;
400
- const pending = telemetryStats.pendingCaptures;
401
- if (pending > 0) {
402
- logger.error(
403
- `[autter] process exiting with ~${pending} captured item(s) never confirmed exported \u2014 await autter.shutdown() on graceful shutdown`
404
- );
405
- }
406
- };
407
- const signalHandlers = /* @__PURE__ */ new Map();
408
- process.on("beforeExit", onBeforeExit);
409
- if (flushOnUncaughtException) {
410
- process.on("uncaughtException", onUncaughtException);
411
- }
412
- for (const signal of signals) {
413
- const handler = () => onSignal(signal);
414
- signalHandlers.set(signal, handler);
415
- process.on(signal, handler);
416
- }
417
- process.on("exit", onExit);
418
- return {
419
- flush,
420
- dispose() {
421
- disposed = true;
422
- process.removeListener("beforeExit", onBeforeExit);
423
- process.removeListener("uncaughtException", onUncaughtException);
424
- process.removeListener("exit", onExit);
425
- for (const [signal, handler] of signalHandlers) {
426
- process.removeListener(signal, handler);
427
- }
428
- signalHandlers.clear();
429
- }
430
- };
431
- }
432
-
433
- // src/redact.ts
434
- var SENSITIVE_KEY_PATTERNS = [
435
- /e-?mail/,
436
- /pass(word|wd|phrase)|^pass$/,
437
- /token/,
438
- /secret/,
439
- /credential/,
440
- /(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/,
441
- /^-?x?-?authorization$|^auth(-|_|$)|bearer/,
442
- /cookie/,
443
- /phone|msisdn/,
444
- /\bssn\b|social[-_ ]?security/,
445
- /cvv|cvc|card([-_. ]?(number|num|no))?$/,
446
- /credit[-_.]?card/,
447
- /connection[-_.]?string/,
448
- /recovery[-_.]?code|\botp\b|magic[-_.]?link/
449
- ];
450
- var PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g;
451
- var EMAIL_VALUE_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
452
- var JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
453
- var OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{20,}\b/g;
454
- var GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g;
455
- var AWS_KEY_RE = /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
456
- var SLACK_TOKEN_RE = /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g;
457
- var BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/gi;
458
- var URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
459
- function toCaseInsensitive(pattern) {
460
- return typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(pattern.source, pattern.flags.includes("i") ? pattern.flags : `${pattern.flags}i`);
461
- }
462
- function compile(options) {
463
- const keyPatterns = [...SENSITIVE_KEY_PATTERNS];
464
- const valuePatterns = [
465
- JWT_RE,
466
- OPENAI_KEY_RE,
467
- GITHUB_TOKEN_RE,
468
- AWS_KEY_RE,
469
- SLACK_TOKEN_RE,
470
- BEARER_RE
471
- ];
472
- if (options?.additionalKeyPatterns) {
473
- for (const p of options.additionalKeyPatterns) {
474
- keyPatterns.push(toCaseInsensitive(p));
475
- }
524
+ var RedactingMetricExporter = class {
525
+ constructor(inner, redactor) {
526
+ this.inner = inner;
527
+ this.redactor = redactor;
476
528
  }
477
- if (options?.additionalValuePatterns) {
478
- for (const p of options.additionalValuePatterns) {
479
- if (!valuePatterns.some((existing) => existing.source === (typeof p === "string" ? p : p.source))) {
480
- valuePatterns.push(typeof p === "string" ? new RegExp(p, "gi") : p);
481
- }
529
+ inner;
530
+ redactor;
531
+ export(metrics2, resultCallback) {
532
+ const redactor = this.redactor();
533
+ if (!redactor.enabled) {
534
+ this.inner.export(metrics2, resultCallback);
535
+ return;
482
536
  }
483
- }
484
- return {
485
- keyPatterns,
486
- valuePatterns,
487
- urlCredentials: URL_CREDENTIALS_RE,
488
- privateKeyBlock: PRIVATE_KEY_BLOCK_RE,
489
- mask: options?.mask ?? "[redacted]",
490
- scrubEmailValues: options?.scrubEmailValues !== false
491
- };
492
- }
493
- function redactString(value, r) {
494
- let out = value.replace(r.privateKeyBlock, r.mask);
495
- for (const re of r.valuePatterns) {
496
- out = out.replace(re, r.mask);
497
- }
498
- out = out.replace(r.urlCredentials, "$1" + r.mask + "@");
499
- if (r.scrubEmailValues) {
500
- out = out.replace(EMAIL_VALUE_RE, r.mask);
501
- }
502
- return out;
503
- }
504
- var MAX_REDACTION_DEPTH = 64;
505
- var MAX_REDACTION_WORK = 1e4;
506
- var MAX_COLLECTION_ENTRIES = 1e3;
507
- function canTraverse(depth, state) {
508
- if (depth > MAX_REDACTION_DEPTH || state.remainingWork <= 0) return false;
509
- state.remainingWork -= 1;
510
- return true;
511
- }
512
- function redactValue(value, r, state, depth) {
513
- if (typeof value === "string") return redactString(value, r);
514
- let isArray = false;
515
- try {
516
- isArray = Array.isArray(value);
517
- } catch {
518
- return r.mask;
519
- }
520
- if (isArray) {
521
- return redactArray(value, r, state, depth);
522
- }
523
- if (typeof value === "object" && value !== null) {
524
- return redactObject(value, r, state, depth);
525
- }
526
- return value;
527
- }
528
- function redactArray(value, r, state, depth) {
529
- const existing = state.ancestors.get(value);
530
- if (existing) return r.mask;
531
- if (!canTraverse(depth, state)) return r.mask;
532
- const out = [];
533
- state.ancestors.set(value, out);
534
- let length;
535
- try {
536
- length = value.length;
537
- } catch {
538
- state.ancestors.delete(value);
539
- return r.mask;
540
- }
541
- const limit = Math.min(length, MAX_COLLECTION_ENTRIES);
542
- for (let i = 0; i < limit; i += 1) {
543
- let item;
537
+ let resource;
544
538
  try {
545
- item = value[i];
546
- } catch {
547
- out.push(r.mask);
548
- continue;
549
- }
550
- out.push(redactValue(item, r, state, depth + 1));
551
- }
552
- if (length > limit) {
553
- out.push(r.mask);
554
- }
555
- state.ancestors.delete(value);
556
- return out;
557
- }
558
- function redactObject(value, r, state, depth) {
559
- const existing = state.ancestors.get(value);
560
- if (existing) return r.mask;
561
- if (!canTraverse(depth, state)) return r.mask;
562
- const out = {};
563
- state.ancestors.set(value, out);
564
- let count = 0;
565
- let truncated = false;
566
- try {
567
- for (const key in value) {
568
- if (!Object.prototype.propertyIsEnumerable.call(
569
- value,
570
- key
571
- )) {
572
- continue;
573
- }
574
- if (count >= MAX_COLLECTION_ENTRIES) {
575
- truncated = true;
576
- break;
577
- }
578
- let nestedValue;
579
- try {
580
- nestedValue = value[key];
581
- } catch {
582
- out[key] = r.mask;
583
- count += 1;
584
- continue;
585
- }
586
- count += 1;
587
- out[key] = isSensitiveKey(key, nestedValue, r) ? r.mask : redactValue(
588
- nestedValue,
589
- r,
590
- state,
591
- depth + 1
592
- );
593
- }
594
- } catch {
595
- truncated = true;
596
- }
597
- if (truncated) {
598
- out.__redaction_truncated__ = r.mask;
599
- }
600
- state.ancestors.delete(value);
601
- return out;
602
- }
603
- var USAGE_TOKEN_KEYS = /* @__PURE__ */ new Set([
604
- "input_tokens",
605
- "output_tokens",
606
- "prompt_tokens",
607
- "completion_tokens",
608
- "total_tokens",
609
- "token_count",
610
- "gen_ai.usage.input_tokens",
611
- "gen_ai.usage.output_tokens",
612
- "gen_ai.usage.prompt_tokens",
613
- "gen_ai.usage.completion_tokens",
614
- "gen_ai.usage.total_tokens",
615
- "gen_ai.usage.token_count"
616
- ]);
617
- function isSensitiveKey(key, value, r) {
618
- const lowered = key.toLowerCase();
619
- if (USAGE_TOKEN_KEYS.has(lowered)) {
620
- return !(typeof value === "number" && Number.isFinite(value) && value >= 0);
621
- }
622
- return r.keyPatterns.some((re) => re.test(lowered));
623
- }
624
- function redactAttributes(attributes, options) {
625
- const r = compile(options);
626
- return redactWith(attributes, r);
627
- }
628
- function redactWith(attributes, r) {
629
- const out = {};
630
- if (!attributes) return out;
631
- const state = {
632
- ancestors: /* @__PURE__ */ new WeakMap(),
633
- remainingWork: MAX_REDACTION_WORK
634
- };
635
- let count = 0;
636
- let truncated = false;
637
- try {
638
- for (const key in attributes) {
639
- if (!Object.prototype.propertyIsEnumerable.call(
640
- attributes,
641
- key
642
- )) {
643
- continue;
644
- }
645
- if (count >= MAX_COLLECTION_ENTRIES || state.remainingWork <= 0) {
646
- truncated = true;
647
- break;
648
- }
649
- let value;
650
- try {
651
- value = attributes[key];
652
- } catch {
653
- out[key] = r.mask;
654
- count += 1;
655
- state.remainingWork -= 1;
656
- continue;
657
- }
658
- count += 1;
659
- state.remainingWork -= 1;
660
- if (value === void 0) continue;
661
- out[key] = isSensitiveKey(key, value, r) ? r.mask : redactValue(value, r, state, 0);
539
+ resource = redactResource(metrics2.resource, redactor);
540
+ } catch (error) {
541
+ resultCallback({ code: ExportResultCode.FAILED, error });
542
+ return;
662
543
  }
663
- } catch {
664
- truncated = true;
544
+ this.inner.export({ ...metrics2, resource }, resultCallback);
665
545
  }
666
- if (truncated) {
667
- out.__redaction_truncated__ = r.mask;
546
+ forceFlush() {
547
+ return this.inner.forceFlush();
668
548
  }
669
- return out;
670
- }
671
- function makeRedactor(options) {
672
- if (options === false) {
673
- return (attributes) => ({ ...attributes ?? {} });
549
+ shutdown() {
550
+ return this.inner.shutdown();
674
551
  }
675
- const r = compile(options === true ? void 0 : options);
676
- return (attributes) => redactWith(attributes, r);
677
- }
552
+ selectAggregationTemporality(instrumentType) {
553
+ return this.inner.selectAggregationTemporality ? this.inner.selectAggregationTemporality(instrumentType) : AggregationTemporality.CUMULATIVE;
554
+ }
555
+ };
678
556
 
679
557
  // src/memory.ts
680
558
  import { PerformanceObserver } from "perf_hooks";
@@ -721,6 +599,383 @@ function startMemoryMetrics() {
721
599
  };
722
600
  }
723
601
 
602
+ // src/requests.ts
603
+ import { AsyncResource } from "async_hooks";
604
+ import { context, trace } from "@opentelemetry/api";
605
+ var REQUEST_STATE = /* @__PURE__ */ Symbol.for("autter.runtime.request");
606
+ var BOUND = /* @__PURE__ */ Symbol.for("autter.runtime.bound");
607
+ var CAPTURED = /* @__PURE__ */ new WeakSet();
608
+ var capturer = () => {
609
+ };
610
+ function setRequestExceptionCapturer(fn) {
611
+ capturer = fn;
612
+ }
613
+ function markCaptured(error) {
614
+ if (error && typeof error === "object") CAPTURED.add(error);
615
+ }
616
+ function expressRouteOf(req) {
617
+ const props = req;
618
+ const path = props?.route?.path;
619
+ if (typeof path !== "string" || path === "") return null;
620
+ const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
621
+ const route = base + path;
622
+ return route.startsWith("/") ? route : null;
623
+ }
624
+ function defaultShouldCapture(error) {
625
+ return error instanceof RuntimeError || errorCodeOf(error) !== void 0 || responseStatusOf(error) >= 500;
626
+ }
627
+ function captureAtBoundary(error, state, rule = void 0) {
628
+ const wanted = typeof rule === "function" ? rule(error) : rule === void 0 ? defaultShouldCapture(error) : rule;
629
+ if (!wanted) return;
630
+ if (error && typeof error === "object" && CAPTURED.has(error)) return;
631
+ markCaptured(error);
632
+ try {
633
+ if (state && !state.sealed) local.run(state, () => capturer(error));
634
+ else capturer(error);
635
+ } catch {
636
+ }
637
+ }
638
+ function bindEmitter(emitter, state) {
639
+ const target = emitter;
640
+ if (!target || typeof target !== "object" || target[BOUND]) return;
641
+ target[BOUND] = true;
642
+ const wrappers = /* @__PURE__ */ new WeakMap();
643
+ const wrap = (listener) => {
644
+ if (typeof listener !== "function") return listener;
645
+ let wrapped = wrappers.get(listener);
646
+ if (!wrapped) {
647
+ wrapped = function(...args) {
648
+ return local.run(
649
+ state,
650
+ () => listener.apply(this, args)
651
+ );
652
+ };
653
+ wrappers.set(listener, wrapped);
654
+ }
655
+ return wrapped;
656
+ };
657
+ for (const method of [
658
+ "on",
659
+ "addListener",
660
+ "prependListener",
661
+ "once",
662
+ "prependOnceListener"
663
+ ]) {
664
+ const original = target[method];
665
+ if (typeof original !== "function") continue;
666
+ target[method] = function(event, listener) {
667
+ return original.call(this, event, wrap(listener));
668
+ };
669
+ }
670
+ for (const method of ["removeListener", "off"]) {
671
+ const original = target[method];
672
+ if (typeof original !== "function") continue;
673
+ target[method] = function(event, listener) {
674
+ const wrapped = typeof listener === "function" ? wrappers.get(listener) ?? listener : listener;
675
+ return original.call(this, event, wrapped);
676
+ };
677
+ }
678
+ }
679
+ function exposeHeaderValue(current, header) {
680
+ const text = Array.isArray(current) ? current.join(", ") : String(current ?? "");
681
+ const names = text.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
682
+ if (names.includes("*") || names.includes(header)) return null;
683
+ return names.length ? `${text}, ${header}` : header;
684
+ }
685
+ function exposeOnCors(res, header) {
686
+ const original = res.writeHead;
687
+ res.writeHead = function(...args) {
688
+ try {
689
+ const inline = args.slice(1).find(
690
+ (arg) => !!arg && typeof arg === "object" && !Array.isArray(arg)
691
+ );
692
+ const inlineKey = (name) => inline ? Object.keys(inline).find((key) => key.toLowerCase() === name) : void 0;
693
+ const cors = res.getHeader("access-control-allow-origin") !== void 0 || inlineKey("access-control-allow-origin") !== void 0;
694
+ if (cors) {
695
+ const key = inlineKey("access-control-expose-headers");
696
+ const value = exposeHeaderValue(
697
+ key && inline ? inline[key] : res.getHeader("access-control-expose-headers"),
698
+ header
699
+ );
700
+ if (value && key && inline) inline[key] = value;
701
+ else if (value) res.setHeader("access-control-expose-headers", value);
702
+ }
703
+ } catch {
704
+ }
705
+ return original.apply(this, args);
706
+ };
707
+ }
708
+ function pathOf(req) {
709
+ const raw = req.originalUrl ?? req.url ?? "/";
710
+ return raw.split(/[?#]/)[0] || "/";
711
+ }
712
+ function finishRequest(state, input) {
713
+ if (state.sealed) return;
714
+ state.name = (input.name ?? `${input.method} ${input.route}`).slice(0, 200);
715
+ state.outcome = requestOutcome({
716
+ ...state.explicitOutcome ? { explicit: state.outcome } : {},
717
+ ...state.error !== void 0 ? { error: state.error } : {},
718
+ ...state.errorThrown ? { thrown: true } : {},
719
+ status: input.status,
720
+ aborted: input.aborted
721
+ });
722
+ const span = trace.getActiveSpan();
723
+ if (span?.isRecording()) span.setAttribute("autter.operation.outcome", state.outcome);
724
+ if (input.span) {
725
+ const remote = trace.wrapSpanContext(input.span);
726
+ context.with(
727
+ trace.setSpan(context.active(), remote),
728
+ () => reportExplicitFailure(state)
729
+ );
730
+ } else reportExplicitFailure(state);
731
+ emitSummary(state, {
732
+ http: {
733
+ "http.request.method": input.method,
734
+ "http.route": input.route,
735
+ "http.response.status_code": input.status,
736
+ "autter.request.aborted": input.aborted
737
+ },
738
+ ...input.span ? { span: input.span } : {}
739
+ });
740
+ }
741
+ function beginNodeRequest(req, res, options, route) {
742
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
743
+ const requestId = resolveRequestId(
744
+ options.trustRequestId === false ? void 0 : req.headers[header]
745
+ );
746
+ const method = (req.method ?? "GET").toUpperCase();
747
+ const path = pathOf(req);
748
+ const state = createRequestState({
749
+ name: `${method} ${normalizeRoutePath(path)}`,
750
+ requestId,
751
+ req,
752
+ res
753
+ });
754
+ req[REQUEST_STATE] = state;
755
+ try {
756
+ if (!res.headersSent) res.setHeader(header, requestId);
757
+ exposeOnCors(res, header);
758
+ } catch {
759
+ }
760
+ const span = trace.getActiveSpan();
761
+ if (span?.isRecording())
762
+ span.setAttributes({
763
+ "autter.request.id": requestId,
764
+ "autter.operation.kind": "request"
765
+ });
766
+ const spanContext = span?.spanContext();
767
+ bindEmitter(req, state);
768
+ bindEmitter(res, state);
769
+ let finished = false;
770
+ const done = () => {
771
+ finishRequest(state, {
772
+ method,
773
+ route: route?.() ?? expressRouteOf(req) ?? normalizeRoutePath(path),
774
+ status: res.statusCode,
775
+ aborted: !finished,
776
+ ...spanContext ? { span: spanContext } : {}
777
+ });
778
+ };
779
+ res.once("finish", () => {
780
+ finished = true;
781
+ done();
782
+ });
783
+ res.once("close", done);
784
+ return state;
785
+ }
786
+ function autterRequests(options = {}) {
787
+ const ignored = compileIgnore(options.ignore);
788
+ return function autterRequestsMiddleware(req, res, next) {
789
+ if (req[REQUEST_STATE] || ignored(pathOf(req))) {
790
+ next();
791
+ return;
792
+ }
793
+ const state = beginNodeRequest(req, res, options);
794
+ local.run(state, () => next());
795
+ };
796
+ }
797
+ function autterErrorResponse(options = {}) {
798
+ return function autterErrorResponseMiddleware(error, req, res, next) {
799
+ const state = req[REQUEST_STATE];
800
+ if (state) noteOperationError(error, true, state);
801
+ captureAtBoundary(error, state, options.capture);
802
+ if (res.headersSent) {
803
+ next(error);
804
+ return;
805
+ }
806
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
807
+ const inbound = req.headers?.[header];
808
+ const requestId = state?.requestId ?? (typeof inbound === "string" ? inbound.slice(0, 128) : void 0);
809
+ res.statusCode = responseStatusOf(error);
810
+ res.setHeader("content-type", "application/json; charset=utf-8");
811
+ res.end(JSON.stringify(toClientError(error, requestId)));
812
+ };
813
+ }
814
+ var RESOURCE = /* @__PURE__ */ Symbol.for("autter.runtime.resource");
815
+ function autterFastify(fastify, options, done) {
816
+ const opts = options ?? {};
817
+ const header = (opts.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
818
+ const ignored = compileIgnore(opts.ignore);
819
+ const stateOf = (request) => request[REQUEST_STATE];
820
+ fastify.addHook("onRequest", ((request, reply, next) => {
821
+ if (ignored(request.url) || request.raw[REQUEST_STATE]) {
822
+ next();
823
+ return;
824
+ }
825
+ const requestId = resolveRequestId(
826
+ opts.trustRequestId === false ? void 0 : request.headers[header]
827
+ );
828
+ const method = String(request.method ?? "GET").toUpperCase();
829
+ const state = createRequestState({
830
+ name: `${method} ${normalizeRoutePath(request.url)}`,
831
+ requestId,
832
+ req: request.raw,
833
+ res: reply.raw
834
+ });
835
+ request[REQUEST_STATE] = state;
836
+ request.raw[REQUEST_STATE] = state;
837
+ reply.header(header, requestId);
838
+ const span = trace.getActiveSpan();
839
+ if (span?.isRecording())
840
+ span.setAttributes({
841
+ "autter.request.id": requestId,
842
+ "autter.operation.kind": "request"
843
+ });
844
+ const spanContext = span?.spanContext();
845
+ bindEmitter(request.raw, state);
846
+ bindEmitter(reply.raw, state);
847
+ let finished = false;
848
+ const finish = (aborted) => finishRequest(state, {
849
+ method,
850
+ route: request.routeOptions?.url ?? request.routerPath ?? normalizeRoutePath(request.url),
851
+ status: reply.raw.statusCode || reply.statusCode,
852
+ aborted,
853
+ ...spanContext ? { span: spanContext } : {}
854
+ });
855
+ reply.raw.once("finish", () => {
856
+ finished = true;
857
+ finish(false);
858
+ });
859
+ reply.raw.once("close", () => finish(!finished));
860
+ local.run(state, () => {
861
+ request[RESOURCE] = new AsyncResource("autter-request");
862
+ next();
863
+ });
864
+ }));
865
+ fastify.addHook("preHandler", ((request, _reply, next) => {
866
+ const resource = request[RESOURCE];
867
+ if (resource) resource.runInAsyncScope(() => next());
868
+ else next();
869
+ }));
870
+ fastify.addHook("onSend", ((request, reply, payload, next) => {
871
+ try {
872
+ if (stateOf(request) && reply.getHeader("access-control-allow-origin") !== void 0) {
873
+ const value = exposeHeaderValue(reply.getHeader("access-control-expose-headers"), header);
874
+ if (value) reply.header("access-control-expose-headers", value);
875
+ }
876
+ } catch {
877
+ }
878
+ next(null, payload);
879
+ }));
880
+ fastify.addHook("onError", ((request, _reply, error, next) => {
881
+ const state = stateOf(request);
882
+ if (state) noteOperationError(error, true, state);
883
+ captureAtBoundary(error, state);
884
+ next();
885
+ }));
886
+ done();
887
+ }
888
+ autterFastify[/* @__PURE__ */ Symbol.for("skip-override")] = true;
889
+ autterFastify[/* @__PURE__ */ Symbol.for("fastify.display-name")] = "autter-runtime";
890
+ function withHeaders(response, header, requestId) {
891
+ const apply = (target) => {
892
+ target.headers.set(header, requestId);
893
+ if (target.headers.get("access-control-allow-origin") !== null) {
894
+ const value = exposeHeaderValue(
895
+ target.headers.get("access-control-expose-headers") ?? "",
896
+ header
897
+ );
898
+ if (value) target.headers.set("access-control-expose-headers", value);
899
+ }
900
+ return target;
901
+ };
902
+ try {
903
+ return apply(response);
904
+ } catch {
905
+ try {
906
+ return apply(new Response(response.body, response));
907
+ } catch {
908
+ return response;
909
+ }
910
+ }
911
+ }
912
+ function withRuntimeRequest(handler, options = {}) {
913
+ const header = (options.requestIdHeader ?? DEFAULT_REQUEST_ID_HEADER).toLowerCase();
914
+ const ignored = compileIgnore(options.ignore);
915
+ return async (request, ...rest) => {
916
+ let pathname = "/";
917
+ try {
918
+ pathname = new URL(request.url, "http://localhost").pathname;
919
+ } catch {
920
+ }
921
+ if (ignored(pathname)) return handler(request, ...rest);
922
+ const requestId = resolveRequestId(
923
+ options.trustRequestId === false ? void 0 : request.headers.get(header)
924
+ );
925
+ const method = String(request.method ?? "GET").toUpperCase();
926
+ const route = options.route ?? normalizeRoutePath(pathname);
927
+ const state = createRequestState({
928
+ name: `${method} ${options.name ?? route}`,
929
+ requestId,
930
+ req: request
931
+ });
932
+ const span = trace.getActiveSpan();
933
+ if (span?.isRecording())
934
+ span.setAttributes({
935
+ "autter.request.id": requestId,
936
+ "autter.operation.kind": "request"
937
+ });
938
+ const spanContext = span?.spanContext();
939
+ let status = 500;
940
+ try {
941
+ const response = await local.run(state, async () => {
942
+ try {
943
+ return await handler(request, ...rest);
944
+ } catch (error) {
945
+ noteOperationError(error, true, state);
946
+ captureAtBoundary(error, state, true);
947
+ throw error;
948
+ }
949
+ });
950
+ status = response.status;
951
+ return withHeaders(response, header, requestId);
952
+ } finally {
953
+ const aborted = request.signal?.aborted === true;
954
+ finishRequest(state, {
955
+ method,
956
+ route,
957
+ name: `${method} ${options.name ?? route}`,
958
+ status,
959
+ aborted,
960
+ ...spanContext ? { span: spanContext } : {}
961
+ });
962
+ if (options.waitUntil) {
963
+ try {
964
+ options.waitUntil(flushRuntimeLogs().catch(() => {
965
+ }));
966
+ } catch {
967
+ }
968
+ }
969
+ }
970
+ };
971
+ }
972
+ function startHookRequest(res) {
973
+ const req = res.req;
974
+ if (!req || req[REQUEST_STATE]) return;
975
+ const state = beginNodeRequest(req, res, {});
976
+ local.enterWith(state);
977
+ }
978
+
724
979
  // src/server.ts
725
980
  var LLM_MARKER_ATTRIBUTES = [
726
981
  "gen_ai.operation.name",
@@ -849,7 +1104,7 @@ var ErrorTraceRetentionProcessor = class {
849
1104
  }
850
1105
  /** Promote the active trace, if any — called on captured errors. */
851
1106
  retainActiveTrace() {
852
- const active2 = trace.getActiveSpan()?.spanContext();
1107
+ const active2 = trace2.getActiveSpan()?.spanContext();
853
1108
  if (!active2 || (active2.traceFlags & TraceFlags.SAMPLED) !== 0) return;
854
1109
  const entry = this.entryFor(active2.traceId);
855
1110
  if (entry.retained) return;
@@ -942,36 +1197,43 @@ async function runLlmSpan(tracer, info, fn) {
942
1197
  kind: SpanKind.CLIENT,
943
1198
  attributes: llmBaseAttributes(info)
944
1199
  });
945
- const setResponseModel = (model) => span.setAttribute("gen_ai.response.model", model);
1200
+ const usage = {};
1201
+ let responseModel;
1202
+ const setResponseModel = (model) => {
1203
+ responseModel = model;
1204
+ span.setAttribute("gen_ai.response.model", model);
1205
+ };
946
1206
  const handle = {
947
- setUsage: (usage) => span.setAttributes(usageAttributes(usage)),
948
- setCost: (usd) => span.setAttribute("autter.llm.cost_usd", usd),
1207
+ setUsage: (next) => {
1208
+ Object.assign(usage, next);
1209
+ span.setAttributes(usageAttributes(next));
1210
+ },
1211
+ setCost: (usd) => {
1212
+ usage.costUsd = usd;
1213
+ span.setAttribute("autter.llm.cost_usd", usd);
1214
+ },
949
1215
  setResponseModel,
950
1216
  setModel: setResponseModel,
951
- setAttributes: (attributes) => span.setAttributes(attributes)
1217
+ setAttributes: (attributes) => span.setAttributes(activeRedactor(attributes))
952
1218
  };
953
1219
  try {
954
- const result = await context.with(
955
- trace.setSpan(context.active(), span),
1220
+ const result = await context2.with(
1221
+ trace2.setSpan(context2.active(), span),
956
1222
  () => fn(handle)
957
1223
  );
958
1224
  span.setStatus({ code: SpanStatusCode.OK });
959
1225
  return result;
960
1226
  } catch (err) {
961
- if (err instanceof Error) {
962
- span.recordException(err);
963
- span.setAttribute("error.type", err.name);
964
- }
965
- span.setStatus({
966
- code: SpanStatusCode.ERROR,
967
- message: err instanceof Error ? err.message : String(err)
968
- });
1227
+ recordError(span, err);
1228
+ if (err instanceof Error) span.setAttribute("error.type", err.name);
969
1229
  throw err;
970
1230
  } finally {
971
1231
  span.end();
1232
+ recordOperationAiUsage({ ...usage, model: responseModel ?? info.model });
972
1233
  }
973
1234
  }
974
1235
  function recordLlmCall(tracer, call) {
1236
+ recordOperationAiUsage({ ...call, model: call.model });
975
1237
  const durationMs = Math.max(0, call.durationMs ?? 0);
976
1238
  const endTime = /* @__PURE__ */ new Date();
977
1239
  const startTime = new Date(endTime.getTime() - durationMs);
@@ -984,50 +1246,52 @@ function recordLlmCall(tracer, call) {
984
1246
  }
985
1247
  );
986
1248
  if (call.error !== void 0 && call.error !== null) {
1249
+ recordError(span, call.error);
987
1250
  if (call.error instanceof Error) {
988
- span.recordException(call.error);
989
1251
  span.setAttribute("error.type", call.error.name);
990
1252
  }
991
- span.setStatus({
992
- code: SpanStatusCode.ERROR,
993
- message: call.error instanceof Error ? call.error.message : String(call.error)
994
- });
995
1253
  } else {
996
1254
  span.setStatus({ code: SpanStatusCode.OK });
997
1255
  }
998
1256
  span.end(endTime);
999
1257
  }
1000
- async function runWithSpan(tracer, name, fn, attributes) {
1001
- const span = tracer.startSpan(name, { attributes });
1258
+ async function runWithSpan(tracer, name, fn, attributes, links) {
1259
+ const span = tracer.startSpan(name, {
1260
+ attributes,
1261
+ ...links?.length ? { links } : {}
1262
+ });
1002
1263
  try {
1003
- const result = await context.with(
1004
- trace.setSpan(context.active(), span),
1264
+ const result = await context2.with(
1265
+ trace2.setSpan(context2.active(), span),
1005
1266
  fn
1006
1267
  );
1007
1268
  span.setStatus({ code: SpanStatusCode.OK });
1008
1269
  return result;
1009
1270
  } catch (err) {
1010
- if (err instanceof Error) span.recordException(err);
1011
- span.setStatus({
1012
- code: SpanStatusCode.ERROR,
1013
- message: err instanceof Error ? err.message : String(err)
1014
- });
1271
+ recordError(span, err);
1015
1272
  throw err;
1016
1273
  } finally {
1017
1274
  span.end();
1018
1275
  }
1019
1276
  }
1020
- function expressRouteOf(req) {
1021
- const props = req;
1022
- const path = props?.route?.path;
1023
- if (typeof path !== "string" || path === "") return null;
1024
- const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
1025
- const route = base + path;
1026
- return route.startsWith("/") ? route : null;
1277
+ function recordError(span, err) {
1278
+ const isError = err instanceof Error;
1279
+ const message = activeRedactor.text(isError ? err.message : String(err));
1280
+ if (isError) {
1281
+ span.addEvent("exception", {
1282
+ "exception.type": err.name,
1283
+ "exception.message": message,
1284
+ ...err.stack ? { "exception.stacktrace": activeRedactor.text(err.stack) } : {}
1285
+ });
1286
+ }
1287
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
1288
+ }
1289
+ function redactWithActive(attributes) {
1290
+ return activeRedactor(attributes);
1027
1291
  }
1028
1292
  function captureExpressRoute(_span, response) {
1029
1293
  if (!(response instanceof ServerResponse)) return;
1030
- const rpcMetadata = getRPCMetadata(context.active());
1294
+ const rpcMetadata = getRPCMetadata(context2.active());
1031
1295
  if (rpcMetadata?.type !== RPCType.HTTP) return;
1032
1296
  const setRoute = () => {
1033
1297
  if (rpcMetadata.route) return;
@@ -1036,6 +1300,21 @@ function captureExpressRoute(_span, response) {
1036
1300
  };
1037
1301
  response.prependListener("finish", setRoute);
1038
1302
  response.prependListener("close", setRoute);
1303
+ if (hookRequests) startHookRequest(response);
1304
+ }
1305
+ var hookRequests = false;
1306
+ function structuredErrorAttributes(error) {
1307
+ const attrs = { ...errorAttributes(error, { causes: false }) };
1308
+ const internal = errorInternal(error);
1309
+ if (internal) {
1310
+ try {
1311
+ attrs["autter.error.internal"] = JSON.stringify(
1312
+ activeRedactor(internal)
1313
+ ).slice(0, 4e3);
1314
+ } catch {
1315
+ }
1316
+ }
1317
+ return attrs;
1039
1318
  }
1040
1319
  var active = null;
1041
1320
  var activeAlwaysOnProvider = null;
@@ -1047,15 +1326,27 @@ function initAutterServer(options) {
1047
1326
  ""
1048
1327
  );
1049
1328
  const headers = { authorization: `Bearer ${options.apiKey}` };
1329
+ const traceExporter = () => new CountingExporter(
1330
+ new RedactingSpanExporter(
1331
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }),
1332
+ () => activeRedactor
1333
+ )
1334
+ );
1050
1335
  const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1051
1336
  if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1052
1337
  debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
1053
1338
  activeRedactor = makeRedactor(options.redactAttributes ?? true);
1054
- const resource = new Resource({
1339
+ const distro = options.distro ?? SDK_IDENTITY;
1340
+ configureCompatCheck({ endpoint, enabled: options.compatCheck !== false, sdk: distro });
1341
+ const resource = new Resource2({
1055
1342
  [ATTR_SERVICE_NAME]: options.service,
1056
1343
  "service.instance.id": (options.instanceId || process.env.AUTTER_RUNTIME_INSTANCE_ID || randomUUID()).slice(0, 128),
1057
1344
  ...options.release ? { [ATTR_SERVICE_VERSION]: options.release } : {},
1058
1345
  "deployment.environment": environment,
1346
+ // OTel semconv for SDK distributions: the ingester records which
1347
+ // Autter SDK version each service runs (version compatibility checks).
1348
+ "telemetry.distro.name": distro.name,
1349
+ "telemetry.distro.version": distro.version,
1059
1350
  // Tells the ingester request metrics arrive on the metrics pipe, so
1060
1351
  // it must not also fold our server spans into usage rollups (that
1061
1352
  // would double-count every sampled request).
@@ -1070,30 +1361,24 @@ function initAutterServer(options) {
1070
1361
  let sampler = retainTraces ? new RecordUnsampledSampler(headSampler) : headSampler;
1071
1362
  if (options.llmTracing !== false) sampler = new LlmAwareSampler(sampler);
1072
1363
  const errorTraceBuffer = retainTraces ? new ErrorTraceRetentionProcessor(
1073
- new BatchSpanProcessor(
1074
- new CountingExporter(
1075
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1076
- ),
1077
- { scheduledDelayMillis: 2e3 }
1078
- ),
1364
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 }),
1079
1365
  retainOnError,
1080
1366
  slowThresholdMs
1081
1367
  ) : null;
1082
- const mainSpanProcessor = new BatchSpanProcessor(
1083
- new CountingExporter(
1084
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1085
- )
1086
- );
1368
+ const mainSpanProcessor = new BatchSpanProcessor(traceExporter());
1087
1369
  const metricReader = new PeriodicExportingMetricReader({
1088
- exporter: new OTLPMetricExporter({
1089
- url: `${endpoint}/v1/metrics`,
1090
- headers,
1091
- // Deltas, not lifetime totals: the ingester SUMs data points
1092
- // into runtime_metrics_1m, and the default (cumulative)
1093
- // temporality would re-count every past request on each
1094
- // 60 s export.
1095
- temporalityPreference: AggregationTemporalityPreference.DELTA
1096
- }),
1370
+ exporter: new RedactingMetricExporter(
1371
+ new OTLPMetricExporter({
1372
+ url: `${endpoint}/v1/metrics`,
1373
+ headers,
1374
+ // Deltas, not lifetime totals: the ingester SUMs data points
1375
+ // into runtime_metrics_1m, and the default (cumulative)
1376
+ // temporality would re-count every past request on each
1377
+ // 60 s export.
1378
+ temporalityPreference: AggregationTemporalityPreference.DELTA
1379
+ }),
1380
+ () => activeRedactor
1381
+ ),
1097
1382
  exportIntervalMillis: options.metricIntervalMs ?? 6e4
1098
1383
  });
1099
1384
  const sdk = new NodeSDK({
@@ -1119,16 +1404,14 @@ function initAutterServer(options) {
1119
1404
  });
1120
1405
  sdk.start();
1121
1406
  const stopMemoryMetrics = options.memoryMetrics === false ? null : startMemoryMetrics();
1407
+ noteCompatFeature("endpoint_latency");
1408
+ if (stopMemoryMetrics) noteCompatFeature("memory_metrics");
1409
+ if (options.llmTracing !== false) noteCompatFeature("llm_calls");
1122
1410
  const alwaysOnProvider = new BasicTracerProvider({
1123
1411
  resource,
1124
1412
  sampler: new AlwaysOnSampler(),
1125
1413
  spanProcessors: [
1126
- new BatchSpanProcessor(
1127
- new CountingExporter(
1128
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1129
- ),
1130
- { scheduledDelayMillis: 2e3 }
1131
- )
1414
+ new BatchSpanProcessor(traceExporter(), { scheduledDelayMillis: 2e3 })
1132
1415
  ]
1133
1416
  });
1134
1417
  const errorTracer = alwaysOnProvider.getTracer("autter-errors");
@@ -1138,31 +1421,43 @@ function initAutterServer(options) {
1138
1421
  errorTraceBuffer?.retainActiveTrace();
1139
1422
  telemetryStats.markCaptured();
1140
1423
  const isError = error instanceof Error;
1141
- const message = isError ? error.message : String(error);
1424
+ const message = activeRedactor.text(isError ? error.message : String(error));
1425
+ const stackText = isError && error.stack ? activeRedactor.text(error.stack) : "";
1142
1426
  const redacted = activeRedactor(attributes);
1143
- const activeSpan = trace.getActiveSpan();
1427
+ const activeSpan = trace2.getActiveSpan();
1144
1428
  const reuseActive = activeSpan?.isRecording() === true;
1145
1429
  const span = reuseActive ? activeSpan : errorTracer.startSpan(isError ? error.name : "Error");
1146
- span.setAttributes({ "autter.severity": "error", ...redacted });
1147
- if (isError && error.stack) {
1148
- if (redacted["autter.handled"] === true) {
1149
- span.addEvent("exception", {
1150
- "exception.type": error.name,
1151
- "exception.message": message,
1152
- "exception.stacktrace": error.stack,
1153
- "autter.handled": true,
1154
- "autter.sampled": redacted["autter.sampled"] === true
1155
- });
1156
- } else {
1157
- span.recordException(error);
1158
- }
1430
+ const structured = structuredErrorAttributes(error);
1431
+ const causes = errorAttributes(error);
1432
+ const declared = Object.fromEntries(
1433
+ Object.entries(causes).filter(([key]) => key !== "autter.error.internal")
1434
+ );
1435
+ const requestId = currentOperation()?.requestId;
1436
+ span.setAttributes({
1437
+ "autter.severity": "error",
1438
+ ...redacted,
1439
+ ...structured,
1440
+ ...requestId ? { "autter.request.id": requestId } : {}
1441
+ });
1442
+ markCaptured(error);
1443
+ noteOperationError(error);
1444
+ notifyException(error, redacted);
1445
+ if (isError && stackText) {
1446
+ span.addEvent("exception", {
1447
+ "exception.type": error.name,
1448
+ "exception.message": message,
1449
+ "exception.stacktrace": stackText,
1450
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1451
+ ...declared
1452
+ });
1159
1453
  } else {
1160
1454
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
1161
1455
  span.addEvent("exception", {
1162
1456
  "exception.type": isError ? error.name : "Error",
1163
1457
  "exception.message": message,
1164
- ...stack ? { "exception.stacktrace": stack } : {},
1165
- ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {}
1458
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1459
+ ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1460
+ ...declared
1166
1461
  });
1167
1462
  }
1168
1463
  span.setStatus({ code: SpanStatusCode.ERROR, message });
@@ -1174,22 +1469,23 @@ function initAutterServer(options) {
1174
1469
  }
1175
1470
  telemetryStats.markCaptured();
1176
1471
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureMessage")).join("\n");
1472
+ const safeMessage = activeRedactor.text(message);
1177
1473
  const span = errorTracer.startSpan("Message", {
1178
1474
  attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1179
1475
  });
1180
1476
  span.addEvent("exception", {
1181
1477
  "exception.type": "Message",
1182
- "exception.message": message,
1183
- ...stack ? { "exception.stacktrace": stack } : {},
1478
+ "exception.message": safeMessage,
1479
+ ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1184
1480
  "autter.severity": severity
1185
1481
  });
1186
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1482
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1187
1483
  span.end();
1188
1484
  }
1189
1485
  function reportOutcome2(name, message, attributes) {
1190
1486
  errorTraceBuffer?.retainActiveTrace();
1191
1487
  telemetryStats.markCaptured();
1192
- const activeSpan = trace.getActiveSpan();
1488
+ const activeSpan = trace2.getActiveSpan();
1193
1489
  const reuseActive = activeSpan?.isRecording() === true;
1194
1490
  const span = reuseActive ? activeSpan : errorTracer.startSpan("OutcomeFailure");
1195
1491
  const safeOutcome = activeRedactor({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
@@ -1204,6 +1500,19 @@ function initAutterServer(options) {
1204
1500
  span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1205
1501
  if (!reuseActive) span.end();
1206
1502
  }
1503
+ configureRuntimeLogger({
1504
+ endpoint,
1505
+ apiKey: options.apiKey,
1506
+ service: options.service,
1507
+ environment,
1508
+ distro,
1509
+ release: options.release,
1510
+ options: options.logging,
1511
+ redact: activeRedactor,
1512
+ run: (name, fn, attributes, links) => runWithSpan(processTracer, name, fn, activeRedactor(attributes), links),
1513
+ reportOutcome: reportOutcome2
1514
+ });
1515
+ hookRequests = options.logging?.requests === true;
1207
1516
  if (options.captureGlobalErrors !== false) {
1208
1517
  process.on("uncaughtExceptionMonitor", (err) => {
1209
1518
  captureException2(err, { "autter.unhandled": true });
@@ -1214,6 +1523,7 @@ function initAutterServer(options) {
1214
1523
  const flushTarget = {
1215
1524
  forceFlush: async () => {
1216
1525
  const results = await Promise.allSettled([
1526
+ flushRuntimeLogs(),
1217
1527
  Promise.resolve().then(() => alwaysOnProvider.forceFlush()),
1218
1528
  Promise.resolve().then(() => mainSpanProcessor.forceFlush()),
1219
1529
  ...errorTraceBuffer ? [Promise.resolve().then(() => errorTraceBuffer.forceFlush())] : [],
@@ -1240,6 +1550,8 @@ function initAutterServer(options) {
1240
1550
  withLlmCall: (info, fn) => runLlmSpan(llmTracer, info, fn),
1241
1551
  trackLlmCall: (call) => recordLlmCall(llmTracer, call),
1242
1552
  shutdown: async () => {
1553
+ const logShutdown = await Promise.allSettled([shutdownRuntimeLogger()]);
1554
+ hookRequests = false;
1243
1555
  stopMemoryMetrics?.();
1244
1556
  active = null;
1245
1557
  activeAlwaysOnProvider = null;
@@ -1248,6 +1560,7 @@ function initAutterServer(options) {
1248
1560
  unregisterFlushTargets();
1249
1561
  telemetryStats.markAllFlushed();
1250
1562
  await Promise.allSettled([alwaysOnProvider.shutdown(), sdk.shutdown()]);
1563
+ if (logShutdown[0]?.status === "rejected") throw logShutdown[0].reason;
1251
1564
  }
1252
1565
  };
1253
1566
  active = server;
@@ -1259,48 +1572,67 @@ function captureException(error, attributes) {
1259
1572
  active.captureException(error, attributes);
1260
1573
  return;
1261
1574
  }
1262
- const span = trace.getTracer("autter-errors").startSpan(error instanceof Error ? error.name : "Error", { attributes });
1263
- if (error instanceof Error) span.recordException(error);
1264
- span.setStatus({
1265
- code: SpanStatusCode.ERROR,
1266
- message: error instanceof Error ? error.message : String(error)
1575
+ if (loggerOnlyCapture) {
1576
+ loggerOnlyCapture(error, attributes);
1577
+ return;
1578
+ }
1579
+ markCaptured(error);
1580
+ noteOperationError(error);
1581
+ notifyException(error, attributes);
1582
+ const span = trace2.getTracer("autter-errors").startSpan(error instanceof Error ? error.name : "Error", {
1583
+ attributes: activeRedactor(attributes)
1267
1584
  });
1585
+ recordError(span, error);
1268
1586
  span.end();
1269
1587
  }
1588
+ var loggerOnlyCapture = null;
1589
+ function setLoggerOnlyCapture(fn) {
1590
+ loggerOnlyCapture = fn;
1591
+ }
1592
+ function serverActive() {
1593
+ return active !== null;
1594
+ }
1595
+ setRequestExceptionCapturer((error) => captureException(error));
1270
1596
  function withProcessSpan(name, fn, attributes) {
1271
1597
  if (active) return active.withProcessSpan(name, fn, attributes);
1272
- return runWithSpan(trace.getTracer("autter-processes"), name, fn, attributes);
1598
+ return runWithSpan(
1599
+ trace2.getTracer("autter-processes"),
1600
+ name,
1601
+ fn,
1602
+ activeRedactor(attributes)
1603
+ );
1273
1604
  }
1274
1605
  function withLlmCall(info, fn) {
1275
1606
  if (active) return active.withLlmCall(info, fn);
1276
- return runLlmSpan(trace.getTracer("autter-llm"), info, fn);
1607
+ return runLlmSpan(trace2.getTracer("autter-llm"), info, fn);
1277
1608
  }
1278
1609
  function trackLlmCall(call) {
1279
1610
  if (active) {
1280
1611
  active.trackLlmCall(call);
1281
1612
  return;
1282
1613
  }
1283
- recordLlmCall(trace.getTracer("autter-llm"), call);
1614
+ recordLlmCall(trace2.getTracer("autter-llm"), call);
1284
1615
  }
1285
1616
  function captureMessage(message, severity = "warning", attributes) {
1286
1617
  if (active) {
1287
1618
  active.captureMessage(message, severity, attributes);
1288
1619
  return;
1289
1620
  }
1290
- const span = trace.getTracer("autter-errors").startSpan("Message", {
1291
- attributes: { "autter.severity": severity, ...attributes }
1621
+ const safeMessage = activeRedactor.text(message);
1622
+ const span = trace2.getTracer("autter-errors").startSpan("Message", {
1623
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
1292
1624
  });
1293
1625
  span.addEvent("exception", {
1294
1626
  "exception.type": "Message",
1295
- "exception.message": message,
1627
+ "exception.message": safeMessage,
1296
1628
  "autter.severity": severity
1297
1629
  });
1298
- span.setStatus({ code: SpanStatusCode.ERROR, message });
1630
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1299
1631
  span.end();
1300
1632
  }
1301
1633
  function reportOutcome(name, message, attributes) {
1302
1634
  if (active) return active.reportOutcome(name, message, attributes);
1303
- const span = trace.getTracer("autter-outcomes").startSpan("OutcomeFailure");
1635
+ const span = trace2.getTracer("autter-outcomes").startSpan("OutcomeFailure");
1304
1636
  const safeOutcome = redactAttributes({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
1305
1637
  const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
1306
1638
  const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
@@ -1321,7 +1653,7 @@ function makeSafeCapture(options) {
1321
1653
  };
1322
1654
  }
1323
1655
  function alwaysOnTracer(name) {
1324
- return activeAlwaysOnProvider ? activeAlwaysOnProvider.getTracer(name) : trace.getTracer(name);
1656
+ return activeAlwaysOnProvider ? activeAlwaysOnProvider.getTracer(name) : trace2.getTracer(name);
1325
1657
  }
1326
1658
  function autterLlmTracer() {
1327
1659
  return alwaysOnTracer("autter-llm");
@@ -1341,7 +1673,7 @@ async function emitLlmSelftestTrace() {
1341
1673
  attributes: { "autter.selftest": true }
1342
1674
  },
1343
1675
  (llm) => {
1344
- traceId = trace.getActiveSpan()?.spanContext().traceId ?? "";
1676
+ traceId = trace2.getActiveSpan()?.spanContext().traceId ?? "";
1345
1677
  llm.setUsage({ inputTokens: 1, outputTokens: 1 });
1346
1678
  llm.setCost(0);
1347
1679
  }
@@ -1389,15 +1721,26 @@ function extractUsage(result) {
1389
1721
  const r = result;
1390
1722
  const usage = r?.usage ?? r?.message?.usage;
1391
1723
  const meta = r?.usageMetadata;
1724
+ const details = usage?.prompt_tokens_details;
1725
+ const cached = usage?.cache_read_input_tokens ?? (typeof details?.cached_tokens === "number" ? details.cached_tokens : void 0) ?? meta?.cachedContentTokenCount;
1392
1726
  const input = usage?.prompt_tokens ?? usage?.input_tokens ?? meta?.promptTokenCount ?? void 0;
1393
1727
  const output = usage?.completion_tokens ?? usage?.output_tokens ?? meta?.candidatesTokenCount ?? void 0;
1394
1728
  const responseModel = r?.model ?? r?.modelVersion;
1395
1729
  return {
1396
1730
  inputTokens: typeof input === "number" ? input : void 0,
1397
1731
  outputTokens: typeof output === "number" ? output : void 0,
1732
+ cacheReadTokens: typeof cached === "number" ? cached : void 0,
1398
1733
  responseModel: typeof responseModel === "string" ? responseModel : void 0
1399
1734
  };
1400
1735
  }
1736
+ function rollUp(model, usage) {
1737
+ recordOperationAiUsage({
1738
+ model: usage.responseModel ?? model,
1739
+ ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
1740
+ ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
1741
+ ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}
1742
+ });
1743
+ }
1401
1744
  function applyUsage(span, usage) {
1402
1745
  if (usage.inputTokens !== void 0) {
1403
1746
  span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
@@ -1410,14 +1753,10 @@ function applyUsage(span, usage) {
1410
1753
  }
1411
1754
  }
1412
1755
  function endWithError(span, err) {
1413
- if (err instanceof Error) span.recordException(err);
1414
- span.setStatus({
1415
- code: SpanStatusCode2.ERROR,
1416
- message: err instanceof Error ? err.message : String(err)
1417
- });
1756
+ recordError(span, err);
1418
1757
  span.end();
1419
1758
  }
1420
- function wrapStream(stream, span) {
1759
+ function wrapStream(stream, span, model) {
1421
1760
  let ended = false;
1422
1761
  const seen = {
1423
1762
  inputTokens: void 0,
@@ -1428,6 +1767,7 @@ function wrapStream(stream, span) {
1428
1767
  if (ended) return;
1429
1768
  ended = true;
1430
1769
  applyUsage(span, seen);
1770
+ rollUp(model, seen);
1431
1771
  span.setAttribute("autter.llm.streamed", true);
1432
1772
  if (err !== void 0) {
1433
1773
  endWithError(span, err);
@@ -1449,6 +1789,7 @@ function wrapStream(stream, span) {
1449
1789
  const usage = extractUsage(step.value);
1450
1790
  if (usage.inputTokens !== void 0) seen.inputTokens = usage.inputTokens;
1451
1791
  if (usage.outputTokens !== void 0) seen.outputTokens = usage.outputTokens;
1792
+ if (usage.cacheReadTokens !== void 0) seen.cacheReadTokens = usage.cacheReadTokens;
1452
1793
  if (usage.responseModel) seen.responseModel = usage.responseModel;
1453
1794
  } else {
1454
1795
  finish();
@@ -1494,7 +1835,7 @@ function instrumentLlmClient(client, options) {
1494
1835
  "gen_ai.system": provider,
1495
1836
  "gen_ai.request.model": request.model,
1496
1837
  ...userId ? { "autter.user_id": userId } : {},
1497
- ...options?.attributes
1838
+ ...redactWithActive(options?.attributes)
1498
1839
  }
1499
1840
  });
1500
1841
  let result;
@@ -1506,9 +1847,11 @@ function instrumentLlmClient(client, options) {
1506
1847
  }
1507
1848
  if (!(result instanceof Promise)) {
1508
1849
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
1509
- return wrapStream(result, span);
1850
+ return wrapStream(result, span, request.model);
1510
1851
  }
1511
- applyUsage(span, extractUsage(result));
1852
+ const usage = extractUsage(result);
1853
+ applyUsage(span, usage);
1854
+ rollUp(request.model, usage);
1512
1855
  span.setStatus({ code: SpanStatusCode2.OK });
1513
1856
  span.end();
1514
1857
  return result;
@@ -1516,9 +1859,11 @@ function instrumentLlmClient(client, options) {
1516
1859
  return result.then(
1517
1860
  (resolved) => {
1518
1861
  if (resolved && typeof resolved === "object" && Symbol.asyncIterator in resolved) {
1519
- return wrapStream(resolved, span);
1862
+ return wrapStream(resolved, span, request.model);
1520
1863
  }
1521
- applyUsage(span, extractUsage(resolved));
1864
+ const usage = extractUsage(resolved);
1865
+ applyUsage(span, usage);
1866
+ rollUp(request.model, usage);
1522
1867
  span.setStatus({ code: SpanStatusCode2.OK });
1523
1868
  span.end();
1524
1869
  return resolved;
@@ -1609,21 +1954,245 @@ ${frames.join("\n")}`;
1609
1954
  });
1610
1955
  } };
1611
1956
  }
1957
+
1958
+ // src/logging-only.ts
1959
+ import {
1960
+ SpanStatusCode as SpanStatusCode3,
1961
+ trace as trace3
1962
+ } from "@opentelemetry/api";
1963
+ var activeLogging = null;
1964
+ function initAutterLogging(options) {
1965
+ if (activeLogging) return activeLogging;
1966
+ if (serverActive()) {
1967
+ console.warn(
1968
+ "[autter-runtime] initAutterLogging() ignored: initAutterServer() is already active and includes logging"
1969
+ );
1970
+ return {
1971
+ captureException,
1972
+ flush: flushRuntimeLogs,
1973
+ shutdown: async () => {
1974
+ }
1975
+ };
1976
+ }
1977
+ if (isDebugEnabled() || options.debug === true) setDebugMode(true);
1978
+ const endpoint = (options.endpoint ?? "https://otlp.autter.dev").replace(/\/$/, "");
1979
+ const environment = options.environment ?? process.env.NODE_ENV ?? "production";
1980
+ const redact = makeRedactor(options.redactAttributes ?? true);
1981
+ const mode = options.exceptions ?? "auto";
1982
+ debugLog(`initialising logger-only service=${options.service} endpoint=${endpoint}`);
1983
+ function captureException2(error, attributes) {
1984
+ markCaptured(error);
1985
+ const redacted = redact(attributes);
1986
+ const span = trace3.getActiveSpan();
1987
+ if (mode === "auto" && span?.isRecording()) {
1988
+ noteOperationError(error);
1989
+ notifyException(error, redacted);
1990
+ span.setAttributes({ "autter.severity": "error", ...redacted, ...structuredErrorAttributes(error) });
1991
+ const declared = errorAttributes(error);
1992
+ if (error instanceof Error) {
1993
+ span.addEvent("exception", {
1994
+ "exception.type": error.name,
1995
+ "exception.message": error.message,
1996
+ ...error.stack ? { "exception.stacktrace": error.stack } : {},
1997
+ ...declared
1998
+ });
1999
+ } else {
2000
+ span.addEvent("exception", {
2001
+ "exception.type": "Error",
2002
+ "exception.message": String(error),
2003
+ ...declared
2004
+ });
2005
+ }
2006
+ span.setStatus({
2007
+ code: SpanStatusCode3.ERROR,
2008
+ message: error instanceof Error ? error.message : String(error)
2009
+ });
2010
+ return;
2011
+ }
2012
+ notifyException(error, redacted);
2013
+ captureExceptionAsLog(error, redacted);
2014
+ }
2015
+ const captured = /* @__PURE__ */ new WeakSet();
2016
+ configureRuntimeLogger({
2017
+ endpoint,
2018
+ apiKey: options.apiKey ?? "",
2019
+ service: options.service,
2020
+ environment,
2021
+ ...options.release ? { release: options.release } : {},
2022
+ ...options.logging ? { options: options.logging } : {},
2023
+ redact,
2024
+ reportOutcome: (name, message, attributes) => {
2025
+ runtimeLogger.error(message, {
2026
+ ...attributes,
2027
+ "autter.outcome.name": name.slice(0, 200),
2028
+ "autter.outcome.message": message.slice(0, 1e3)
2029
+ });
2030
+ },
2031
+ captureThrown: (error) => {
2032
+ if (error && typeof error === "object") {
2033
+ if (captured.has(error)) return;
2034
+ captured.add(error);
2035
+ }
2036
+ captureException2(error);
2037
+ }
2038
+ });
2039
+ setLoggerOnlyCapture(captureException2);
2040
+ const onCrash = (error) => captureException2(error, { "autter.unhandled": true });
2041
+ if (options.captureGlobalErrors !== false)
2042
+ process.on("uncaughtExceptionMonitor", onCrash);
2043
+ registerFlushTarget("logging", { forceFlush: () => flushRuntimeLogs() });
2044
+ let autoFlushHandle = options.autoFlush === false ? null : installAutterAutoFlush();
2045
+ const handle = {
2046
+ captureException: captureException2,
2047
+ flush: flushRuntimeLogs,
2048
+ shutdown: async () => {
2049
+ process.off("uncaughtExceptionMonitor", onCrash);
2050
+ setLoggerOnlyCapture(null);
2051
+ autoFlushHandle?.dispose();
2052
+ autoFlushHandle = null;
2053
+ unregisterFlushTargets();
2054
+ activeLogging = null;
2055
+ await shutdownRuntimeLogger();
2056
+ }
2057
+ };
2058
+ activeLogging = handle;
2059
+ return handle;
2060
+ }
2061
+
2062
+ // src/enrichers.ts
2063
+ function headerOf(req, name) {
2064
+ const headers = req?.headers;
2065
+ if (!headers) return void 0;
2066
+ if (typeof headers.get === "function") {
2067
+ return headers.get(name) ?? void 0;
2068
+ }
2069
+ const value = headers[name.toLowerCase()];
2070
+ if (Array.isArray(value)) return value[0] === void 0 ? void 0 : String(value[0]);
2071
+ return value === void 0 || value === null ? void 0 : String(value);
2072
+ }
2073
+ function enrichUserAgent() {
2074
+ return (event, { req }) => {
2075
+ const ua = headerOf(req, "user-agent");
2076
+ if (!ua) return;
2077
+ const info = parseUserAgent(ua);
2078
+ if (info.browser) event.attributes["user_agent.browser"] = info.browser;
2079
+ if (info.os) event.attributes["user_agent.os"] = info.os;
2080
+ event.attributes["user_agent.device"] = info.device;
2081
+ event.attributes["user_agent.bot"] = info.bot;
2082
+ };
2083
+ }
2084
+ function enrichRequestSize() {
2085
+ return (event, { req, res }) => {
2086
+ const request = Number(headerOf(req, "content-length"));
2087
+ if (Number.isFinite(request) && request >= 0)
2088
+ event.attributes["http.request.body.size"] = request;
2089
+ const raw = res?.getHeader?.(
2090
+ "content-length"
2091
+ );
2092
+ const response = Number(raw);
2093
+ if (raw !== void 0 && Number.isFinite(response) && response >= 0)
2094
+ event.attributes["http.response.body.size"] = response;
2095
+ };
2096
+ }
2097
+ var COUNTRY_HEADERS = [
2098
+ "cf-ipcountry",
2099
+ "x-vercel-ip-country",
2100
+ "cloudfront-viewer-country",
2101
+ "x-country-code",
2102
+ "fastly-geo-country-code"
2103
+ ];
2104
+ function enrichEdgeGeo() {
2105
+ return (event, { req }) => {
2106
+ for (const name of COUNTRY_HEADERS) {
2107
+ const value = headerOf(req, name)?.trim().toUpperCase();
2108
+ if (value && /^[A-Z]{2}$/.test(value) && value !== "XX" && value !== "T1") {
2109
+ event.attributes["geo.country.iso_code"] = value;
2110
+ return;
2111
+ }
2112
+ }
2113
+ };
2114
+ }
2115
+ var REGION_VARS = [
2116
+ "AUTTER_REGION",
2117
+ "AWS_REGION",
2118
+ "VERCEL_REGION",
2119
+ "FLY_REGION",
2120
+ "RAILWAY_REPLICA_REGION",
2121
+ "GOOGLE_CLOUD_REGION",
2122
+ "CLOUD_RUN_REGION",
2123
+ "RENDER_REGION",
2124
+ "AZURE_REGION"
2125
+ ];
2126
+ var COMMIT_VARS = [
2127
+ "AUTTER_COMMIT",
2128
+ "GIT_SHA",
2129
+ "GIT_COMMIT",
2130
+ "COMMIT_SHA",
2131
+ "SOURCE_COMMIT",
2132
+ "VERCEL_GIT_COMMIT_SHA",
2133
+ "GITHUB_SHA",
2134
+ "RENDER_GIT_COMMIT",
2135
+ "RAILWAY_GIT_COMMIT_SHA",
2136
+ "CF_PAGES_COMMIT_SHA",
2137
+ "HEROKU_SLUG_COMMIT",
2138
+ "SOURCE_VERSION"
2139
+ ];
2140
+ function enrichDeployment(env = process.env) {
2141
+ const pick = (names) => names.map((name) => env[name]?.trim()).find((value) => value);
2142
+ const region = pick(REGION_VARS)?.slice(0, 64);
2143
+ const commit = pick(COMMIT_VARS)?.slice(0, 64);
2144
+ return (event) => {
2145
+ if (region) event.attributes["deployment.region"] = region;
2146
+ if (commit) event.attributes["deployment.commit"] = commit;
2147
+ };
2148
+ }
1612
2149
  export {
2150
+ CODE_PATTERN,
2151
+ COMPAT_MANIFEST,
2152
+ INGESTER_VERSION_HEADER,
2153
+ RuntimeError,
2154
+ SDK_IDENTITY,
2155
+ autterErrorResponse,
2156
+ autterFastify,
2157
+ autterRequests,
1613
2158
  captureException,
1614
2159
  captureMessage,
2160
+ consoleSink,
1615
2161
  createBrowserRelayFetchHandler,
1616
2162
  createBrowserRelayHandler,
2163
+ createRuntimeLogger,
2164
+ defineRuntimeErrors,
1617
2165
  emitLlmSelftestTrace,
2166
+ enrichDeployment,
2167
+ enrichEdgeGeo,
2168
+ enrichRequestSize,
2169
+ enrichUserAgent,
2170
+ errorAttributes,
2171
+ evaluateCompat,
2172
+ fetchIngesterCompat,
2173
+ fileSink,
2174
+ flushRuntimeLogs,
2175
+ initAutterLogging,
1618
2176
  initAutterServer,
1619
2177
  installAutterAutoFlush,
1620
2178
  instrumentLlmClient,
2179
+ isRuntimeErrorLike,
1621
2180
  makeSafeCapture,
2181
+ otlpSink,
1622
2182
  redactAttributes,
2183
+ redactText,
1623
2184
  reportOutcome,
2185
+ runDoctor,
2186
+ runInBackground,
2187
+ runtimeContext,
2188
+ runtimeLogStats,
2189
+ runtimeLogger,
1624
2190
  sanitizeBrowserPayload,
1625
2191
  startCaughtExceptionSampler,
2192
+ toClientError,
1626
2193
  trackLlmCall,
1627
2194
  withLlmCall,
1628
- withProcessSpan
2195
+ withProcessSpan,
2196
+ withRuntimeOperation,
2197
+ withRuntimeRequest
1629
2198
  };