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