@radaros/observability 0.3.21 → 0.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1053 -0
- package/dist/index.d.cts +223 -0
- package/package.json +9 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CallbackExporter: () => CallbackExporter,
|
|
24
|
+
ConsoleExporter: () => ConsoleExporter,
|
|
25
|
+
JsonFileExporter: () => JsonFileExporter,
|
|
26
|
+
LangfuseExporter: () => LangfuseExporter,
|
|
27
|
+
MetricsCollector: () => MetricsCollector,
|
|
28
|
+
OTelExporter: () => OTelExporter,
|
|
29
|
+
StructuredLogger: () => StructuredLogger,
|
|
30
|
+
Tracer: () => Tracer,
|
|
31
|
+
instrument: () => instrument,
|
|
32
|
+
instrumentBus: () => instrumentBus
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/exporters/callback.ts
|
|
37
|
+
var CallbackExporter = class {
|
|
38
|
+
name = "callback";
|
|
39
|
+
callback;
|
|
40
|
+
constructor(callback) {
|
|
41
|
+
this.callback = callback;
|
|
42
|
+
}
|
|
43
|
+
async export(trace) {
|
|
44
|
+
await this.callback(trace);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/exporters/console.ts
|
|
49
|
+
var C = {
|
|
50
|
+
reset: "\x1B[0m",
|
|
51
|
+
bold: "\x1B[1m",
|
|
52
|
+
dim: "\x1B[2m",
|
|
53
|
+
cyan: "\x1B[36m",
|
|
54
|
+
green: "\x1B[32m",
|
|
55
|
+
red: "\x1B[31m",
|
|
56
|
+
yellow: "\x1B[33m",
|
|
57
|
+
magenta: "\x1B[35m",
|
|
58
|
+
gray: "\x1B[90m",
|
|
59
|
+
brightCyan: "\x1B[96m",
|
|
60
|
+
brightGreen: "\x1B[92m"
|
|
61
|
+
};
|
|
62
|
+
function c(code, text) {
|
|
63
|
+
return `${code}${text}${C.reset}`;
|
|
64
|
+
}
|
|
65
|
+
function statusIcon(status) {
|
|
66
|
+
if (status === "ok") return c(C.green, "\u2713");
|
|
67
|
+
if (status === "error") return c(C.red, "\u2717");
|
|
68
|
+
return c(C.yellow, "\u22EF");
|
|
69
|
+
}
|
|
70
|
+
function kindColor(kind) {
|
|
71
|
+
switch (kind) {
|
|
72
|
+
case "agent":
|
|
73
|
+
return C.brightCyan;
|
|
74
|
+
case "llm":
|
|
75
|
+
return C.yellow;
|
|
76
|
+
case "tool":
|
|
77
|
+
return C.magenta;
|
|
78
|
+
case "handoff":
|
|
79
|
+
return C.brightGreen;
|
|
80
|
+
case "team":
|
|
81
|
+
return C.cyan;
|
|
82
|
+
default:
|
|
83
|
+
return C.gray;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var ConsoleExporter = class {
|
|
87
|
+
name = "console";
|
|
88
|
+
async export(trace) {
|
|
89
|
+
const line = c(C.dim, "\u2500".repeat(80));
|
|
90
|
+
console.log(`
|
|
91
|
+
${line}`);
|
|
92
|
+
console.log(
|
|
93
|
+
` ${c(C.bold + C.brightCyan, "Trace")} ${c(C.dim, trace.traceId)} ${c(C.dim, "duration=")}${c(C.brightGreen, `${trace.durationMs ?? "?"}ms`)}`
|
|
94
|
+
);
|
|
95
|
+
if (trace.metadata.agentName) {
|
|
96
|
+
console.log(` ${c(C.dim, "agent=")}${trace.metadata.agentName}`);
|
|
97
|
+
}
|
|
98
|
+
console.log(line);
|
|
99
|
+
const spanMap = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const span of trace.spans) {
|
|
101
|
+
const parentId = span.parentSpanId ?? "__root__";
|
|
102
|
+
if (!spanMap.has(parentId)) spanMap.set(parentId, []);
|
|
103
|
+
spanMap.get(parentId).push(span);
|
|
104
|
+
}
|
|
105
|
+
const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
|
|
106
|
+
if (root) {
|
|
107
|
+
this.printSpan(root, spanMap, 0, trace.startTime);
|
|
108
|
+
}
|
|
109
|
+
console.log(line);
|
|
110
|
+
console.log("");
|
|
111
|
+
}
|
|
112
|
+
printSpan(span, childMap, depth, traceStart) {
|
|
113
|
+
const indent = " " + "\u2502 ".repeat(depth);
|
|
114
|
+
const connector = depth > 0 ? "\u251C\u2500 " : "";
|
|
115
|
+
const offset = span.startTime - traceStart;
|
|
116
|
+
const dur = span.durationMs ?? "?";
|
|
117
|
+
const kc = kindColor(span.kind);
|
|
118
|
+
const icon = statusIcon(span.status);
|
|
119
|
+
let line = `${indent}${connector}${icon} ${c(kc, span.name)}`;
|
|
120
|
+
line += ` ${c(C.dim, `[${offset}ms \u2192 +${dur}ms]`)}`;
|
|
121
|
+
if (span.attributes.tokens) {
|
|
122
|
+
line += ` ${c(C.brightGreen, `${span.attributes.tokens} tok`)}`;
|
|
123
|
+
}
|
|
124
|
+
if (span.attributes.toolName) {
|
|
125
|
+
line += ` ${c(C.dim, `(${span.attributes.toolName})`)}`;
|
|
126
|
+
}
|
|
127
|
+
if (span.attributes.cached) {
|
|
128
|
+
line += ` ${c(C.yellow, "[cached]")}`;
|
|
129
|
+
}
|
|
130
|
+
if (span.status === "error" && span.attributes.error) {
|
|
131
|
+
line += ` ${c(C.red, String(span.attributes.error))}`;
|
|
132
|
+
}
|
|
133
|
+
console.log(line);
|
|
134
|
+
for (const evt of span.events) {
|
|
135
|
+
console.log(
|
|
136
|
+
`${indent}\u2502 ${c(C.dim, `\u2937 ${evt.name}`)}${evt.attributes ? c(C.gray, ` ${JSON.stringify(evt.attributes)}`) : ""}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const children = childMap.get(span.spanId) ?? [];
|
|
140
|
+
for (const child of children) {
|
|
141
|
+
this.printSpan(child, childMap, depth + 1, traceStart);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/exporters/json-file.ts
|
|
147
|
+
var import_promises = require("fs/promises");
|
|
148
|
+
var JsonFileExporter = class {
|
|
149
|
+
name = "json-file";
|
|
150
|
+
path;
|
|
151
|
+
mode;
|
|
152
|
+
pretty;
|
|
153
|
+
constructor(config) {
|
|
154
|
+
this.mode = config?.mode ?? "append";
|
|
155
|
+
this.path = config?.path ?? `traces-${Date.now()}.${this.mode === "append" ? "jsonl" : "json"}`;
|
|
156
|
+
this.pretty = config?.pretty ?? true;
|
|
157
|
+
}
|
|
158
|
+
async export(trace) {
|
|
159
|
+
const json = this.pretty ? JSON.stringify(trace, null, 2) : JSON.stringify(trace);
|
|
160
|
+
try {
|
|
161
|
+
if (this.mode === "append") {
|
|
162
|
+
await (0, import_promises.appendFile)(this.path, json + "\n");
|
|
163
|
+
} else {
|
|
164
|
+
await (0, import_promises.writeFile)(this.path, json);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[radaros/observability] Failed to write trace to ${this.path}:`,
|
|
169
|
+
err instanceof Error ? err.message : err
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/exporters/langfuse.ts
|
|
176
|
+
var eventCounter = 0;
|
|
177
|
+
function eventId() {
|
|
178
|
+
return `evt_${Date.now().toString(36)}_${(eventCounter++).toString(36)}`;
|
|
179
|
+
}
|
|
180
|
+
function extractIO(span) {
|
|
181
|
+
const input = span.attributes.input ?? null;
|
|
182
|
+
const output = span.attributes.output ?? null;
|
|
183
|
+
return { input, output };
|
|
184
|
+
}
|
|
185
|
+
var LangfuseExporter = class {
|
|
186
|
+
name = "langfuse";
|
|
187
|
+
publicKey;
|
|
188
|
+
secretKey;
|
|
189
|
+
baseUrl;
|
|
190
|
+
constructor(config) {
|
|
191
|
+
this.publicKey = config?.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY ?? "";
|
|
192
|
+
this.secretKey = config?.secretKey ?? process.env.LANGFUSE_SECRET_KEY ?? "";
|
|
193
|
+
this.baseUrl = (config?.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com").replace(
|
|
194
|
+
/\/$/,
|
|
195
|
+
""
|
|
196
|
+
);
|
|
197
|
+
if (!this.publicKey || !this.secretKey) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"LangfuseExporter: missing credentials. Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY env vars, or pass them in config."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async fetchWithRetry(url, init, retries = 2) {
|
|
204
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
208
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
if (res.status >= 500 && attempt < retries) {
|
|
211
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
return res;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (attempt === retries) throw err;
|
|
217
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw new Error("Unreachable");
|
|
221
|
+
}
|
|
222
|
+
async export(trace) {
|
|
223
|
+
const events = [];
|
|
224
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
225
|
+
const rootSpan = trace.spans.find((s) => s.spanId === trace.rootSpanId);
|
|
226
|
+
events.push({
|
|
227
|
+
id: eventId(),
|
|
228
|
+
type: "trace-create",
|
|
229
|
+
timestamp: now,
|
|
230
|
+
body: {
|
|
231
|
+
id: trace.traceId,
|
|
232
|
+
name: String(trace.metadata.agentName ?? "agent.run"),
|
|
233
|
+
input: trace.metadata.input ?? rootSpan?.attributes.input ?? null,
|
|
234
|
+
output: trace.metadata.output ?? rootSpan?.attributes.output ?? null,
|
|
235
|
+
metadata: { ...trace.metadata, input: void 0, output: void 0 },
|
|
236
|
+
timestamp: new Date(trace.startTime).toISOString()
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
for (const span of trace.spans) {
|
|
240
|
+
const { input, output } = extractIO(span);
|
|
241
|
+
const { input: _i, output: _o, ...restAttrs } = span.attributes;
|
|
242
|
+
if (span.kind === "llm" || span.name.startsWith("llm.")) {
|
|
243
|
+
events.push({
|
|
244
|
+
id: eventId(),
|
|
245
|
+
type: "generation-create",
|
|
246
|
+
timestamp: now,
|
|
247
|
+
body: {
|
|
248
|
+
id: span.spanId,
|
|
249
|
+
traceId: trace.traceId,
|
|
250
|
+
parentObservationId: span.parentSpanId,
|
|
251
|
+
name: span.name,
|
|
252
|
+
model: span.attributes.modelId ?? void 0,
|
|
253
|
+
input,
|
|
254
|
+
output,
|
|
255
|
+
startTime: new Date(span.startTime).toISOString(),
|
|
256
|
+
endTime: span.endTime ? new Date(span.endTime).toISOString() : void 0,
|
|
257
|
+
usage: {
|
|
258
|
+
promptTokens: span.attributes.promptTokens,
|
|
259
|
+
completionTokens: span.attributes.completionTokens,
|
|
260
|
+
totalTokens: span.attributes.tokens
|
|
261
|
+
},
|
|
262
|
+
metadata: restAttrs
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
events.push({
|
|
267
|
+
id: eventId(),
|
|
268
|
+
type: "span-create",
|
|
269
|
+
timestamp: now,
|
|
270
|
+
body: {
|
|
271
|
+
id: span.spanId,
|
|
272
|
+
traceId: trace.traceId,
|
|
273
|
+
parentObservationId: span.parentSpanId,
|
|
274
|
+
name: span.name,
|
|
275
|
+
input,
|
|
276
|
+
output,
|
|
277
|
+
startTime: new Date(span.startTime).toISOString(),
|
|
278
|
+
endTime: span.endTime ? new Date(span.endTime).toISOString() : void 0,
|
|
279
|
+
metadata: restAttrs,
|
|
280
|
+
level: span.status === "error" ? "ERROR" : "DEFAULT"
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const auth = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64");
|
|
286
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/api/public/ingestion`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
Authorization: `Basic ${auth}`
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({ batch: events })
|
|
293
|
+
});
|
|
294
|
+
if (!res.ok && res.status !== 207) {
|
|
295
|
+
throw new Error(`Langfuse export failed: ${res.status} ${res.statusText}`);
|
|
296
|
+
}
|
|
297
|
+
if (res.status === 207) {
|
|
298
|
+
const body = await res.json();
|
|
299
|
+
if (body.errors && body.errors.length > 0) {
|
|
300
|
+
const realErrors = body.errors.filter((e) => e.status >= 400);
|
|
301
|
+
if (realErrors.length > 0) {
|
|
302
|
+
throw new Error(`Langfuse partial failure: ${JSON.stringify(realErrors[0])}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/exporters/otel.ts
|
|
310
|
+
function parseEnvHeaders(raw) {
|
|
311
|
+
if (!raw) return {};
|
|
312
|
+
const result = {};
|
|
313
|
+
for (const pair of raw.split(",")) {
|
|
314
|
+
const [k, ...v] = pair.split("=");
|
|
315
|
+
if (k) result[k.trim()] = v.join("=").trim();
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
var OTelExporter = class {
|
|
320
|
+
name = "otel";
|
|
321
|
+
endpoint;
|
|
322
|
+
headers;
|
|
323
|
+
serviceName;
|
|
324
|
+
constructor(config) {
|
|
325
|
+
const ep = config?.endpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
|
|
326
|
+
if (!ep) {
|
|
327
|
+
throw new Error("OTelExporter: missing endpoint. Set OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass it in config.");
|
|
328
|
+
}
|
|
329
|
+
this.endpoint = ep.replace(/\/$/, "");
|
|
330
|
+
this.headers = config?.headers ?? parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);
|
|
331
|
+
this.serviceName = config?.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "radaros";
|
|
332
|
+
}
|
|
333
|
+
async fetchWithRetry(url, init, retries = 2) {
|
|
334
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
335
|
+
try {
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
338
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
if (res.status >= 500 && attempt < retries) {
|
|
341
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
return res;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (attempt === retries) throw err;
|
|
347
|
+
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
throw new Error("Unreachable");
|
|
351
|
+
}
|
|
352
|
+
async export(trace) {
|
|
353
|
+
const otlpSpans = trace.spans.map((span) => ({
|
|
354
|
+
traceId: this.padHex(span.traceId, 32),
|
|
355
|
+
spanId: this.padHex(span.spanId, 16),
|
|
356
|
+
parentSpanId: span.parentSpanId ? this.padHex(span.parentSpanId, 16) : void 0,
|
|
357
|
+
name: span.name,
|
|
358
|
+
kind: this.mapKind(span.kind),
|
|
359
|
+
startTimeUnixNano: String(span.startTime * 1e6),
|
|
360
|
+
endTimeUnixNano: span.endTime ? String(span.endTime * 1e6) : void 0,
|
|
361
|
+
status: {
|
|
362
|
+
code: span.status === "error" ? 2 : 1,
|
|
363
|
+
message: span.status === "error" ? String(span.attributes.error ?? "") : void 0
|
|
364
|
+
},
|
|
365
|
+
attributes: Object.entries(span.attributes).map(([key, value]) => ({
|
|
366
|
+
key,
|
|
367
|
+
value: this.toOtlpValue(value)
|
|
368
|
+
})),
|
|
369
|
+
events: span.events.map((evt) => ({
|
|
370
|
+
name: evt.name,
|
|
371
|
+
timeUnixNano: String(evt.timestamp * 1e6),
|
|
372
|
+
attributes: evt.attributes ? Object.entries(evt.attributes).map(([k, v]) => ({ key: k, value: this.toOtlpValue(v) })) : []
|
|
373
|
+
}))
|
|
374
|
+
}));
|
|
375
|
+
const payload = {
|
|
376
|
+
resourceSpans: [
|
|
377
|
+
{
|
|
378
|
+
resource: {
|
|
379
|
+
attributes: [{ key: "service.name", value: { stringValue: this.serviceName } }]
|
|
380
|
+
},
|
|
381
|
+
scopeSpans: [
|
|
382
|
+
{
|
|
383
|
+
scope: { name: "@radaros/observability" },
|
|
384
|
+
spans: otlpSpans
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
};
|
|
390
|
+
const url = `${this.endpoint}/v1/traces`;
|
|
391
|
+
const res = await this.fetchWithRetry(url, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: {
|
|
394
|
+
"Content-Type": "application/json",
|
|
395
|
+
...this.headers
|
|
396
|
+
},
|
|
397
|
+
body: JSON.stringify(payload)
|
|
398
|
+
});
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
throw new Error(`OTLP export failed: ${res.status} ${res.statusText}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
padHex(id, length) {
|
|
404
|
+
let hex = "";
|
|
405
|
+
for (let i = 0; i < id.length; i++) {
|
|
406
|
+
hex += id.charCodeAt(i).toString(16).padStart(2, "0");
|
|
407
|
+
}
|
|
408
|
+
return hex.padStart(length, "0").slice(-length);
|
|
409
|
+
}
|
|
410
|
+
mapKind(kind) {
|
|
411
|
+
switch (kind) {
|
|
412
|
+
case "agent":
|
|
413
|
+
return 1;
|
|
414
|
+
case "llm":
|
|
415
|
+
return 3;
|
|
416
|
+
case "tool":
|
|
417
|
+
return 3;
|
|
418
|
+
default:
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
toOtlpValue(value) {
|
|
423
|
+
if (typeof value === "string") return { stringValue: value };
|
|
424
|
+
if (typeof value === "number")
|
|
425
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
426
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
427
|
+
if (Array.isArray(value)) return { stringValue: JSON.stringify(value) };
|
|
428
|
+
return { stringValue: String(value) };
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/metrics.ts
|
|
433
|
+
var MetricsCollector = class {
|
|
434
|
+
counters = {
|
|
435
|
+
runs_total: 0,
|
|
436
|
+
runs_success: 0,
|
|
437
|
+
runs_error: 0,
|
|
438
|
+
tool_calls_total: 0,
|
|
439
|
+
handoffs_total: 0,
|
|
440
|
+
cache_hits: 0,
|
|
441
|
+
cache_misses: 0
|
|
442
|
+
};
|
|
443
|
+
histograms = {
|
|
444
|
+
run_duration_ms: [],
|
|
445
|
+
tool_latency_ms: []
|
|
446
|
+
};
|
|
447
|
+
gauges = {
|
|
448
|
+
total_cost_usd: 0,
|
|
449
|
+
total_tokens: 0
|
|
450
|
+
};
|
|
451
|
+
MAX_HISTOGRAM_SIZE = 1e4;
|
|
452
|
+
toolStartTimes = /* @__PURE__ */ new Map();
|
|
453
|
+
runStartTimes = /* @__PURE__ */ new Map();
|
|
454
|
+
listeners = [];
|
|
455
|
+
attach(eventBus) {
|
|
456
|
+
const on = (event, handler) => {
|
|
457
|
+
eventBus.on(event, handler);
|
|
458
|
+
this.listeners.push({ event, handler });
|
|
459
|
+
};
|
|
460
|
+
on("run.start", (data) => {
|
|
461
|
+
this.counters.runs_total++;
|
|
462
|
+
this.runStartTimes.set(data.runId, Date.now());
|
|
463
|
+
});
|
|
464
|
+
on("run.complete", (data) => {
|
|
465
|
+
this.counters.runs_success++;
|
|
466
|
+
const startTime = this.runStartTimes.get(data.runId);
|
|
467
|
+
if (startTime) {
|
|
468
|
+
const duration = Date.now() - startTime;
|
|
469
|
+
this.histograms.run_duration_ms.push(duration);
|
|
470
|
+
if (this.histograms.run_duration_ms.length > this.MAX_HISTOGRAM_SIZE) {
|
|
471
|
+
this.histograms.run_duration_ms = this.histograms.run_duration_ms.slice(-this.MAX_HISTOGRAM_SIZE);
|
|
472
|
+
}
|
|
473
|
+
this.runStartTimes.delete(data.runId);
|
|
474
|
+
}
|
|
475
|
+
if (data.output?.usage?.totalTokens) {
|
|
476
|
+
this.gauges.total_tokens += data.output.usage.totalTokens;
|
|
477
|
+
}
|
|
478
|
+
for (const key of this.toolStartTimes.keys()) {
|
|
479
|
+
if (key.startsWith(`${data.runId}:`)) this.toolStartTimes.delete(key);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
on("run.error", (data) => {
|
|
483
|
+
this.counters.runs_error++;
|
|
484
|
+
const startTime = this.runStartTimes.get(data.runId);
|
|
485
|
+
if (startTime) {
|
|
486
|
+
const duration = Date.now() - startTime;
|
|
487
|
+
this.histograms.run_duration_ms.push(duration);
|
|
488
|
+
if (this.histograms.run_duration_ms.length > this.MAX_HISTOGRAM_SIZE) {
|
|
489
|
+
this.histograms.run_duration_ms = this.histograms.run_duration_ms.slice(-this.MAX_HISTOGRAM_SIZE);
|
|
490
|
+
}
|
|
491
|
+
this.runStartTimes.delete(data.runId);
|
|
492
|
+
}
|
|
493
|
+
for (const key of this.toolStartTimes.keys()) {
|
|
494
|
+
if (key.startsWith(`${data.runId}:`)) this.toolStartTimes.delete(key);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
on("tool.call", (data) => {
|
|
498
|
+
this.counters.tool_calls_total++;
|
|
499
|
+
this.toolStartTimes.set(`${data.runId}:${data.toolName}`, Date.now());
|
|
500
|
+
});
|
|
501
|
+
on("tool.result", (data) => {
|
|
502
|
+
const key = `${data.runId}:${data.toolName}`;
|
|
503
|
+
const start = this.toolStartTimes.get(key);
|
|
504
|
+
if (start) {
|
|
505
|
+
this.histograms.tool_latency_ms.push(Date.now() - start);
|
|
506
|
+
if (this.histograms.tool_latency_ms.length > this.MAX_HISTOGRAM_SIZE) {
|
|
507
|
+
this.histograms.tool_latency_ms = this.histograms.tool_latency_ms.slice(-this.MAX_HISTOGRAM_SIZE);
|
|
508
|
+
}
|
|
509
|
+
this.toolStartTimes.delete(key);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
on("handoff.transfer", () => {
|
|
513
|
+
this.counters.handoffs_total++;
|
|
514
|
+
});
|
|
515
|
+
on("cache.hit", () => {
|
|
516
|
+
this.counters.cache_hits++;
|
|
517
|
+
});
|
|
518
|
+
on("cache.miss", () => {
|
|
519
|
+
this.counters.cache_misses++;
|
|
520
|
+
});
|
|
521
|
+
on("cost.tracked", (data) => {
|
|
522
|
+
if (data.usage?.totalTokens) {
|
|
523
|
+
this.gauges.total_tokens += data.usage.totalTokens;
|
|
524
|
+
}
|
|
525
|
+
if (data.usage?.cost) {
|
|
526
|
+
this.gauges.total_cost_usd += data.usage.cost;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
detach(eventBus) {
|
|
531
|
+
for (const { event, handler } of this.listeners) {
|
|
532
|
+
eventBus.off(event, handler);
|
|
533
|
+
}
|
|
534
|
+
this.listeners = [];
|
|
535
|
+
}
|
|
536
|
+
getMetrics() {
|
|
537
|
+
const totalCacheAttempts = this.counters.cache_hits + this.counters.cache_misses;
|
|
538
|
+
return {
|
|
539
|
+
counters: { ...this.counters },
|
|
540
|
+
histograms: {
|
|
541
|
+
run_duration_ms: [...this.histograms.run_duration_ms],
|
|
542
|
+
tool_latency_ms: [...this.histograms.tool_latency_ms]
|
|
543
|
+
},
|
|
544
|
+
gauges: { ...this.gauges },
|
|
545
|
+
rates: {
|
|
546
|
+
cache_hit_ratio: totalCacheAttempts > 0 ? this.counters.cache_hits / totalCacheAttempts : 0,
|
|
547
|
+
error_rate: this.counters.runs_total > 0 ? this.counters.runs_error / this.counters.runs_total : 0
|
|
548
|
+
},
|
|
549
|
+
timestamp: Date.now()
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
reset() {
|
|
553
|
+
this.counters = {
|
|
554
|
+
runs_total: 0,
|
|
555
|
+
runs_success: 0,
|
|
556
|
+
runs_error: 0,
|
|
557
|
+
tool_calls_total: 0,
|
|
558
|
+
handoffs_total: 0,
|
|
559
|
+
cache_hits: 0,
|
|
560
|
+
cache_misses: 0
|
|
561
|
+
};
|
|
562
|
+
this.histograms = {
|
|
563
|
+
run_duration_ms: [],
|
|
564
|
+
tool_latency_ms: []
|
|
565
|
+
};
|
|
566
|
+
this.gauges = {
|
|
567
|
+
total_cost_usd: 0,
|
|
568
|
+
total_tokens: 0
|
|
569
|
+
};
|
|
570
|
+
this.toolStartTimes.clear();
|
|
571
|
+
this.runStartTimes.clear();
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/structured-logger.ts
|
|
576
|
+
var StructuredLogger = class {
|
|
577
|
+
drain;
|
|
578
|
+
tracer;
|
|
579
|
+
listeners = [];
|
|
580
|
+
constructor(drain = "json", tracer) {
|
|
581
|
+
this.drain = drain;
|
|
582
|
+
this.tracer = tracer ?? null;
|
|
583
|
+
}
|
|
584
|
+
attach(eventBus) {
|
|
585
|
+
const on = (event, handler) => {
|
|
586
|
+
eventBus.on(event, handler);
|
|
587
|
+
this.listeners.push({ event, handler });
|
|
588
|
+
};
|
|
589
|
+
on("run.start", (data) => {
|
|
590
|
+
this.log(
|
|
591
|
+
"info",
|
|
592
|
+
"Run started",
|
|
593
|
+
data.agentName,
|
|
594
|
+
{
|
|
595
|
+
runId: data.runId,
|
|
596
|
+
input: data.input?.slice(0, 200) ?? ""
|
|
597
|
+
},
|
|
598
|
+
data.runId
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
on("run.complete", (data) => {
|
|
602
|
+
this.log(
|
|
603
|
+
"info",
|
|
604
|
+
"Run completed",
|
|
605
|
+
data.output?.agentName ?? void 0,
|
|
606
|
+
{
|
|
607
|
+
runId: data.runId,
|
|
608
|
+
tokens: data.output?.usage?.totalTokens,
|
|
609
|
+
durationMs: data.output?.durationMs
|
|
610
|
+
},
|
|
611
|
+
data.runId
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
on("run.error", (data) => {
|
|
615
|
+
this.log(
|
|
616
|
+
"error",
|
|
617
|
+
`Run failed: ${data.error?.message ?? "unknown error"}`,
|
|
618
|
+
void 0,
|
|
619
|
+
{
|
|
620
|
+
runId: data.runId,
|
|
621
|
+
error: data.error?.message ?? "unknown error",
|
|
622
|
+
stack: data.error?.stack?.split("\n").slice(0, 3).join("\n")
|
|
623
|
+
},
|
|
624
|
+
data.runId
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
on("tool.call", (data) => {
|
|
628
|
+
this.log(
|
|
629
|
+
"debug",
|
|
630
|
+
`Tool call: ${data.toolName}`,
|
|
631
|
+
void 0,
|
|
632
|
+
{
|
|
633
|
+
runId: data.runId,
|
|
634
|
+
toolName: data.toolName
|
|
635
|
+
},
|
|
636
|
+
data.runId
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
on("tool.result", (data) => {
|
|
640
|
+
this.log(
|
|
641
|
+
"debug",
|
|
642
|
+
`Tool result: ${data.toolName}`,
|
|
643
|
+
void 0,
|
|
644
|
+
{
|
|
645
|
+
runId: data.runId,
|
|
646
|
+
toolName: data.toolName
|
|
647
|
+
},
|
|
648
|
+
data.runId
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
on("handoff.transfer", (data) => {
|
|
652
|
+
this.log(
|
|
653
|
+
"info",
|
|
654
|
+
`Handoff: ${data.fromAgent} -> ${data.toAgent}`,
|
|
655
|
+
data.fromAgent,
|
|
656
|
+
{
|
|
657
|
+
runId: data.runId,
|
|
658
|
+
toAgent: data.toAgent,
|
|
659
|
+
reason: data.reason
|
|
660
|
+
},
|
|
661
|
+
data.runId
|
|
662
|
+
);
|
|
663
|
+
});
|
|
664
|
+
on("cache.hit", (data) => {
|
|
665
|
+
this.log("debug", "Cache hit", data.agentName, {
|
|
666
|
+
input: data.input?.slice(0, 100) ?? ""
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
on("cache.miss", (data) => {
|
|
670
|
+
this.log("debug", "Cache miss", data.agentName, {
|
|
671
|
+
input: data.input?.slice(0, 100) ?? ""
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
on("cost.tracked", (data) => {
|
|
675
|
+
this.log(
|
|
676
|
+
"info",
|
|
677
|
+
`Cost tracked: ${data.modelId}`,
|
|
678
|
+
data.agentName,
|
|
679
|
+
{
|
|
680
|
+
runId: data.runId,
|
|
681
|
+
modelId: data.modelId,
|
|
682
|
+
tokens: data.usage?.totalTokens
|
|
683
|
+
},
|
|
684
|
+
data.runId
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
detach(eventBus) {
|
|
689
|
+
for (const { event, handler } of this.listeners) {
|
|
690
|
+
eventBus.off(event, handler);
|
|
691
|
+
}
|
|
692
|
+
this.listeners = [];
|
|
693
|
+
}
|
|
694
|
+
log(level, message, agentName, attributes, runId) {
|
|
695
|
+
const traceId = runId && this.tracer ? this.tracer.getTraceByRunId(runId)?.traceId : void 0;
|
|
696
|
+
const entry = {
|
|
697
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
698
|
+
level,
|
|
699
|
+
message,
|
|
700
|
+
traceId,
|
|
701
|
+
agentName,
|
|
702
|
+
attributes
|
|
703
|
+
};
|
|
704
|
+
if (typeof this.drain === "function") {
|
|
705
|
+
this.drain(entry);
|
|
706
|
+
} else if (this.drain === "json") {
|
|
707
|
+
console.log(JSON.stringify(entry));
|
|
708
|
+
} else {
|
|
709
|
+
console.log(`[${entry.timestamp}] ${level.toUpperCase()} ${message}${traceId ? ` trace=${traceId}` : ""}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/tracer.ts
|
|
715
|
+
var idCounter = 0;
|
|
716
|
+
function genId() {
|
|
717
|
+
return `${Date.now().toString(36)}_${(idCounter++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
718
|
+
}
|
|
719
|
+
var Tracer = class {
|
|
720
|
+
traces = /* @__PURE__ */ new Map();
|
|
721
|
+
runToTrace = /* @__PURE__ */ new Map();
|
|
722
|
+
runToRootSpan = /* @__PURE__ */ new Map();
|
|
723
|
+
activeSpans = /* @__PURE__ */ new Map();
|
|
724
|
+
exporters;
|
|
725
|
+
listeners = [];
|
|
726
|
+
pendingExports = [];
|
|
727
|
+
maxTraces = 1e3;
|
|
728
|
+
constructor(exporters = []) {
|
|
729
|
+
this.exporters = exporters;
|
|
730
|
+
}
|
|
731
|
+
attach(eventBus) {
|
|
732
|
+
const on = (event, handler) => {
|
|
733
|
+
eventBus.on(event, handler);
|
|
734
|
+
this.listeners.push({ event, handler });
|
|
735
|
+
};
|
|
736
|
+
on("run.start", (data) => {
|
|
737
|
+
const traceId = genId();
|
|
738
|
+
const inputText = data.input?.slice(0, 1e3) ?? "";
|
|
739
|
+
const span = this.startSpan(traceId, "agent.run", "agent", {
|
|
740
|
+
agentName: data.agentName,
|
|
741
|
+
input: inputText,
|
|
742
|
+
runId: data.runId
|
|
743
|
+
});
|
|
744
|
+
this.runToTrace.set(data.runId, traceId);
|
|
745
|
+
this.runToRootSpan.set(data.runId, span.spanId);
|
|
746
|
+
const trace = {
|
|
747
|
+
traceId,
|
|
748
|
+
spans: [span],
|
|
749
|
+
rootSpanId: span.spanId,
|
|
750
|
+
startTime: span.startTime,
|
|
751
|
+
metadata: { agentName: data.agentName, runId: data.runId, input: inputText }
|
|
752
|
+
};
|
|
753
|
+
this.traces.set(traceId, trace);
|
|
754
|
+
this.activeSpans.set(`${data.runId}:root`, span);
|
|
755
|
+
});
|
|
756
|
+
on("run.complete", (data) => {
|
|
757
|
+
const span = this.activeSpans.get(`${data.runId}:root`);
|
|
758
|
+
if (span) {
|
|
759
|
+
const outputText = data.output?.text?.slice(0, 2e3) ?? "";
|
|
760
|
+
span.attributes.output = outputText;
|
|
761
|
+
span.attributes.outputLength = data.output?.text?.length ?? 0;
|
|
762
|
+
span.attributes.tokens = data.output?.usage?.totalTokens ?? 0;
|
|
763
|
+
span.attributes.promptTokens = data.output?.usage?.promptTokens ?? 0;
|
|
764
|
+
span.attributes.completionTokens = data.output?.usage?.completionTokens ?? 0;
|
|
765
|
+
this.endSpan(span, "ok");
|
|
766
|
+
this.activeSpans.delete(`${data.runId}:root`);
|
|
767
|
+
const traceId = this.runToTrace.get(data.runId);
|
|
768
|
+
const trace = traceId ? this.traces.get(traceId) : void 0;
|
|
769
|
+
if (trace) {
|
|
770
|
+
trace.metadata.output = outputText;
|
|
771
|
+
}
|
|
772
|
+
this.finalizeTrace(data.runId);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
on("run.error", (data) => {
|
|
776
|
+
const span = this.activeSpans.get(`${data.runId}:root`);
|
|
777
|
+
if (span) {
|
|
778
|
+
span.attributes.error = data.error.message;
|
|
779
|
+
this.endSpan(span, "error");
|
|
780
|
+
this.activeSpans.delete(`${data.runId}:root`);
|
|
781
|
+
this.finalizeTrace(data.runId);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
on("tool.call", (data) => {
|
|
785
|
+
const traceId = this.runToTrace.get(data.runId);
|
|
786
|
+
const parentId = this.runToRootSpan.get(data.runId);
|
|
787
|
+
if (!traceId) return;
|
|
788
|
+
const argsStr = typeof data.args === "string" ? data.args : JSON.stringify(data.args ?? {});
|
|
789
|
+
const span = this.startSpan(
|
|
790
|
+
traceId,
|
|
791
|
+
`tool.${data.toolName}`,
|
|
792
|
+
"tool",
|
|
793
|
+
{
|
|
794
|
+
toolName: data.toolName,
|
|
795
|
+
runId: data.runId,
|
|
796
|
+
input: argsStr.slice(0, 1e3)
|
|
797
|
+
},
|
|
798
|
+
parentId
|
|
799
|
+
);
|
|
800
|
+
const trace = this.traces.get(traceId);
|
|
801
|
+
trace?.spans.push(span);
|
|
802
|
+
this.activeSpans.set(`${data.runId}:tool:${data.toolName}:${span.spanId}`, span);
|
|
803
|
+
});
|
|
804
|
+
on("tool.result", (data) => {
|
|
805
|
+
const prefix = `${data.runId}:tool:${data.toolName}:`;
|
|
806
|
+
let matchKey;
|
|
807
|
+
for (const key of this.activeSpans.keys()) {
|
|
808
|
+
if (key.startsWith(prefix)) {
|
|
809
|
+
matchKey = key;
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!matchKey) return;
|
|
814
|
+
const span = this.activeSpans.get(matchKey);
|
|
815
|
+
const resultStr = typeof data.result === "string" ? data.result : JSON.stringify(data.result ?? "");
|
|
816
|
+
span.attributes.output = resultStr.slice(0, 2e3);
|
|
817
|
+
span.attributes.resultLength = resultStr.length;
|
|
818
|
+
span.attributes.cached = resultStr.startsWith("[cached]");
|
|
819
|
+
this.endSpan(span, "ok");
|
|
820
|
+
this.activeSpans.delete(matchKey);
|
|
821
|
+
});
|
|
822
|
+
on("handoff.transfer", (data) => {
|
|
823
|
+
const traceId = this.runToTrace.get(data.runId);
|
|
824
|
+
const parentId = this.runToRootSpan.get(data.runId);
|
|
825
|
+
if (!traceId) return;
|
|
826
|
+
const span = this.startSpan(
|
|
827
|
+
traceId,
|
|
828
|
+
`handoff.${data.fromAgent}->${data.toAgent}`,
|
|
829
|
+
"handoff",
|
|
830
|
+
{
|
|
831
|
+
fromAgent: data.fromAgent,
|
|
832
|
+
toAgent: data.toAgent,
|
|
833
|
+
reason: data.reason
|
|
834
|
+
},
|
|
835
|
+
parentId
|
|
836
|
+
);
|
|
837
|
+
this.endSpan(span, "ok");
|
|
838
|
+
const trace = this.traces.get(traceId);
|
|
839
|
+
trace?.spans.push(span);
|
|
840
|
+
});
|
|
841
|
+
on("handoff.complete", (data) => {
|
|
842
|
+
const span = this.activeSpans.get(`${data.runId}:root`);
|
|
843
|
+
if (span) {
|
|
844
|
+
span.attributes.handoffChain = data.chain;
|
|
845
|
+
span.attributes.finalAgent = data.finalAgent;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
on("team.delegate", (data) => {
|
|
849
|
+
const traceId = this.runToTrace.get(data.runId);
|
|
850
|
+
const parentId = this.runToRootSpan.get(data.runId);
|
|
851
|
+
if (!traceId) return;
|
|
852
|
+
const span = this.startSpan(
|
|
853
|
+
traceId,
|
|
854
|
+
`team.delegate.${data.memberId}`,
|
|
855
|
+
"team",
|
|
856
|
+
{
|
|
857
|
+
memberId: data.memberId,
|
|
858
|
+
task: data.task.slice(0, 200)
|
|
859
|
+
},
|
|
860
|
+
parentId
|
|
861
|
+
);
|
|
862
|
+
this.endSpan(span, "ok");
|
|
863
|
+
const trace = this.traces.get(traceId);
|
|
864
|
+
trace?.spans.push(span);
|
|
865
|
+
});
|
|
866
|
+
on("cache.hit", (data) => {
|
|
867
|
+
for (const [key, span] of this.activeSpans) {
|
|
868
|
+
if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
|
|
869
|
+
span.events.push({ name: "cache.hit", timestamp: Date.now(), attributes: { cachedId: data.cachedId } });
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
on("cache.miss", (data) => {
|
|
875
|
+
for (const [key, span] of this.activeSpans) {
|
|
876
|
+
if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
|
|
877
|
+
span.events.push({ name: "cache.miss", timestamp: Date.now(), attributes: {} });
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
on("cost.tracked", (data) => {
|
|
883
|
+
const span = this.activeSpans.get(`${data.runId}:root`);
|
|
884
|
+
if (span) {
|
|
885
|
+
span.attributes.modelId = data.modelId;
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
on("memory.extract", (data) => {
|
|
889
|
+
for (const [key, span] of this.activeSpans) {
|
|
890
|
+
if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
|
|
891
|
+
span.events.push({
|
|
892
|
+
name: "memory.extract",
|
|
893
|
+
timestamp: Date.now(),
|
|
894
|
+
attributes: { sessionId: data.sessionId, agentName: data.agentName }
|
|
895
|
+
});
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
detach(eventBus) {
|
|
902
|
+
for (const { event, handler } of this.listeners) {
|
|
903
|
+
eventBus.off(event, handler);
|
|
904
|
+
}
|
|
905
|
+
this.listeners = [];
|
|
906
|
+
}
|
|
907
|
+
getTrace(traceId) {
|
|
908
|
+
return this.traces.get(traceId);
|
|
909
|
+
}
|
|
910
|
+
getTraceByRunId(runId) {
|
|
911
|
+
const traceId = this.runToTrace.get(runId);
|
|
912
|
+
return traceId ? this.traces.get(traceId) : void 0;
|
|
913
|
+
}
|
|
914
|
+
getAllTraces() {
|
|
915
|
+
return [...this.traces.values()];
|
|
916
|
+
}
|
|
917
|
+
clear() {
|
|
918
|
+
this.traces.clear();
|
|
919
|
+
this.runToTrace.clear();
|
|
920
|
+
this.runToRootSpan.clear();
|
|
921
|
+
this.activeSpans.clear();
|
|
922
|
+
}
|
|
923
|
+
async flush() {
|
|
924
|
+
await Promise.allSettled(this.pendingExports);
|
|
925
|
+
this.pendingExports = [];
|
|
926
|
+
for (const exporter of this.exporters) {
|
|
927
|
+
if (exporter.flush) await exporter.flush();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async shutdown() {
|
|
931
|
+
await this.flush();
|
|
932
|
+
for (const exporter of this.exporters) {
|
|
933
|
+
if (exporter.shutdown) await exporter.shutdown();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
startSpan(traceId, name, kind, attributes, parentSpanId) {
|
|
937
|
+
return {
|
|
938
|
+
traceId,
|
|
939
|
+
spanId: genId(),
|
|
940
|
+
parentSpanId,
|
|
941
|
+
name,
|
|
942
|
+
kind,
|
|
943
|
+
startTime: Date.now(),
|
|
944
|
+
status: "running",
|
|
945
|
+
attributes,
|
|
946
|
+
events: []
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
endSpan(span, status) {
|
|
950
|
+
span.endTime = Date.now();
|
|
951
|
+
span.durationMs = span.endTime - span.startTime;
|
|
952
|
+
span.status = status;
|
|
953
|
+
}
|
|
954
|
+
addEventToLatestSpan(runId, name, attributes) {
|
|
955
|
+
const span = this.activeSpans.get(`${runId}:root`);
|
|
956
|
+
if (span) {
|
|
957
|
+
span.events.push({ name, timestamp: Date.now(), attributes });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
finalizeTrace(runId) {
|
|
961
|
+
const traceId = this.runToTrace.get(runId);
|
|
962
|
+
if (!traceId) return;
|
|
963
|
+
const trace = this.traces.get(traceId);
|
|
964
|
+
if (!trace) return;
|
|
965
|
+
const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
|
|
966
|
+
if (root) {
|
|
967
|
+
trace.endTime = root.endTime;
|
|
968
|
+
trace.durationMs = root.durationMs;
|
|
969
|
+
}
|
|
970
|
+
const exportPromise = this.runExporters(trace);
|
|
971
|
+
this.pendingExports.push(exportPromise);
|
|
972
|
+
exportPromise.finally(() => {
|
|
973
|
+
const idx = this.pendingExports.indexOf(exportPromise);
|
|
974
|
+
if (idx >= 0) this.pendingExports.splice(idx, 1);
|
|
975
|
+
});
|
|
976
|
+
this.runToTrace.delete(runId);
|
|
977
|
+
this.runToRootSpan.delete(runId);
|
|
978
|
+
if (this.traces.size > this.maxTraces) {
|
|
979
|
+
const oldest = this.traces.keys().next().value;
|
|
980
|
+
if (oldest) this.traces.delete(oldest);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async runExporters(trace) {
|
|
984
|
+
for (const exporter of this.exporters) {
|
|
985
|
+
try {
|
|
986
|
+
await exporter.export(trace);
|
|
987
|
+
} catch (err) {
|
|
988
|
+
console.warn(
|
|
989
|
+
`[radaros/observability] Export failed (${exporter.name}):`,
|
|
990
|
+
err instanceof Error ? err.message : err
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// src/instrument.ts
|
|
998
|
+
function resolveExporters(raw) {
|
|
999
|
+
if (!raw || raw.length === 0) return [];
|
|
1000
|
+
return raw.map((e) => {
|
|
1001
|
+
if (typeof e !== "string") return e;
|
|
1002
|
+
const shorthand = {
|
|
1003
|
+
console: () => new ConsoleExporter(),
|
|
1004
|
+
langfuse: () => new LangfuseExporter(),
|
|
1005
|
+
"json-file": () => new JsonFileExporter(),
|
|
1006
|
+
otel: () => new OTelExporter()
|
|
1007
|
+
};
|
|
1008
|
+
const factory = shorthand[e];
|
|
1009
|
+
if (!factory) throw new Error(`Unknown exporter: "${e}". Use "console", "langfuse", "json-file", or "otel".`);
|
|
1010
|
+
return factory();
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function buildResult(eventBus, config) {
|
|
1014
|
+
const exporters = resolveExporters(config?.exporters);
|
|
1015
|
+
const tracer = new Tracer(exporters);
|
|
1016
|
+
tracer.attach(eventBus);
|
|
1017
|
+
let metrics = null;
|
|
1018
|
+
if (config?.metrics !== false) {
|
|
1019
|
+
metrics = new MetricsCollector();
|
|
1020
|
+
metrics.attach(eventBus);
|
|
1021
|
+
}
|
|
1022
|
+
let logger = null;
|
|
1023
|
+
if (config?.structuredLogs) {
|
|
1024
|
+
const drain = config.structuredLogs === true ? "json" : config.structuredLogs;
|
|
1025
|
+
logger = new StructuredLogger(drain, tracer);
|
|
1026
|
+
logger.attach(eventBus);
|
|
1027
|
+
}
|
|
1028
|
+
const detach = () => {
|
|
1029
|
+
tracer.detach(eventBus);
|
|
1030
|
+
metrics?.detach(eventBus);
|
|
1031
|
+
logger?.detach(eventBus);
|
|
1032
|
+
};
|
|
1033
|
+
return { tracer, metrics, logger, detach };
|
|
1034
|
+
}
|
|
1035
|
+
function instrument(agent, config) {
|
|
1036
|
+
return buildResult(agent.eventBus, config);
|
|
1037
|
+
}
|
|
1038
|
+
function instrumentBus(eventBus, config) {
|
|
1039
|
+
return buildResult(eventBus, config);
|
|
1040
|
+
}
|
|
1041
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1042
|
+
0 && (module.exports = {
|
|
1043
|
+
CallbackExporter,
|
|
1044
|
+
ConsoleExporter,
|
|
1045
|
+
JsonFileExporter,
|
|
1046
|
+
LangfuseExporter,
|
|
1047
|
+
MetricsCollector,
|
|
1048
|
+
OTelExporter,
|
|
1049
|
+
StructuredLogger,
|
|
1050
|
+
Tracer,
|
|
1051
|
+
instrument,
|
|
1052
|
+
instrumentBus
|
|
1053
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { EventBus, Agent } from '@radaros/core';
|
|
2
|
+
|
|
3
|
+
type SpanKind = "agent" | "llm" | "tool" | "guardrail" | "memory" | "cache" | "handoff" | "team" | "workflow" | "internal";
|
|
4
|
+
type SpanStatus = "ok" | "error" | "running";
|
|
5
|
+
interface SpanEvent {
|
|
6
|
+
name: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
attributes?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface Span {
|
|
11
|
+
traceId: string;
|
|
12
|
+
spanId: string;
|
|
13
|
+
parentSpanId?: string;
|
|
14
|
+
name: string;
|
|
15
|
+
kind: SpanKind;
|
|
16
|
+
startTime: number;
|
|
17
|
+
endTime?: number;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
status: SpanStatus;
|
|
20
|
+
attributes: Record<string, unknown>;
|
|
21
|
+
events: SpanEvent[];
|
|
22
|
+
}
|
|
23
|
+
interface Trace {
|
|
24
|
+
traceId: string;
|
|
25
|
+
spans: Span[];
|
|
26
|
+
rootSpanId: string;
|
|
27
|
+
startTime: number;
|
|
28
|
+
endTime?: number;
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
metadata: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
interface TraceExporter {
|
|
33
|
+
name: string;
|
|
34
|
+
export(trace: Trace): Promise<void>;
|
|
35
|
+
flush?(): Promise<void>;
|
|
36
|
+
shutdown?(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
interface MetricsSnapshot {
|
|
39
|
+
counters: {
|
|
40
|
+
runs_total: number;
|
|
41
|
+
runs_success: number;
|
|
42
|
+
runs_error: number;
|
|
43
|
+
tool_calls_total: number;
|
|
44
|
+
handoffs_total: number;
|
|
45
|
+
cache_hits: number;
|
|
46
|
+
cache_misses: number;
|
|
47
|
+
};
|
|
48
|
+
histograms: {
|
|
49
|
+
run_duration_ms: number[];
|
|
50
|
+
tool_latency_ms: number[];
|
|
51
|
+
};
|
|
52
|
+
gauges: {
|
|
53
|
+
total_cost_usd: number;
|
|
54
|
+
total_tokens: number;
|
|
55
|
+
};
|
|
56
|
+
rates: {
|
|
57
|
+
cache_hit_ratio: number;
|
|
58
|
+
error_rate: number;
|
|
59
|
+
};
|
|
60
|
+
timestamp: number;
|
|
61
|
+
}
|
|
62
|
+
type ExporterShorthand = "console" | "langfuse" | "json-file" | "otel";
|
|
63
|
+
interface ObservabilityConfig {
|
|
64
|
+
/** Exporter instances or shorthand strings like "console", "langfuse". */
|
|
65
|
+
exporters?: (TraceExporter | ExporterShorthand)[];
|
|
66
|
+
metrics?: boolean;
|
|
67
|
+
structuredLogs?: boolean | LogDrain;
|
|
68
|
+
}
|
|
69
|
+
type LogDrain = "console" | "json" | ((entry: LogEntry) => void);
|
|
70
|
+
interface LogEntry {
|
|
71
|
+
timestamp: string;
|
|
72
|
+
level: "debug" | "info" | "warn" | "error";
|
|
73
|
+
message: string;
|
|
74
|
+
traceId?: string;
|
|
75
|
+
spanId?: string;
|
|
76
|
+
agentName?: string;
|
|
77
|
+
attributes?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
declare class CallbackExporter implements TraceExporter {
|
|
81
|
+
name: string;
|
|
82
|
+
private callback;
|
|
83
|
+
constructor(callback: (trace: Trace) => void | Promise<void>);
|
|
84
|
+
export(trace: Trace): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare class ConsoleExporter implements TraceExporter {
|
|
88
|
+
name: string;
|
|
89
|
+
export(trace: Trace): Promise<void>;
|
|
90
|
+
private printSpan;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface JsonFileExporterConfig {
|
|
94
|
+
path?: string;
|
|
95
|
+
mode?: "overwrite" | "append";
|
|
96
|
+
pretty?: boolean;
|
|
97
|
+
}
|
|
98
|
+
declare class JsonFileExporter implements TraceExporter {
|
|
99
|
+
name: string;
|
|
100
|
+
private path;
|
|
101
|
+
private mode;
|
|
102
|
+
private pretty;
|
|
103
|
+
constructor(config?: JsonFileExporterConfig);
|
|
104
|
+
export(trace: Trace): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface LangfuseExporterConfig {
|
|
108
|
+
/** Defaults to LANGFUSE_PUBLIC_KEY env var. */
|
|
109
|
+
publicKey?: string;
|
|
110
|
+
/** Defaults to LANGFUSE_SECRET_KEY env var. */
|
|
111
|
+
secretKey?: string;
|
|
112
|
+
/** Defaults to LANGFUSE_BASE_URL env var or https://cloud.langfuse.com. */
|
|
113
|
+
baseUrl?: string;
|
|
114
|
+
}
|
|
115
|
+
declare class LangfuseExporter implements TraceExporter {
|
|
116
|
+
name: string;
|
|
117
|
+
private publicKey;
|
|
118
|
+
private secretKey;
|
|
119
|
+
private baseUrl;
|
|
120
|
+
constructor(config?: LangfuseExporterConfig);
|
|
121
|
+
private fetchWithRetry;
|
|
122
|
+
export(trace: Trace): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface OTelExporterConfig {
|
|
126
|
+
/** Defaults to OTEL_EXPORTER_OTLP_ENDPOINT env var. */
|
|
127
|
+
endpoint?: string;
|
|
128
|
+
/** Defaults to OTEL_EXPORTER_OTLP_HEADERS env var (comma-separated key=value pairs). */
|
|
129
|
+
headers?: Record<string, string>;
|
|
130
|
+
protocol?: "http/json" | "http/protobuf";
|
|
131
|
+
/** Defaults to OTEL_SERVICE_NAME env var or "radaros". */
|
|
132
|
+
serviceName?: string;
|
|
133
|
+
}
|
|
134
|
+
declare class OTelExporter implements TraceExporter {
|
|
135
|
+
name: string;
|
|
136
|
+
private endpoint;
|
|
137
|
+
private headers;
|
|
138
|
+
private serviceName;
|
|
139
|
+
constructor(config?: OTelExporterConfig);
|
|
140
|
+
private fetchWithRetry;
|
|
141
|
+
export(trace: Trace): Promise<void>;
|
|
142
|
+
private padHex;
|
|
143
|
+
private mapKind;
|
|
144
|
+
private toOtlpValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
declare class MetricsCollector {
|
|
148
|
+
private counters;
|
|
149
|
+
private histograms;
|
|
150
|
+
private gauges;
|
|
151
|
+
private readonly MAX_HISTOGRAM_SIZE;
|
|
152
|
+
private toolStartTimes;
|
|
153
|
+
private runStartTimes;
|
|
154
|
+
private listeners;
|
|
155
|
+
attach(eventBus: EventBus): void;
|
|
156
|
+
detach(eventBus: EventBus): void;
|
|
157
|
+
getMetrics(): MetricsSnapshot;
|
|
158
|
+
reset(): void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare class Tracer {
|
|
162
|
+
private traces;
|
|
163
|
+
private runToTrace;
|
|
164
|
+
private runToRootSpan;
|
|
165
|
+
private activeSpans;
|
|
166
|
+
private exporters;
|
|
167
|
+
private listeners;
|
|
168
|
+
private pendingExports;
|
|
169
|
+
private maxTraces;
|
|
170
|
+
constructor(exporters?: TraceExporter[]);
|
|
171
|
+
attach(eventBus: EventBus): void;
|
|
172
|
+
detach(eventBus: EventBus): void;
|
|
173
|
+
getTrace(traceId: string): Trace | undefined;
|
|
174
|
+
getTraceByRunId(runId: string): Trace | undefined;
|
|
175
|
+
getAllTraces(): Trace[];
|
|
176
|
+
clear(): void;
|
|
177
|
+
flush(): Promise<void>;
|
|
178
|
+
shutdown(): Promise<void>;
|
|
179
|
+
private startSpan;
|
|
180
|
+
private endSpan;
|
|
181
|
+
private addEventToLatestSpan;
|
|
182
|
+
private finalizeTrace;
|
|
183
|
+
private runExporters;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
declare class StructuredLogger {
|
|
187
|
+
private drain;
|
|
188
|
+
private tracer;
|
|
189
|
+
private listeners;
|
|
190
|
+
constructor(drain?: LogDrain, tracer?: Tracer);
|
|
191
|
+
attach(eventBus: EventBus): void;
|
|
192
|
+
detach(eventBus: EventBus): void;
|
|
193
|
+
private log;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface InstrumentResult {
|
|
197
|
+
tracer: Tracer;
|
|
198
|
+
metrics: MetricsCollector | null;
|
|
199
|
+
logger: StructuredLogger | null;
|
|
200
|
+
detach: () => void;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Attach observability to an agent in one call.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* import { instrument } from "@radaros/observability";
|
|
207
|
+
*
|
|
208
|
+
* // Minimal — just pass string shorthands:
|
|
209
|
+
* const obs = instrument(agent, { exporters: ["langfuse", "console"] });
|
|
210
|
+
*
|
|
211
|
+
* // Or bring your own exporter instances for custom config:
|
|
212
|
+
* const obs = instrument(agent, {
|
|
213
|
+
* exporters: [new LangfuseExporter({ baseUrl: "..." }), "console"],
|
|
214
|
+
* });
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
declare function instrument(agent: Agent, config?: ObservabilityConfig): InstrumentResult;
|
|
218
|
+
/**
|
|
219
|
+
* Attach observability to a raw EventBus (for teams, workflows, or custom setups).
|
|
220
|
+
*/
|
|
221
|
+
declare function instrumentBus(eventBus: EventBus, config?: ObservabilityConfig): InstrumentResult;
|
|
222
|
+
|
|
223
|
+
export { CallbackExporter, ConsoleExporter, type ExporterShorthand, type InstrumentResult, JsonFileExporter, type JsonFileExporterConfig, LangfuseExporter, type LangfuseExporterConfig, type LogDrain, type LogEntry, MetricsCollector, type MetricsSnapshot, OTelExporter, type OTelExporterConfig, type ObservabilityConfig, type Span, type SpanEvent, type SpanKind, type SpanStatus, StructuredLogger, type Trace, type TraceExporter, Tracer, instrument, instrumentBus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radaros/observability",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Tracing, metrics, and structured logging for RadarOS agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,24 +18,27 @@
|
|
|
18
18
|
"opentelemetry"
|
|
19
19
|
],
|
|
20
20
|
"type": "module",
|
|
21
|
-
"main": "./dist/index.
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
22
23
|
"types": "./dist/index.d.ts",
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
25
27
|
"import": "./dist/index.js",
|
|
26
|
-
"
|
|
28
|
+
"require": "./dist/index.cjs",
|
|
29
|
+
"default": "./dist/index.js"
|
|
27
30
|
}
|
|
28
31
|
},
|
|
29
32
|
"files": [
|
|
30
33
|
"dist"
|
|
31
34
|
],
|
|
32
35
|
"scripts": {
|
|
33
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
34
|
-
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
36
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --shims --clean",
|
|
37
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --shims --watch",
|
|
35
38
|
"prepublishOnly": "npm run build"
|
|
36
39
|
},
|
|
37
40
|
"peerDependencies": {
|
|
38
|
-
"@radaros/core": "^0.3.
|
|
41
|
+
"@radaros/core": "^0.3.23"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@types/node": "^25.3.1",
|