@autter/runtime-node 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -1
- package/dist/index.cjs +1126 -28
- package/dist/index.d.cts +283 -2
- package/dist/index.d.ts +283 -2
- package/dist/index.js +1133 -30
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -24,8 +24,16 @@ __export(index_exports, {
|
|
|
24
24
|
captureMessage: () => captureMessage,
|
|
25
25
|
createBrowserRelayFetchHandler: () => createBrowserRelayFetchHandler,
|
|
26
26
|
createBrowserRelayHandler: () => createBrowserRelayHandler,
|
|
27
|
+
emitLlmSelftestTrace: () => emitLlmSelftestTrace,
|
|
27
28
|
initAutterServer: () => initAutterServer,
|
|
28
|
-
|
|
29
|
+
installAutterAutoFlush: () => installAutterAutoFlush,
|
|
30
|
+
instrumentLlmClient: () => instrumentLlmClient,
|
|
31
|
+
makeSafeCapture: () => makeSafeCapture,
|
|
32
|
+
redactAttributes: () => redactAttributes,
|
|
33
|
+
sanitizeBrowserPayload: () => sanitizeBrowserPayload,
|
|
34
|
+
trackLlmCall: () => trackLlmCall,
|
|
35
|
+
withLlmCall: () => withLlmCall,
|
|
36
|
+
withProcessSpan: () => withProcessSpan
|
|
29
37
|
});
|
|
30
38
|
module.exports = __toCommonJS(index_exports);
|
|
31
39
|
|
|
@@ -229,7 +237,9 @@ function createBrowserRelayHandler(opts) {
|
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
// src/server.ts
|
|
240
|
+
var import_node_http = require("http");
|
|
232
241
|
var import_api = require("@opentelemetry/api");
|
|
242
|
+
var import_core2 = require("@opentelemetry/core");
|
|
233
243
|
var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
234
244
|
var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
|
|
235
245
|
var import_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
@@ -238,7 +248,741 @@ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
|
238
248
|
var import_sdk_node = require("@opentelemetry/sdk-node");
|
|
239
249
|
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
240
250
|
var import_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
251
|
+
|
|
252
|
+
// src/lifecycle.ts
|
|
253
|
+
var import_core = require("@opentelemetry/core");
|
|
254
|
+
var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
|
|
255
|
+
function setDebugMode(enabled) {
|
|
256
|
+
debugEnabled = enabled;
|
|
257
|
+
}
|
|
258
|
+
function isDebugEnabled() {
|
|
259
|
+
return debugEnabled;
|
|
260
|
+
}
|
|
261
|
+
function debugLog(...args) {
|
|
262
|
+
if (debugEnabled) {
|
|
263
|
+
console.error("[autter]", ...args);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
var TelemetryStats = class {
|
|
267
|
+
/** Captures queued via our APIs and not yet seen leaving an exporter. */
|
|
268
|
+
pendingCaptures = 0;
|
|
269
|
+
markCaptured(count = 1) {
|
|
270
|
+
this.pendingCaptures += count;
|
|
271
|
+
}
|
|
272
|
+
/** An exporter batch left the process successfully. */
|
|
273
|
+
onExported(spanCount) {
|
|
274
|
+
this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
|
|
275
|
+
if (debugEnabled && spanCount > 0) {
|
|
276
|
+
debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
markAllFlushed() {
|
|
280
|
+
this.pendingCaptures = 0;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var telemetryStats = new TelemetryStats();
|
|
284
|
+
var CountingExporter = class {
|
|
285
|
+
constructor(inner) {
|
|
286
|
+
this.inner = inner;
|
|
287
|
+
}
|
|
288
|
+
inner;
|
|
289
|
+
export(spans, resultCallback) {
|
|
290
|
+
this.inner.export(spans, (result) => {
|
|
291
|
+
if (result.code === import_core.ExportResultCode.SUCCESS) {
|
|
292
|
+
telemetryStats.onExported(spans.length);
|
|
293
|
+
}
|
|
294
|
+
resultCallback(result);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
shutdown() {
|
|
298
|
+
return this.inner.shutdown();
|
|
299
|
+
}
|
|
300
|
+
forceFlush() {
|
|
301
|
+
return this.inner.forceFlush?.() ?? Promise.resolve();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var flushTargets = /* @__PURE__ */ new Map();
|
|
305
|
+
function registerFlushTarget(key, target) {
|
|
306
|
+
flushTargets.set(key, target);
|
|
307
|
+
}
|
|
308
|
+
function unregisterFlushTargets() {
|
|
309
|
+
flushTargets.clear();
|
|
310
|
+
}
|
|
311
|
+
function activeFlushTargets() {
|
|
312
|
+
return [...flushTargets.values()];
|
|
313
|
+
}
|
|
314
|
+
var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
315
|
+
var SIGNAL_EXIT_CODES = {
|
|
316
|
+
SIGINT: 130,
|
|
317
|
+
SIGTERM: 143
|
|
318
|
+
};
|
|
319
|
+
function installAutterAutoFlush(options = {}) {
|
|
320
|
+
const logger = options.logger ?? console;
|
|
321
|
+
const log = options.log !== false;
|
|
322
|
+
const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
|
|
323
|
+
const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
|
|
324
|
+
const signals = options.signals ?? DEFAULT_SIGNALS;
|
|
325
|
+
let disposed = false;
|
|
326
|
+
let inFlight = null;
|
|
327
|
+
const doFlush = async () => {
|
|
328
|
+
const targets = options.targets ?? activeFlushTargets();
|
|
329
|
+
if (targets.length === 0) return true;
|
|
330
|
+
let timer;
|
|
331
|
+
const timedOut = new Promise((resolve) => {
|
|
332
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
333
|
+
});
|
|
334
|
+
const drained = Promise.allSettled(
|
|
335
|
+
targets.map(
|
|
336
|
+
(target) => Promise.resolve().then(() => target.forceFlush())
|
|
337
|
+
)
|
|
338
|
+
).then(
|
|
339
|
+
(results) => results.every((result) => result.status === "fulfilled")
|
|
340
|
+
);
|
|
341
|
+
let ok;
|
|
342
|
+
try {
|
|
343
|
+
ok = await Promise.race([drained, timedOut]);
|
|
344
|
+
} finally {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
}
|
|
347
|
+
if (ok) {
|
|
348
|
+
telemetryStats.markAllFlushed();
|
|
349
|
+
if (log) {
|
|
350
|
+
logger.log("[autter] telemetry flushed");
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
logger.error(
|
|
354
|
+
`[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return ok;
|
|
358
|
+
};
|
|
359
|
+
const flush = (reason = "exit") => {
|
|
360
|
+
if (disposed) return Promise.resolve(false);
|
|
361
|
+
if (!inFlight) {
|
|
362
|
+
inFlight = doFlush().finally(() => {
|
|
363
|
+
inFlight = null;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
|
|
367
|
+
return inFlight;
|
|
368
|
+
};
|
|
369
|
+
const signalCounts = /* @__PURE__ */ new Map();
|
|
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
|
+
for (const signal of signals) {
|
|
410
|
+
const handler = () => onSignal(signal);
|
|
411
|
+
signalHandlers.set(signal, handler);
|
|
412
|
+
process.on(signal, handler);
|
|
413
|
+
}
|
|
414
|
+
process.on("exit", onExit);
|
|
415
|
+
return {
|
|
416
|
+
flush,
|
|
417
|
+
dispose() {
|
|
418
|
+
disposed = true;
|
|
419
|
+
process.removeListener("beforeExit", onBeforeExit);
|
|
420
|
+
process.removeListener("exit", onExit);
|
|
421
|
+
for (const [signal, handler] of signalHandlers) {
|
|
422
|
+
process.removeListener(signal, handler);
|
|
423
|
+
}
|
|
424
|
+
signalHandlers.clear();
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/redact.ts
|
|
430
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
431
|
+
/e-?mail/,
|
|
432
|
+
/pass(word|wd|phrase)|^pass$/,
|
|
433
|
+
/token/,
|
|
434
|
+
/secret/,
|
|
435
|
+
/credential/,
|
|
436
|
+
/(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/,
|
|
437
|
+
/^-?x?-?authorization$|^auth(-|_|$)|bearer/,
|
|
438
|
+
/cookie/,
|
|
439
|
+
/phone|msisdn/,
|
|
440
|
+
/\bssn\b|social[-_ ]?security/,
|
|
441
|
+
/cvv|cvc|card([-_. ]?(number|num|no))?$/,
|
|
442
|
+
/credit[-_.]?card/,
|
|
443
|
+
/connection[-_.]?string/,
|
|
444
|
+
/recovery[-_.]?code|\botp\b|magic[-_.]?link/
|
|
445
|
+
];
|
|
446
|
+
var PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g;
|
|
447
|
+
var EMAIL_VALUE_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
448
|
+
var JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
449
|
+
var OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{20,}\b/g;
|
|
450
|
+
var GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g;
|
|
451
|
+
var AWS_KEY_RE = /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
|
|
452
|
+
var SLACK_TOKEN_RE = /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g;
|
|
453
|
+
var BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/gi;
|
|
454
|
+
var URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
|
|
455
|
+
function toCaseInsensitive(pattern) {
|
|
456
|
+
return typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(pattern.source, pattern.flags.includes("i") ? pattern.flags : `${pattern.flags}i`);
|
|
457
|
+
}
|
|
458
|
+
function compile(options) {
|
|
459
|
+
const keyPatterns = [...SENSITIVE_KEY_PATTERNS];
|
|
460
|
+
const valuePatterns = [
|
|
461
|
+
JWT_RE,
|
|
462
|
+
OPENAI_KEY_RE,
|
|
463
|
+
GITHUB_TOKEN_RE,
|
|
464
|
+
AWS_KEY_RE,
|
|
465
|
+
SLACK_TOKEN_RE,
|
|
466
|
+
BEARER_RE
|
|
467
|
+
];
|
|
468
|
+
if (options?.additionalKeyPatterns) {
|
|
469
|
+
for (const p of options.additionalKeyPatterns) {
|
|
470
|
+
keyPatterns.push(toCaseInsensitive(p));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (options?.additionalValuePatterns) {
|
|
474
|
+
for (const p of options.additionalValuePatterns) {
|
|
475
|
+
if (!valuePatterns.some((existing) => existing.source === (typeof p === "string" ? p : p.source))) {
|
|
476
|
+
valuePatterns.push(typeof p === "string" ? new RegExp(p, "gi") : p);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
keyPatterns,
|
|
482
|
+
valuePatterns,
|
|
483
|
+
urlCredentials: URL_CREDENTIALS_RE,
|
|
484
|
+
privateKeyBlock: PRIVATE_KEY_BLOCK_RE,
|
|
485
|
+
mask: options?.mask ?? "[redacted]",
|
|
486
|
+
scrubEmailValues: options?.scrubEmailValues !== false
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function redactString(value, r) {
|
|
490
|
+
let out = value.replace(r.privateKeyBlock, r.mask);
|
|
491
|
+
for (const re of r.valuePatterns) {
|
|
492
|
+
out = out.replace(re, r.mask);
|
|
493
|
+
}
|
|
494
|
+
out = out.replace(r.urlCredentials, "$1" + r.mask + "@");
|
|
495
|
+
if (r.scrubEmailValues) {
|
|
496
|
+
out = out.replace(EMAIL_VALUE_RE, r.mask);
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
var MAX_REDACTION_DEPTH = 64;
|
|
501
|
+
var MAX_REDACTION_WORK = 1e4;
|
|
502
|
+
var MAX_COLLECTION_ENTRIES = 1e3;
|
|
503
|
+
function canTraverse(depth, state) {
|
|
504
|
+
if (depth > MAX_REDACTION_DEPTH || state.remainingWork <= 0) return false;
|
|
505
|
+
state.remainingWork -= 1;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
function redactValue(value, r, state, depth) {
|
|
509
|
+
if (typeof value === "string") return redactString(value, r);
|
|
510
|
+
let isArray = false;
|
|
511
|
+
try {
|
|
512
|
+
isArray = Array.isArray(value);
|
|
513
|
+
} catch {
|
|
514
|
+
return r.mask;
|
|
515
|
+
}
|
|
516
|
+
if (isArray) {
|
|
517
|
+
return redactArray(value, r, state, depth);
|
|
518
|
+
}
|
|
519
|
+
if (typeof value === "object" && value !== null) {
|
|
520
|
+
return redactObject(value, r, state, depth);
|
|
521
|
+
}
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
function redactArray(value, r, state, depth) {
|
|
525
|
+
const existing = state.ancestors.get(value);
|
|
526
|
+
if (existing) return r.mask;
|
|
527
|
+
if (!canTraverse(depth, state)) return r.mask;
|
|
528
|
+
const out = [];
|
|
529
|
+
state.ancestors.set(value, out);
|
|
530
|
+
let length;
|
|
531
|
+
try {
|
|
532
|
+
length = value.length;
|
|
533
|
+
} catch {
|
|
534
|
+
state.ancestors.delete(value);
|
|
535
|
+
return r.mask;
|
|
536
|
+
}
|
|
537
|
+
const limit = Math.min(length, MAX_COLLECTION_ENTRIES);
|
|
538
|
+
for (let i = 0; i < limit; i += 1) {
|
|
539
|
+
let item;
|
|
540
|
+
try {
|
|
541
|
+
item = value[i];
|
|
542
|
+
} catch {
|
|
543
|
+
out.push(r.mask);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
out.push(redactValue(item, r, state, depth + 1));
|
|
547
|
+
}
|
|
548
|
+
if (length > limit) {
|
|
549
|
+
out.push(r.mask);
|
|
550
|
+
}
|
|
551
|
+
state.ancestors.delete(value);
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
function redactObject(value, r, state, depth) {
|
|
555
|
+
const existing = state.ancestors.get(value);
|
|
556
|
+
if (existing) return r.mask;
|
|
557
|
+
if (!canTraverse(depth, state)) return r.mask;
|
|
558
|
+
const out = {};
|
|
559
|
+
state.ancestors.set(value, out);
|
|
560
|
+
let count = 0;
|
|
561
|
+
let truncated = false;
|
|
562
|
+
try {
|
|
563
|
+
for (const key in value) {
|
|
564
|
+
if (!Object.prototype.propertyIsEnumerable.call(
|
|
565
|
+
value,
|
|
566
|
+
key
|
|
567
|
+
)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (count >= MAX_COLLECTION_ENTRIES) {
|
|
571
|
+
truncated = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
let nestedValue;
|
|
575
|
+
try {
|
|
576
|
+
nestedValue = value[key];
|
|
577
|
+
} catch {
|
|
578
|
+
out[key] = r.mask;
|
|
579
|
+
count += 1;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
count += 1;
|
|
583
|
+
out[key] = isSensitiveKey(key, nestedValue, r) ? r.mask : redactValue(
|
|
584
|
+
nestedValue,
|
|
585
|
+
r,
|
|
586
|
+
state,
|
|
587
|
+
depth + 1
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
truncated = true;
|
|
592
|
+
}
|
|
593
|
+
if (truncated) {
|
|
594
|
+
out.__redaction_truncated__ = r.mask;
|
|
595
|
+
}
|
|
596
|
+
state.ancestors.delete(value);
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
var USAGE_TOKEN_KEYS = /* @__PURE__ */ new Set([
|
|
600
|
+
"input_tokens",
|
|
601
|
+
"output_tokens",
|
|
602
|
+
"prompt_tokens",
|
|
603
|
+
"completion_tokens",
|
|
604
|
+
"total_tokens",
|
|
605
|
+
"token_count",
|
|
606
|
+
"gen_ai.usage.input_tokens",
|
|
607
|
+
"gen_ai.usage.output_tokens",
|
|
608
|
+
"gen_ai.usage.prompt_tokens",
|
|
609
|
+
"gen_ai.usage.completion_tokens",
|
|
610
|
+
"gen_ai.usage.total_tokens",
|
|
611
|
+
"gen_ai.usage.token_count"
|
|
612
|
+
]);
|
|
613
|
+
function isSensitiveKey(key, value, r) {
|
|
614
|
+
const lowered = key.toLowerCase();
|
|
615
|
+
if (USAGE_TOKEN_KEYS.has(lowered)) {
|
|
616
|
+
return !(typeof value === "number" && Number.isFinite(value) && value >= 0);
|
|
617
|
+
}
|
|
618
|
+
return r.keyPatterns.some((re) => re.test(lowered));
|
|
619
|
+
}
|
|
620
|
+
function redactAttributes(attributes, options) {
|
|
621
|
+
const r = compile(options);
|
|
622
|
+
return redactWith(attributes, r);
|
|
623
|
+
}
|
|
624
|
+
function redactWith(attributes, r) {
|
|
625
|
+
const out = {};
|
|
626
|
+
if (!attributes) return out;
|
|
627
|
+
const state = {
|
|
628
|
+
ancestors: /* @__PURE__ */ new WeakMap(),
|
|
629
|
+
remainingWork: MAX_REDACTION_WORK
|
|
630
|
+
};
|
|
631
|
+
let count = 0;
|
|
632
|
+
let truncated = false;
|
|
633
|
+
try {
|
|
634
|
+
for (const key in attributes) {
|
|
635
|
+
if (!Object.prototype.propertyIsEnumerable.call(
|
|
636
|
+
attributes,
|
|
637
|
+
key
|
|
638
|
+
)) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (count >= MAX_COLLECTION_ENTRIES || state.remainingWork <= 0) {
|
|
642
|
+
truncated = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
let value;
|
|
646
|
+
try {
|
|
647
|
+
value = attributes[key];
|
|
648
|
+
} catch {
|
|
649
|
+
out[key] = r.mask;
|
|
650
|
+
count += 1;
|
|
651
|
+
state.remainingWork -= 1;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
count += 1;
|
|
655
|
+
state.remainingWork -= 1;
|
|
656
|
+
if (value === void 0) continue;
|
|
657
|
+
out[key] = isSensitiveKey(key, value, r) ? r.mask : redactValue(value, r, state, 0);
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
truncated = true;
|
|
661
|
+
}
|
|
662
|
+
if (truncated) {
|
|
663
|
+
out.__redaction_truncated__ = r.mask;
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
function makeRedactor(options) {
|
|
668
|
+
if (options === false) {
|
|
669
|
+
return (attributes) => ({ ...attributes ?? {} });
|
|
670
|
+
}
|
|
671
|
+
const r = compile(options === true ? void 0 : options);
|
|
672
|
+
return (attributes) => redactWith(attributes, r);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/server.ts
|
|
676
|
+
var LLM_MARKER_ATTRIBUTES = [
|
|
677
|
+
"gen_ai.operation.name",
|
|
678
|
+
"gen_ai.system",
|
|
679
|
+
"gen_ai.provider.name",
|
|
680
|
+
"gen_ai.request.model",
|
|
681
|
+
"autter.llm.provider"
|
|
682
|
+
];
|
|
683
|
+
function isLlmSpan(name, attributes) {
|
|
684
|
+
if (name.startsWith("ai.") || name.startsWith("gen_ai.")) return true;
|
|
685
|
+
return LLM_MARKER_ATTRIBUTES.some((key) => attributes[key] !== void 0);
|
|
686
|
+
}
|
|
687
|
+
var LlmAwareSampler = class {
|
|
688
|
+
constructor(delegate) {
|
|
689
|
+
this.delegate = delegate;
|
|
690
|
+
}
|
|
691
|
+
delegate;
|
|
692
|
+
shouldSample(ctx, traceId, spanName, spanKind, attributes, links) {
|
|
693
|
+
if (isLlmSpan(spanName, attributes)) {
|
|
694
|
+
return { decision: import_sdk_trace_base.SamplingDecision.RECORD_AND_SAMPLED };
|
|
695
|
+
}
|
|
696
|
+
return this.delegate.shouldSample(
|
|
697
|
+
ctx,
|
|
698
|
+
traceId,
|
|
699
|
+
spanName,
|
|
700
|
+
spanKind,
|
|
701
|
+
attributes,
|
|
702
|
+
links
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
toString() {
|
|
706
|
+
return `LlmAware(${this.delegate.toString()})`;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var RecordUnsampledSampler = class {
|
|
710
|
+
constructor(delegate) {
|
|
711
|
+
this.delegate = delegate;
|
|
712
|
+
}
|
|
713
|
+
delegate;
|
|
714
|
+
shouldSample(ctx, traceId, spanName, spanKind, attributes, links) {
|
|
715
|
+
const result = this.delegate.shouldSample(
|
|
716
|
+
ctx,
|
|
717
|
+
traceId,
|
|
718
|
+
spanName,
|
|
719
|
+
spanKind,
|
|
720
|
+
attributes,
|
|
721
|
+
links
|
|
722
|
+
);
|
|
723
|
+
if (result.decision === import_sdk_trace_base.SamplingDecision.NOT_RECORD) {
|
|
724
|
+
return { ...result, decision: import_sdk_trace_base.SamplingDecision.RECORD };
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
toString() {
|
|
729
|
+
return `RecordUnsampled(${this.delegate.toString()})`;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var RETENTION_MAX_SPANS_PER_TRACE = 256;
|
|
733
|
+
var RETENTION_MAX_BUFFERED_SPANS = 5e3;
|
|
734
|
+
var RETENTION_SWEEP_INTERVAL_MS = 1e4;
|
|
735
|
+
var RETENTION_TRACE_TTL_MS = 3e4;
|
|
736
|
+
function spanIndicatesError(span) {
|
|
737
|
+
if (span.status.code === import_api.SpanStatusCode.ERROR) return true;
|
|
738
|
+
return span.events.some((event) => event.name === "exception");
|
|
739
|
+
}
|
|
740
|
+
var TAIL_RETAINED_ATTR = "autter.tail_retained";
|
|
741
|
+
function withSampledFlag(span) {
|
|
742
|
+
const spanContext = {
|
|
743
|
+
...span.spanContext(),
|
|
744
|
+
traceFlags: span.spanContext().traceFlags | import_api.TraceFlags.SAMPLED
|
|
745
|
+
};
|
|
746
|
+
return {
|
|
747
|
+
name: span.name,
|
|
748
|
+
kind: span.kind,
|
|
749
|
+
spanContext: () => spanContext,
|
|
750
|
+
parentSpanId: span.parentSpanId,
|
|
751
|
+
startTime: span.startTime,
|
|
752
|
+
endTime: span.endTime,
|
|
753
|
+
status: span.status,
|
|
754
|
+
attributes: { ...span.attributes, [TAIL_RETAINED_ATTR]: true },
|
|
755
|
+
links: span.links,
|
|
756
|
+
events: span.events,
|
|
757
|
+
duration: span.duration,
|
|
758
|
+
ended: span.ended,
|
|
759
|
+
resource: span.resource,
|
|
760
|
+
instrumentationLibrary: span.instrumentationLibrary,
|
|
761
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
762
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
763
|
+
droppedLinksCount: span.droppedLinksCount
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
var ErrorTraceRetentionProcessor = class {
|
|
767
|
+
constructor(inner) {
|
|
768
|
+
this.inner = inner;
|
|
769
|
+
this.sweeper = setInterval(() => this.sweep(), RETENTION_SWEEP_INTERVAL_MS);
|
|
770
|
+
this.sweeper.unref?.();
|
|
771
|
+
}
|
|
772
|
+
inner;
|
|
773
|
+
traces = /* @__PURE__ */ new Map();
|
|
774
|
+
bufferedSpans = 0;
|
|
775
|
+
sweeper;
|
|
776
|
+
onStart(_span, _parentContext) {
|
|
777
|
+
}
|
|
778
|
+
onEnd(span) {
|
|
779
|
+
const ctx = span.spanContext();
|
|
780
|
+
if ((ctx.traceFlags & import_api.TraceFlags.SAMPLED) !== 0) return;
|
|
781
|
+
const entry = this.entryFor(ctx.traceId);
|
|
782
|
+
if (entry.retained) {
|
|
783
|
+
this.forward(span);
|
|
784
|
+
} else if (spanIndicatesError(span)) {
|
|
785
|
+
entry.retained = true;
|
|
786
|
+
this.flush(entry);
|
|
787
|
+
this.forward(span);
|
|
788
|
+
} else if (entry.spans.length < RETENTION_MAX_SPANS_PER_TRACE) {
|
|
789
|
+
entry.spans.push(span);
|
|
790
|
+
this.bufferedSpans++;
|
|
791
|
+
this.evictIfOverBudget();
|
|
792
|
+
}
|
|
793
|
+
if (span.parentSpanId === void 0 && !entry.retained) {
|
|
794
|
+
this.drop(ctx.traceId);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** Promote the active trace, if any — called on captured errors. */
|
|
798
|
+
retainActiveTrace() {
|
|
799
|
+
const active2 = import_api.trace.getActiveSpan()?.spanContext();
|
|
800
|
+
if (!active2 || (active2.traceFlags & import_api.TraceFlags.SAMPLED) !== 0) return;
|
|
801
|
+
const entry = this.entryFor(active2.traceId);
|
|
802
|
+
if (entry.retained) return;
|
|
803
|
+
entry.retained = true;
|
|
804
|
+
this.flush(entry);
|
|
805
|
+
}
|
|
806
|
+
forceFlush() {
|
|
807
|
+
return this.inner.forceFlush();
|
|
808
|
+
}
|
|
809
|
+
shutdown() {
|
|
810
|
+
clearInterval(this.sweeper);
|
|
811
|
+
this.traces.clear();
|
|
812
|
+
this.bufferedSpans = 0;
|
|
813
|
+
return this.inner.shutdown();
|
|
814
|
+
}
|
|
815
|
+
entryFor(traceId) {
|
|
816
|
+
let entry = this.traces.get(traceId);
|
|
817
|
+
if (!entry) {
|
|
818
|
+
entry = { spans: [], retained: false, firstSeenAtMs: Date.now() };
|
|
819
|
+
this.traces.set(traceId, entry);
|
|
820
|
+
}
|
|
821
|
+
return entry;
|
|
822
|
+
}
|
|
823
|
+
forward(span) {
|
|
824
|
+
this.inner.onEnd(withSampledFlag(span));
|
|
825
|
+
}
|
|
826
|
+
flush(entry) {
|
|
827
|
+
for (const span of entry.spans) this.forward(span);
|
|
828
|
+
this.bufferedSpans -= entry.spans.length;
|
|
829
|
+
entry.spans = [];
|
|
830
|
+
}
|
|
831
|
+
drop(traceId) {
|
|
832
|
+
const entry = this.traces.get(traceId);
|
|
833
|
+
if (!entry) return;
|
|
834
|
+
this.bufferedSpans -= entry.spans.length;
|
|
835
|
+
this.traces.delete(traceId);
|
|
836
|
+
}
|
|
837
|
+
evictIfOverBudget() {
|
|
838
|
+
if (this.bufferedSpans <= RETENTION_MAX_BUFFERED_SPANS) return;
|
|
839
|
+
for (const [traceId, entry] of this.traces) {
|
|
840
|
+
if (entry.spans.length === 0) continue;
|
|
841
|
+
this.drop(traceId);
|
|
842
|
+
if (this.bufferedSpans <= RETENTION_MAX_BUFFERED_SPANS) return;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
sweep() {
|
|
846
|
+
const cutoff = Date.now() - RETENTION_TRACE_TTL_MS;
|
|
847
|
+
for (const [traceId, entry] of this.traces) {
|
|
848
|
+
if (entry.firstSeenAtMs <= cutoff) this.drop(traceId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
function llmBaseAttributes(info) {
|
|
853
|
+
return {
|
|
854
|
+
"gen_ai.operation.name": info.operation ?? "chat",
|
|
855
|
+
// Both keys: gen_ai.system is the pre-1.37 semconv name and what
|
|
856
|
+
// most tooling still matches on.
|
|
857
|
+
"gen_ai.provider.name": info.provider,
|
|
858
|
+
"gen_ai.system": info.provider,
|
|
859
|
+
"gen_ai.request.model": info.model,
|
|
860
|
+
...info.userId ? { "autter.user_id": info.userId } : {},
|
|
861
|
+
...info.sessionId ? { "autter.session_id": info.sessionId } : {},
|
|
862
|
+
// Caller-supplied extras go through redaction like every other
|
|
863
|
+
// custom attribute family (prompts can contain emails/keys).
|
|
864
|
+
...info.attributes ? activeRedactor(info.attributes) : {}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function usageAttributes(usage) {
|
|
868
|
+
const attrs = {};
|
|
869
|
+
if (usage.inputTokens !== void 0) {
|
|
870
|
+
attrs["gen_ai.usage.input_tokens"] = Math.max(0, Math.round(usage.inputTokens));
|
|
871
|
+
}
|
|
872
|
+
if (usage.outputTokens !== void 0) {
|
|
873
|
+
attrs["gen_ai.usage.output_tokens"] = Math.max(
|
|
874
|
+
0,
|
|
875
|
+
Math.round(usage.outputTokens)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (usage.costUsd !== void 0) {
|
|
879
|
+
attrs["autter.llm.cost_usd"] = usage.costUsd;
|
|
880
|
+
}
|
|
881
|
+
return attrs;
|
|
882
|
+
}
|
|
883
|
+
async function runLlmSpan(tracer, info, fn) {
|
|
884
|
+
const span = tracer.startSpan(`${info.operation ?? "chat"} ${info.model}`, {
|
|
885
|
+
kind: import_api.SpanKind.CLIENT,
|
|
886
|
+
attributes: llmBaseAttributes(info)
|
|
887
|
+
});
|
|
888
|
+
const setResponseModel = (model) => span.setAttribute("gen_ai.response.model", model);
|
|
889
|
+
const handle = {
|
|
890
|
+
setUsage: (usage) => span.setAttributes(usageAttributes(usage)),
|
|
891
|
+
setCost: (usd) => span.setAttribute("autter.llm.cost_usd", usd),
|
|
892
|
+
setResponseModel,
|
|
893
|
+
setModel: setResponseModel,
|
|
894
|
+
setAttributes: (attributes) => span.setAttributes(attributes)
|
|
895
|
+
};
|
|
896
|
+
try {
|
|
897
|
+
const result = await import_api.context.with(
|
|
898
|
+
import_api.trace.setSpan(import_api.context.active(), span),
|
|
899
|
+
() => fn(handle)
|
|
900
|
+
);
|
|
901
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
902
|
+
return result;
|
|
903
|
+
} catch (err) {
|
|
904
|
+
if (err instanceof Error) {
|
|
905
|
+
span.recordException(err);
|
|
906
|
+
span.setAttribute("error.type", err.name);
|
|
907
|
+
}
|
|
908
|
+
span.setStatus({
|
|
909
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
910
|
+
message: err instanceof Error ? err.message : String(err)
|
|
911
|
+
});
|
|
912
|
+
throw err;
|
|
913
|
+
} finally {
|
|
914
|
+
span.end();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function recordLlmCall(tracer, call) {
|
|
918
|
+
const durationMs = Math.max(0, call.durationMs ?? 0);
|
|
919
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
920
|
+
const startTime = new Date(endTime.getTime() - durationMs);
|
|
921
|
+
const span = tracer.startSpan(
|
|
922
|
+
`${call.operation ?? "chat"} ${call.model}`,
|
|
923
|
+
{
|
|
924
|
+
kind: import_api.SpanKind.CLIENT,
|
|
925
|
+
startTime,
|
|
926
|
+
attributes: { ...llmBaseAttributes(call), ...usageAttributes(call) }
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
if (call.error !== void 0 && call.error !== null) {
|
|
930
|
+
if (call.error instanceof Error) {
|
|
931
|
+
span.recordException(call.error);
|
|
932
|
+
span.setAttribute("error.type", call.error.name);
|
|
933
|
+
}
|
|
934
|
+
span.setStatus({
|
|
935
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
936
|
+
message: call.error instanceof Error ? call.error.message : String(call.error)
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
940
|
+
}
|
|
941
|
+
span.end(endTime);
|
|
942
|
+
}
|
|
943
|
+
async function runWithSpan(tracer, name, fn, attributes) {
|
|
944
|
+
const span = tracer.startSpan(name, { attributes });
|
|
945
|
+
try {
|
|
946
|
+
const result = await import_api.context.with(
|
|
947
|
+
import_api.trace.setSpan(import_api.context.active(), span),
|
|
948
|
+
fn
|
|
949
|
+
);
|
|
950
|
+
span.setStatus({ code: import_api.SpanStatusCode.OK });
|
|
951
|
+
return result;
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (err instanceof Error) span.recordException(err);
|
|
954
|
+
span.setStatus({
|
|
955
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
956
|
+
message: err instanceof Error ? err.message : String(err)
|
|
957
|
+
});
|
|
958
|
+
throw err;
|
|
959
|
+
} finally {
|
|
960
|
+
span.end();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function expressRouteOf(req) {
|
|
964
|
+
const props = req;
|
|
965
|
+
const path = props?.route?.path;
|
|
966
|
+
if (typeof path !== "string" || path === "") return null;
|
|
967
|
+
const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
|
|
968
|
+
const route = base + path;
|
|
969
|
+
return route.startsWith("/") ? route : null;
|
|
970
|
+
}
|
|
971
|
+
function captureExpressRoute(_span, response) {
|
|
972
|
+
if (!(response instanceof import_node_http.ServerResponse)) return;
|
|
973
|
+
const rpcMetadata = (0, import_core2.getRPCMetadata)(import_api.context.active());
|
|
974
|
+
if (rpcMetadata?.type !== import_core2.RPCType.HTTP) return;
|
|
975
|
+
const setRoute = () => {
|
|
976
|
+
if (rpcMetadata.route) return;
|
|
977
|
+
const route = expressRouteOf(response.req);
|
|
978
|
+
if (route) rpcMetadata.route = route;
|
|
979
|
+
};
|
|
980
|
+
response.prependListener("finish", setRoute);
|
|
981
|
+
response.prependListener("close", setRoute);
|
|
982
|
+
}
|
|
241
983
|
var active = null;
|
|
984
|
+
var activeAlwaysOnProvider = null;
|
|
985
|
+
var activeRedactor = makeRedactor(true);
|
|
242
986
|
function initAutterServer(options) {
|
|
243
987
|
if (active) return active;
|
|
244
988
|
const endpoint = (options.endpoint ?? "https://otlp.autter.dev").replace(
|
|
@@ -247,65 +991,112 @@ function initAutterServer(options) {
|
|
|
247
991
|
);
|
|
248
992
|
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
249
993
|
const environment = options.environment ?? process.env.NODE_ENV ?? "production";
|
|
994
|
+
if (isDebugEnabled() || options.debug === true) setDebugMode(true);
|
|
995
|
+
debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
|
|
996
|
+
activeRedactor = makeRedactor(options.redactAttributes ?? true);
|
|
250
997
|
const resource = new import_resources.Resource({
|
|
251
998
|
[import_semantic_conventions.ATTR_SERVICE_NAME]: options.service,
|
|
252
999
|
...options.release ? { [import_semantic_conventions.ATTR_SERVICE_VERSION]: options.release } : {},
|
|
253
|
-
"deployment.environment": environment
|
|
1000
|
+
"deployment.environment": environment,
|
|
1001
|
+
// Tells the ingester request metrics arrive on the metrics pipe, so
|
|
1002
|
+
// it must not also fold our server spans into usage rollups (that
|
|
1003
|
+
// would double-count every sampled request).
|
|
1004
|
+
"autter.metrics_wired": true
|
|
1005
|
+
});
|
|
1006
|
+
const headSampler = new import_sdk_trace_base.ParentBasedSampler({
|
|
1007
|
+
root: new import_sdk_trace_base.TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01)
|
|
1008
|
+
});
|
|
1009
|
+
const retainOnError = options.retainTracesOnError !== false;
|
|
1010
|
+
let sampler = retainOnError ? new RecordUnsampledSampler(headSampler) : headSampler;
|
|
1011
|
+
if (options.llmTracing !== false) sampler = new LlmAwareSampler(sampler);
|
|
1012
|
+
const errorTraceBuffer = retainOnError ? new ErrorTraceRetentionProcessor(
|
|
1013
|
+
new import_sdk_trace_base.BatchSpanProcessor(
|
|
1014
|
+
new CountingExporter(
|
|
1015
|
+
new import_exporter_trace_otlp_http.OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
|
|
1016
|
+
),
|
|
1017
|
+
{ scheduledDelayMillis: 2e3 }
|
|
1018
|
+
)
|
|
1019
|
+
) : null;
|
|
1020
|
+
const mainSpanProcessor = new import_sdk_trace_base.BatchSpanProcessor(
|
|
1021
|
+
new CountingExporter(
|
|
1022
|
+
new import_exporter_trace_otlp_http.OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
const metricReader = new import_sdk_metrics.PeriodicExportingMetricReader({
|
|
1026
|
+
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
|
|
1027
|
+
url: `${endpoint}/v1/metrics`,
|
|
1028
|
+
headers,
|
|
1029
|
+
// Deltas, not lifetime totals: the ingester SUMs data points
|
|
1030
|
+
// into runtime_metrics_1m, and the default (cumulative)
|
|
1031
|
+
// temporality would re-count every past request on each
|
|
1032
|
+
// 60 s export.
|
|
1033
|
+
temporalityPreference: import_exporter_metrics_otlp_http.AggregationTemporalityPreference.DELTA
|
|
1034
|
+
}),
|
|
1035
|
+
exportIntervalMillis: options.metricIntervalMs ?? 6e4
|
|
254
1036
|
});
|
|
255
1037
|
const sdk = new import_sdk_node.NodeSDK({
|
|
256
1038
|
resource,
|
|
257
|
-
sampler
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}),
|
|
264
|
-
metricReader: new import_sdk_metrics.PeriodicExportingMetricReader({
|
|
265
|
-
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
|
|
266
|
-
url: `${endpoint}/v1/metrics`,
|
|
267
|
-
headers
|
|
268
|
-
}),
|
|
269
|
-
exportIntervalMillis: options.metricIntervalMs ?? 6e4
|
|
270
|
-
}),
|
|
1039
|
+
sampler,
|
|
1040
|
+
spanProcessors: [
|
|
1041
|
+
mainSpanProcessor,
|
|
1042
|
+
...errorTraceBuffer ? [errorTraceBuffer] : []
|
|
1043
|
+
],
|
|
1044
|
+
metricReader,
|
|
271
1045
|
instrumentations: [
|
|
272
|
-
new import_instrumentation_http.HttpInstrumentation(),
|
|
1046
|
+
new import_instrumentation_http.HttpInstrumentation({ responseHook: captureExpressRoute }),
|
|
273
1047
|
...options.instrumentations ?? []
|
|
274
1048
|
]
|
|
275
1049
|
});
|
|
276
1050
|
sdk.start();
|
|
277
|
-
const
|
|
1051
|
+
const alwaysOnProvider = new import_sdk_trace_base.BasicTracerProvider({
|
|
278
1052
|
resource,
|
|
279
1053
|
sampler: new import_sdk_trace_base.AlwaysOnSampler(),
|
|
280
1054
|
spanProcessors: [
|
|
281
1055
|
new import_sdk_trace_base.BatchSpanProcessor(
|
|
282
|
-
new
|
|
1056
|
+
new CountingExporter(
|
|
1057
|
+
new import_exporter_trace_otlp_http.OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
|
|
1058
|
+
),
|
|
283
1059
|
{ scheduledDelayMillis: 2e3 }
|
|
284
1060
|
)
|
|
285
1061
|
]
|
|
286
1062
|
});
|
|
287
|
-
const errorTracer =
|
|
1063
|
+
const errorTracer = alwaysOnProvider.getTracer("autter-errors");
|
|
1064
|
+
const processTracer = alwaysOnProvider.getTracer("autter-processes");
|
|
1065
|
+
const llmTracer = alwaysOnProvider.getTracer("autter-llm");
|
|
288
1066
|
function captureException2(error, attributes) {
|
|
1067
|
+
errorTraceBuffer?.retainActiveTrace();
|
|
1068
|
+
telemetryStats.markCaptured();
|
|
289
1069
|
const isError = error instanceof Error;
|
|
290
1070
|
const message = isError ? error.message : String(error);
|
|
291
1071
|
const span = errorTracer.startSpan(isError ? error.name : "Error", {
|
|
292
|
-
attributes: {
|
|
1072
|
+
attributes: {
|
|
1073
|
+
"autter.severity": "error",
|
|
1074
|
+
// Redaction is applied here, not at export time: PII must not
|
|
1075
|
+
// leave the process even if an exporter misbehaves.
|
|
1076
|
+
...activeRedactor(attributes)
|
|
1077
|
+
}
|
|
293
1078
|
});
|
|
294
|
-
if (isError) {
|
|
1079
|
+
if (isError && error.stack) {
|
|
295
1080
|
span.recordException(error);
|
|
296
1081
|
} else {
|
|
1082
|
+
const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
|
|
297
1083
|
span.addEvent("exception", {
|
|
298
|
-
"exception.type": "Error",
|
|
299
|
-
"exception.message": message
|
|
1084
|
+
"exception.type": isError ? error.name : "Error",
|
|
1085
|
+
"exception.message": message,
|
|
1086
|
+
...stack ? { "exception.stacktrace": stack } : {}
|
|
300
1087
|
});
|
|
301
1088
|
}
|
|
302
1089
|
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message });
|
|
303
1090
|
span.end();
|
|
304
1091
|
}
|
|
305
1092
|
function captureMessage2(message, severity = "warning", attributes) {
|
|
1093
|
+
if (severity === "error" || severity === "fatal") {
|
|
1094
|
+
errorTraceBuffer?.retainActiveTrace();
|
|
1095
|
+
}
|
|
1096
|
+
telemetryStats.markCaptured();
|
|
306
1097
|
const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureMessage")).join("\n");
|
|
307
1098
|
const span = errorTracer.startSpan("Message", {
|
|
308
|
-
attributes: { "autter.severity": severity, ...attributes }
|
|
1099
|
+
attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
|
|
309
1100
|
});
|
|
310
1101
|
span.addEvent("exception", {
|
|
311
1102
|
"exception.type": "Message",
|
|
@@ -319,19 +1110,54 @@ function initAutterServer(options) {
|
|
|
319
1110
|
if (options.captureGlobalErrors !== false) {
|
|
320
1111
|
process.on("uncaughtExceptionMonitor", (err) => {
|
|
321
1112
|
captureException2(err, { "autter.unhandled": true });
|
|
322
|
-
void
|
|
1113
|
+
void flushTarget.forceFlush();
|
|
1114
|
+
});
|
|
1115
|
+
process.on("unhandledRejection", (reason) => {
|
|
1116
|
+
captureException2(reason, {
|
|
1117
|
+
"autter.unhandled": true,
|
|
1118
|
+
...reason instanceof Error ? {} : { "autter.severity": "warning" }
|
|
323
1119
|
});
|
|
324
1120
|
});
|
|
325
1121
|
}
|
|
1122
|
+
const flushTarget = {
|
|
1123
|
+
forceFlush: async () => {
|
|
1124
|
+
const results = await Promise.allSettled([
|
|
1125
|
+
Promise.resolve().then(() => alwaysOnProvider.forceFlush()),
|
|
1126
|
+
Promise.resolve().then(() => mainSpanProcessor.forceFlush()),
|
|
1127
|
+
...errorTraceBuffer ? [Promise.resolve().then(() => errorTraceBuffer.forceFlush())] : [],
|
|
1128
|
+
Promise.resolve().then(() => metricReader.forceFlush())
|
|
1129
|
+
]);
|
|
1130
|
+
const failed = results.find(
|
|
1131
|
+
(result) => result.status === "rejected"
|
|
1132
|
+
);
|
|
1133
|
+
if (failed) {
|
|
1134
|
+
throw failed.reason;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
registerFlushTarget("active-server", flushTarget);
|
|
1139
|
+
let autoFlushHandle = null;
|
|
1140
|
+
if (options.autoFlush !== false) {
|
|
1141
|
+
autoFlushHandle = installAutterAutoFlush();
|
|
1142
|
+
}
|
|
326
1143
|
const server = {
|
|
327
1144
|
captureException: captureException2,
|
|
328
1145
|
captureMessage: captureMessage2,
|
|
1146
|
+
withProcessSpan: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
|
|
1147
|
+
withLlmCall: (info, fn) => runLlmSpan(llmTracer, info, fn),
|
|
1148
|
+
trackLlmCall: (call) => recordLlmCall(llmTracer, call),
|
|
329
1149
|
shutdown: async () => {
|
|
330
1150
|
active = null;
|
|
331
|
-
|
|
1151
|
+
activeAlwaysOnProvider = null;
|
|
1152
|
+
autoFlushHandle?.dispose();
|
|
1153
|
+
autoFlushHandle = null;
|
|
1154
|
+
unregisterFlushTargets();
|
|
1155
|
+
telemetryStats.markAllFlushed();
|
|
1156
|
+
await Promise.allSettled([alwaysOnProvider.shutdown(), sdk.shutdown()]);
|
|
332
1157
|
}
|
|
333
1158
|
};
|
|
334
1159
|
active = server;
|
|
1160
|
+
activeAlwaysOnProvider = alwaysOnProvider;
|
|
335
1161
|
return server;
|
|
336
1162
|
}
|
|
337
1163
|
function captureException(error, attributes) {
|
|
@@ -347,6 +1173,21 @@ function captureException(error, attributes) {
|
|
|
347
1173
|
});
|
|
348
1174
|
span.end();
|
|
349
1175
|
}
|
|
1176
|
+
function withProcessSpan(name, fn, attributes) {
|
|
1177
|
+
if (active) return active.withProcessSpan(name, fn, attributes);
|
|
1178
|
+
return runWithSpan(import_api.trace.getTracer("autter-processes"), name, fn, attributes);
|
|
1179
|
+
}
|
|
1180
|
+
function withLlmCall(info, fn) {
|
|
1181
|
+
if (active) return active.withLlmCall(info, fn);
|
|
1182
|
+
return runLlmSpan(import_api.trace.getTracer("autter-llm"), info, fn);
|
|
1183
|
+
}
|
|
1184
|
+
function trackLlmCall(call) {
|
|
1185
|
+
if (active) {
|
|
1186
|
+
active.trackLlmCall(call);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
recordLlmCall(import_api.trace.getTracer("autter-llm"), call);
|
|
1190
|
+
}
|
|
350
1191
|
function captureMessage(message, severity = "warning", attributes) {
|
|
351
1192
|
if (active) {
|
|
352
1193
|
active.captureMessage(message, severity, attributes);
|
|
@@ -363,12 +1204,269 @@ function captureMessage(message, severity = "warning", attributes) {
|
|
|
363
1204
|
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message });
|
|
364
1205
|
span.end();
|
|
365
1206
|
}
|
|
1207
|
+
function makeSafeCapture(options) {
|
|
1208
|
+
const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
|
|
1209
|
+
return {
|
|
1210
|
+
captureException: (error, attributes) => captureException(error, clean(attributes)),
|
|
1211
|
+
captureMessage: (message, severity, attributes) => captureMessage(message, severity ?? "warning", clean(attributes))
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function alwaysOnTracer(name) {
|
|
1215
|
+
return activeAlwaysOnProvider ? activeAlwaysOnProvider.getTracer(name) : import_api.trace.getTracer(name);
|
|
1216
|
+
}
|
|
1217
|
+
function autterLlmTracer() {
|
|
1218
|
+
return alwaysOnTracer("autter-llm");
|
|
1219
|
+
}
|
|
1220
|
+
async function emitLlmSelftestTrace() {
|
|
1221
|
+
const provider = activeAlwaysOnProvider;
|
|
1222
|
+
if (!provider) {
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
"emitLlmSelftestTrace() requires initAutterServer() to have been called"
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
let traceId = "";
|
|
1228
|
+
await withLlmCall(
|
|
1229
|
+
{
|
|
1230
|
+
provider: "autter-selftest",
|
|
1231
|
+
model: "autter-selftest",
|
|
1232
|
+
attributes: { "autter.selftest": true }
|
|
1233
|
+
},
|
|
1234
|
+
(llm) => {
|
|
1235
|
+
traceId = import_api.trace.getActiveSpan()?.spanContext().traceId ?? "";
|
|
1236
|
+
llm.setUsage({ inputTokens: 1, outputTokens: 1 });
|
|
1237
|
+
llm.setCost(0);
|
|
1238
|
+
}
|
|
1239
|
+
);
|
|
1240
|
+
await provider.forceFlush();
|
|
1241
|
+
return { traceId };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/llm-instrument.ts
|
|
1245
|
+
var import_api2 = require("@opentelemetry/api");
|
|
1246
|
+
var CALL_METHODS = /* @__PURE__ */ new Set([
|
|
1247
|
+
"create",
|
|
1248
|
+
// openai chat/completions/embeddings/responses, anthropic messages
|
|
1249
|
+
"stream",
|
|
1250
|
+
// anthropic messages.stream
|
|
1251
|
+
"generateContent",
|
|
1252
|
+
"generateContentStream",
|
|
1253
|
+
// google genai
|
|
1254
|
+
"embedContent",
|
|
1255
|
+
"batchEmbedContents",
|
|
1256
|
+
"parse"
|
|
1257
|
+
// openai beta structured outputs
|
|
1258
|
+
]);
|
|
1259
|
+
function detectProvider(client) {
|
|
1260
|
+
const name = client.constructor?.name?.toLowerCase() ?? "";
|
|
1261
|
+
for (const known of ["openai", "anthropic", "mistral", "groq", "cohere"]) {
|
|
1262
|
+
if (name.includes(known)) return known;
|
|
1263
|
+
}
|
|
1264
|
+
if (name.includes("google") || name.includes("genai")) return "google";
|
|
1265
|
+
const baseUrl = String(
|
|
1266
|
+
client.baseURL ?? client.baseUrl ?? ""
|
|
1267
|
+
);
|
|
1268
|
+
const host = baseUrl.match(/https?:\/\/([^/]+)/)?.[1] ?? "";
|
|
1269
|
+
if (host.includes("openai")) return "openai";
|
|
1270
|
+
if (host.includes("anthropic")) return "anthropic";
|
|
1271
|
+
if (host.includes("googleapis")) return "google";
|
|
1272
|
+
if (host.includes("mistral")) return "mistral";
|
|
1273
|
+
if (host.includes("groq")) return "groq";
|
|
1274
|
+
return "unknown";
|
|
1275
|
+
}
|
|
1276
|
+
function extractUsage(result) {
|
|
1277
|
+
const r = result;
|
|
1278
|
+
const usage = r?.usage ?? r?.message?.usage;
|
|
1279
|
+
const meta = r?.usageMetadata;
|
|
1280
|
+
const input = usage?.prompt_tokens ?? usage?.input_tokens ?? meta?.promptTokenCount ?? void 0;
|
|
1281
|
+
const output = usage?.completion_tokens ?? usage?.output_tokens ?? meta?.candidatesTokenCount ?? void 0;
|
|
1282
|
+
const responseModel = r?.model ?? r?.modelVersion;
|
|
1283
|
+
return {
|
|
1284
|
+
inputTokens: typeof input === "number" ? input : void 0,
|
|
1285
|
+
outputTokens: typeof output === "number" ? output : void 0,
|
|
1286
|
+
responseModel: typeof responseModel === "string" ? responseModel : void 0
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function applyUsage(span, usage) {
|
|
1290
|
+
if (usage.inputTokens !== void 0) {
|
|
1291
|
+
span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
|
|
1292
|
+
}
|
|
1293
|
+
if (usage.outputTokens !== void 0) {
|
|
1294
|
+
span.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens);
|
|
1295
|
+
}
|
|
1296
|
+
if (usage.responseModel) {
|
|
1297
|
+
span.setAttribute("gen_ai.response.model", usage.responseModel);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function endWithError(span, err) {
|
|
1301
|
+
if (err instanceof Error) span.recordException(err);
|
|
1302
|
+
span.setStatus({
|
|
1303
|
+
code: import_api2.SpanStatusCode.ERROR,
|
|
1304
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1305
|
+
});
|
|
1306
|
+
span.end();
|
|
1307
|
+
}
|
|
1308
|
+
function wrapStream(stream, span) {
|
|
1309
|
+
let ended = false;
|
|
1310
|
+
const seen = {
|
|
1311
|
+
inputTokens: void 0,
|
|
1312
|
+
outputTokens: void 0,
|
|
1313
|
+
responseModel: void 0
|
|
1314
|
+
};
|
|
1315
|
+
const finish = (err) => {
|
|
1316
|
+
if (ended) return;
|
|
1317
|
+
ended = true;
|
|
1318
|
+
applyUsage(span, seen);
|
|
1319
|
+
span.setAttribute("autter.llm.streamed", true);
|
|
1320
|
+
if (err !== void 0) {
|
|
1321
|
+
endWithError(span, err);
|
|
1322
|
+
} else {
|
|
1323
|
+
span.setStatus({ code: import_api2.SpanStatusCode.OK });
|
|
1324
|
+
span.end();
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
return new Proxy(stream, {
|
|
1328
|
+
get(target, prop, receiver) {
|
|
1329
|
+
if (prop === Symbol.asyncIterator) {
|
|
1330
|
+
return () => {
|
|
1331
|
+
const inner = target[Symbol.asyncIterator]();
|
|
1332
|
+
return {
|
|
1333
|
+
async next() {
|
|
1334
|
+
try {
|
|
1335
|
+
const step = await inner.next();
|
|
1336
|
+
if (!step.done) {
|
|
1337
|
+
const usage = extractUsage(step.value);
|
|
1338
|
+
if (usage.inputTokens !== void 0) seen.inputTokens = usage.inputTokens;
|
|
1339
|
+
if (usage.outputTokens !== void 0) seen.outputTokens = usage.outputTokens;
|
|
1340
|
+
if (usage.responseModel) seen.responseModel = usage.responseModel;
|
|
1341
|
+
} else {
|
|
1342
|
+
finish();
|
|
1343
|
+
}
|
|
1344
|
+
return step;
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
finish(err);
|
|
1347
|
+
throw err;
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
async return(value2) {
|
|
1351
|
+
finish();
|
|
1352
|
+
return inner.return ? inner.return(value2) : { done: true, value: void 0 };
|
|
1353
|
+
},
|
|
1354
|
+
async throw(err) {
|
|
1355
|
+
finish(err);
|
|
1356
|
+
if (inner.throw) return inner.throw(err);
|
|
1357
|
+
throw err;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1363
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function instrumentLlmClient(client, options) {
|
|
1368
|
+
const provider = (options?.provider ?? detectProvider(client)).toLowerCase();
|
|
1369
|
+
const proxied = /* @__PURE__ */ new WeakMap();
|
|
1370
|
+
function resolveUserId() {
|
|
1371
|
+
return typeof options?.userId === "function" ? options.userId() : options?.userId;
|
|
1372
|
+
}
|
|
1373
|
+
function traceCall(fn, self, callPath, args) {
|
|
1374
|
+
const request = args[0];
|
|
1375
|
+
const operation = /embed/i.test(callPath) ? "embeddings" : "chat";
|
|
1376
|
+
const userId = resolveUserId();
|
|
1377
|
+
const span = autterLlmTracer().startSpan(`${operation} ${request.model}`, {
|
|
1378
|
+
kind: import_api2.SpanKind.CLIENT,
|
|
1379
|
+
attributes: {
|
|
1380
|
+
"gen_ai.operation.name": operation,
|
|
1381
|
+
"gen_ai.provider.name": provider,
|
|
1382
|
+
"gen_ai.system": provider,
|
|
1383
|
+
"gen_ai.request.model": request.model,
|
|
1384
|
+
...userId ? { "autter.user_id": userId } : {},
|
|
1385
|
+
...options?.attributes
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
let result;
|
|
1389
|
+
try {
|
|
1390
|
+
result = fn.apply(self, args);
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
endWithError(span, err);
|
|
1393
|
+
throw err;
|
|
1394
|
+
}
|
|
1395
|
+
if (!(result instanceof Promise)) {
|
|
1396
|
+
if (result && typeof result === "object" && Symbol.asyncIterator in result) {
|
|
1397
|
+
return wrapStream(result, span);
|
|
1398
|
+
}
|
|
1399
|
+
applyUsage(span, extractUsage(result));
|
|
1400
|
+
span.setStatus({ code: import_api2.SpanStatusCode.OK });
|
|
1401
|
+
span.end();
|
|
1402
|
+
return result;
|
|
1403
|
+
}
|
|
1404
|
+
return result.then(
|
|
1405
|
+
(resolved) => {
|
|
1406
|
+
if (resolved && typeof resolved === "object" && Symbol.asyncIterator in resolved) {
|
|
1407
|
+
return wrapStream(resolved, span);
|
|
1408
|
+
}
|
|
1409
|
+
applyUsage(span, extractUsage(resolved));
|
|
1410
|
+
span.setStatus({ code: import_api2.SpanStatusCode.OK });
|
|
1411
|
+
span.end();
|
|
1412
|
+
return resolved;
|
|
1413
|
+
},
|
|
1414
|
+
(err) => {
|
|
1415
|
+
endWithError(span, err);
|
|
1416
|
+
throw err;
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
function deepProxy(target, path = "") {
|
|
1421
|
+
const cached = proxied.get(target);
|
|
1422
|
+
if (cached) return cached;
|
|
1423
|
+
const proxy = new Proxy(target, {
|
|
1424
|
+
get(t, prop, receiver) {
|
|
1425
|
+
const value = Reflect.get(t, prop, receiver);
|
|
1426
|
+
if (typeof prop === "symbol") return value;
|
|
1427
|
+
const propPath = path ? `${path}.${prop}` : prop;
|
|
1428
|
+
if (typeof value === "function") {
|
|
1429
|
+
if (CALL_METHODS.has(prop)) {
|
|
1430
|
+
return (...args) => {
|
|
1431
|
+
const first = args[0];
|
|
1432
|
+
if (first && typeof first === "object" && typeof first.model === "string") {
|
|
1433
|
+
return traceCall(
|
|
1434
|
+
value,
|
|
1435
|
+
t,
|
|
1436
|
+
propPath,
|
|
1437
|
+
args
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
return value.apply(t, args);
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
return value.bind(t);
|
|
1444
|
+
}
|
|
1445
|
+
if (value !== null && typeof value === "object" && !(value instanceof Promise)) {
|
|
1446
|
+
return deepProxy(value, propPath);
|
|
1447
|
+
}
|
|
1448
|
+
return value;
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
proxied.set(target, proxy);
|
|
1452
|
+
return proxy;
|
|
1453
|
+
}
|
|
1454
|
+
return deepProxy(client);
|
|
1455
|
+
}
|
|
366
1456
|
// Annotate the CommonJS export names for ESM import in node:
|
|
367
1457
|
0 && (module.exports = {
|
|
368
1458
|
captureException,
|
|
369
1459
|
captureMessage,
|
|
370
1460
|
createBrowserRelayFetchHandler,
|
|
371
1461
|
createBrowserRelayHandler,
|
|
1462
|
+
emitLlmSelftestTrace,
|
|
372
1463
|
initAutterServer,
|
|
373
|
-
|
|
1464
|
+
installAutterAutoFlush,
|
|
1465
|
+
instrumentLlmClient,
|
|
1466
|
+
makeSafeCapture,
|
|
1467
|
+
redactAttributes,
|
|
1468
|
+
sanitizeBrowserPayload,
|
|
1469
|
+
trackLlmCall,
|
|
1470
|
+
withLlmCall,
|
|
1471
|
+
withProcessSpan
|
|
374
1472
|
});
|