@llmops/app 0.6.6 → 0.6.7-beta.1
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 +290 -16
- package/dist/index.mjs +290 -16
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -17072,9 +17072,264 @@ const app$9 = new hono.Hono().post("/", zv("json", zod_default.object({
|
|
|
17072
17072
|
});
|
|
17073
17073
|
var targeting_default = app$9;
|
|
17074
17074
|
|
|
17075
|
+
//#endregion
|
|
17076
|
+
//#region src/server/services/otlpQueryClient.ts
|
|
17077
|
+
function nanoToISOString(nanoStr) {
|
|
17078
|
+
const ms = Math.floor(Number(BigInt(nanoStr) / BigInt(1e6)));
|
|
17079
|
+
return new Date(ms).toISOString();
|
|
17080
|
+
}
|
|
17081
|
+
function nanoToMs(nanoStr) {
|
|
17082
|
+
return Math.floor(Number(BigInt(nanoStr) / BigInt(1e6)));
|
|
17083
|
+
}
|
|
17084
|
+
function extractAttr(attrs, keys) {
|
|
17085
|
+
if (!attrs) return void 0;
|
|
17086
|
+
for (const key of keys) {
|
|
17087
|
+
const kv = attrs.find((a) => a.key === key);
|
|
17088
|
+
if (kv) {
|
|
17089
|
+
const v = kv.value;
|
|
17090
|
+
if (v.stringValue !== void 0) return v.stringValue;
|
|
17091
|
+
if (v.intValue !== void 0) return Number(v.intValue);
|
|
17092
|
+
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
17093
|
+
}
|
|
17094
|
+
}
|
|
17095
|
+
}
|
|
17096
|
+
function attrsToRecord(attrs) {
|
|
17097
|
+
if (!attrs) return {};
|
|
17098
|
+
const out = {};
|
|
17099
|
+
for (const kv of attrs) {
|
|
17100
|
+
const v = kv.value;
|
|
17101
|
+
if (v.stringValue !== void 0) out[kv.key] = v.stringValue;
|
|
17102
|
+
else if (v.intValue !== void 0) out[kv.key] = Number(v.intValue);
|
|
17103
|
+
else if (v.doubleValue !== void 0) out[kv.key] = v.doubleValue;
|
|
17104
|
+
else if (v.boolValue !== void 0) out[kv.key] = v.boolValue;
|
|
17105
|
+
}
|
|
17106
|
+
return out;
|
|
17107
|
+
}
|
|
17108
|
+
function mapOtlpSpanToRow(span, resourceAttrs) {
|
|
17109
|
+
const allAttrs = [...resourceAttrs ?? [], ...span.attributes ?? []];
|
|
17110
|
+
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
17111
|
+
const endMs = span.endTimeUnixNano ? nanoToMs(span.endTimeUnixNano) : null;
|
|
17112
|
+
const durationMs = endMs !== null ? endMs - startMs : null;
|
|
17113
|
+
const provider = extractAttr(allAttrs, [
|
|
17114
|
+
"gen_ai.provider.name",
|
|
17115
|
+
"gen_ai.system",
|
|
17116
|
+
"ai.model.provider"
|
|
17117
|
+
]) ?? null;
|
|
17118
|
+
const model = extractAttr(allAttrs, ["gen_ai.request.model", "ai.model.id"]) ?? null;
|
|
17119
|
+
const promptTokens = Number(extractAttr(allAttrs, ["gen_ai.usage.input_tokens", "ai.usage.promptTokens"]) ?? 0);
|
|
17120
|
+
const completionTokens = Number(extractAttr(allAttrs, ["gen_ai.usage.output_tokens", "ai.usage.completionTokens"]) ?? 0);
|
|
17121
|
+
const input = extractAttr(allAttrs, [
|
|
17122
|
+
"ai.prompt.messages",
|
|
17123
|
+
"gen_ai.prompt",
|
|
17124
|
+
"ai.prompt"
|
|
17125
|
+
]) ?? null;
|
|
17126
|
+
const output = extractAttr(allAttrs, [
|
|
17127
|
+
"ai.response.text",
|
|
17128
|
+
"ai.response.object",
|
|
17129
|
+
"gen_ai.completion",
|
|
17130
|
+
"ai.response.toolCalls"
|
|
17131
|
+
]) ?? null;
|
|
17132
|
+
return {
|
|
17133
|
+
id: span.spanId,
|
|
17134
|
+
traceId: span.traceId,
|
|
17135
|
+
spanId: span.spanId,
|
|
17136
|
+
parentSpanId: span.parentSpanId || null,
|
|
17137
|
+
name: span.name,
|
|
17138
|
+
kind: span.kind ?? 1,
|
|
17139
|
+
status: span.status?.code ?? 0,
|
|
17140
|
+
statusMessage: span.status?.message ?? null,
|
|
17141
|
+
startTime: new Date(startMs).toISOString(),
|
|
17142
|
+
endTime: endMs !== null ? new Date(endMs).toISOString() : null,
|
|
17143
|
+
durationMs,
|
|
17144
|
+
provider,
|
|
17145
|
+
model,
|
|
17146
|
+
promptTokens,
|
|
17147
|
+
completionTokens,
|
|
17148
|
+
totalTokens: promptTokens + completionTokens,
|
|
17149
|
+
cost: 0,
|
|
17150
|
+
source: "otlp",
|
|
17151
|
+
input,
|
|
17152
|
+
output,
|
|
17153
|
+
attributes: attrsToRecord(allAttrs)
|
|
17154
|
+
};
|
|
17155
|
+
}
|
|
17156
|
+
function createOtlpQueryClient(otlpConfig) {
|
|
17157
|
+
const baseUrl = otlpConfig.endpoint.replace(/\/$/, "");
|
|
17158
|
+
const headers = {
|
|
17159
|
+
Accept: "application/json",
|
|
17160
|
+
...otlpConfig.headers
|
|
17161
|
+
};
|
|
17162
|
+
async function tempoFetch(path$1) {
|
|
17163
|
+
const url$2 = `${baseUrl}${path$1}`;
|
|
17164
|
+
__llmops_core.logger.info(`[OtlpQuery] GET ${url$2}`);
|
|
17165
|
+
const res = await fetch(url$2, { headers });
|
|
17166
|
+
if (!res.ok) {
|
|
17167
|
+
const body = await res.text().catch(() => "");
|
|
17168
|
+
throw new Error(`OTLP query failed: ${res.status} ${res.statusText} — ${body}`);
|
|
17169
|
+
}
|
|
17170
|
+
return res.json();
|
|
17171
|
+
}
|
|
17172
|
+
return {
|
|
17173
|
+
async listTraces(params) {
|
|
17174
|
+
const limit = params.limit ?? 50;
|
|
17175
|
+
const offset = params.offset ?? 0;
|
|
17176
|
+
const qs = new URLSearchParams();
|
|
17177
|
+
qs.set("limit", String(limit + offset));
|
|
17178
|
+
if (params.startDate) qs.set("start", String(Math.floor(params.startDate.getTime() / 1e3)));
|
|
17179
|
+
if (params.endDate) qs.set("end", String(Math.floor(params.endDate.getTime() / 1e3)));
|
|
17180
|
+
const filters = [];
|
|
17181
|
+
if (params.name) filters.push(`name = "${params.name}"`);
|
|
17182
|
+
if (params.status) {
|
|
17183
|
+
const statusMap = {
|
|
17184
|
+
ok: "ok",
|
|
17185
|
+
error: "error",
|
|
17186
|
+
unset: "unset"
|
|
17187
|
+
};
|
|
17188
|
+
if (statusMap[params.status]) filters.push(`status = ${statusMap[params.status]}`);
|
|
17189
|
+
}
|
|
17190
|
+
if (params.tags) {
|
|
17191
|
+
for (const [key, values] of Object.entries(params.tags)) if (values.length > 0) filters.push(`span.${key} = "${values[0]}"`);
|
|
17192
|
+
}
|
|
17193
|
+
if (filters.length > 0) qs.set("q", `{ ${filters.join(" && ")} }`);
|
|
17194
|
+
const allTraces = ((await tempoFetch(`/api/search?${qs.toString()}`)).traces ?? []).map((t) => {
|
|
17195
|
+
const startMs = nanoToMs(t.startTimeUnixNano);
|
|
17196
|
+
const durationMs = t.durationMs ?? null;
|
|
17197
|
+
const endMs = durationMs !== null ? startMs + durationMs : null;
|
|
17198
|
+
let spanCount = 0;
|
|
17199
|
+
const sets = t.spanSets ?? (t.spanSet ? [t.spanSet] : []);
|
|
17200
|
+
for (const ss of sets) spanCount += ss.matched ?? ss.spans?.length ?? 0;
|
|
17201
|
+
if (t.serviceStats) spanCount = Object.values(t.serviceStats).reduce((sum, s) => sum + s.spanCount, 0);
|
|
17202
|
+
return {
|
|
17203
|
+
id: t.traceID,
|
|
17204
|
+
traceId: t.traceID,
|
|
17205
|
+
name: t.rootTraceName ?? null,
|
|
17206
|
+
sessionId: null,
|
|
17207
|
+
userId: null,
|
|
17208
|
+
status: "unset",
|
|
17209
|
+
startTime: new Date(startMs).toISOString(),
|
|
17210
|
+
endTime: endMs !== null ? new Date(endMs).toISOString() : null,
|
|
17211
|
+
durationMs,
|
|
17212
|
+
spanCount: spanCount || 1,
|
|
17213
|
+
totalInputTokens: 0,
|
|
17214
|
+
totalOutputTokens: 0,
|
|
17215
|
+
totalTokens: 0,
|
|
17216
|
+
totalCost: 0,
|
|
17217
|
+
tags: {},
|
|
17218
|
+
metadata: {},
|
|
17219
|
+
createdAt: new Date(startMs).toISOString(),
|
|
17220
|
+
updatedAt: new Date(startMs).toISOString()
|
|
17221
|
+
};
|
|
17222
|
+
});
|
|
17223
|
+
return {
|
|
17224
|
+
data: allTraces.slice(offset, offset + limit),
|
|
17225
|
+
total: allTraces.length,
|
|
17226
|
+
limit,
|
|
17227
|
+
offset
|
|
17228
|
+
};
|
|
17229
|
+
},
|
|
17230
|
+
async getTraceWithSpans(traceId) {
|
|
17231
|
+
let result;
|
|
17232
|
+
try {
|
|
17233
|
+
result = await tempoFetch(`/api/traces/${traceId}`);
|
|
17234
|
+
} catch (e) {
|
|
17235
|
+
__llmops_core.logger.error(`[OtlpQuery] Failed to fetch trace ${traceId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17236
|
+
return;
|
|
17237
|
+
}
|
|
17238
|
+
const resourceSpansList = result.resourceSpans ?? result.batches ?? [];
|
|
17239
|
+
const spans = [];
|
|
17240
|
+
const events = [];
|
|
17241
|
+
for (const rs of resourceSpansList) {
|
|
17242
|
+
const resourceAttrs = rs.resource?.attributes;
|
|
17243
|
+
const scopeSpansList = [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []];
|
|
17244
|
+
for (const ss of scopeSpansList) for (const otlpSpan of ss.spans ?? []) {
|
|
17245
|
+
spans.push(mapOtlpSpanToRow(otlpSpan, resourceAttrs));
|
|
17246
|
+
for (const event of otlpSpan.events ?? []) events.push({
|
|
17247
|
+
id: `${otlpSpan.spanId}-${event.name}-${event.timeUnixNano}`,
|
|
17248
|
+
traceId: otlpSpan.traceId,
|
|
17249
|
+
spanId: otlpSpan.spanId,
|
|
17250
|
+
name: event.name,
|
|
17251
|
+
timestamp: nanoToISOString(event.timeUnixNano),
|
|
17252
|
+
attributes: attrsToRecord(event.attributes)
|
|
17253
|
+
});
|
|
17254
|
+
}
|
|
17255
|
+
}
|
|
17256
|
+
if (spans.length === 0) return void 0;
|
|
17257
|
+
const rootSpan = spans.find((s) => !s.parentSpanId) ?? spans[0];
|
|
17258
|
+
const startTimes = spans.map((s) => new Date(s.startTime).getTime());
|
|
17259
|
+
const endTimes = spans.filter((s) => s.endTime).map((s) => new Date(s.endTime).getTime());
|
|
17260
|
+
const traceStart = Math.min(...startTimes);
|
|
17261
|
+
const traceEnd = endTimes.length > 0 ? Math.max(...endTimes) : null;
|
|
17262
|
+
const totalInputTokens = spans.reduce((sum, s) => sum + s.promptTokens, 0);
|
|
17263
|
+
const totalOutputTokens = spans.reduce((sum, s) => sum + s.completionTokens, 0);
|
|
17264
|
+
const hasError = spans.some((s) => s.status === 2);
|
|
17265
|
+
const hasOk = spans.some((s) => s.status === 1);
|
|
17266
|
+
const traceStatus = hasError ? "error" : hasOk ? "ok" : "unset";
|
|
17267
|
+
return {
|
|
17268
|
+
trace: {
|
|
17269
|
+
id: traceId,
|
|
17270
|
+
traceId,
|
|
17271
|
+
name: rootSpan.name,
|
|
17272
|
+
sessionId: null,
|
|
17273
|
+
userId: null,
|
|
17274
|
+
status: traceStatus,
|
|
17275
|
+
startTime: new Date(traceStart).toISOString(),
|
|
17276
|
+
endTime: traceEnd !== null ? new Date(traceEnd).toISOString() : null,
|
|
17277
|
+
durationMs: traceEnd !== null ? traceEnd - traceStart : null,
|
|
17278
|
+
spanCount: spans.length,
|
|
17279
|
+
totalInputTokens,
|
|
17280
|
+
totalOutputTokens,
|
|
17281
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
17282
|
+
totalCost: 0,
|
|
17283
|
+
tags: {},
|
|
17284
|
+
metadata: {},
|
|
17285
|
+
createdAt: new Date(traceStart).toISOString(),
|
|
17286
|
+
updatedAt: new Date(traceStart).toISOString()
|
|
17287
|
+
},
|
|
17288
|
+
spans,
|
|
17289
|
+
events
|
|
17290
|
+
};
|
|
17291
|
+
},
|
|
17292
|
+
async getTraceStats(params) {
|
|
17293
|
+
const qs = new URLSearchParams();
|
|
17294
|
+
qs.set("start", String(Math.floor(params.startDate.getTime() / 1e3)));
|
|
17295
|
+
qs.set("end", String(Math.floor(params.endDate.getTime() / 1e3)));
|
|
17296
|
+
qs.set("limit", "1000");
|
|
17297
|
+
const traces = (await tempoFetch(`/api/search?${qs.toString()}`)).traces ?? [];
|
|
17298
|
+
const totalTraces = traces.length;
|
|
17299
|
+
const durations = traces.map((t) => t.durationMs).filter((d) => d !== void 0);
|
|
17300
|
+
const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
17301
|
+
let totalSpans = 0;
|
|
17302
|
+
for (const t of traces) if (t.serviceStats) totalSpans += Object.values(t.serviceStats).reduce((sum, s) => sum + s.spanCount, 0);
|
|
17303
|
+
else {
|
|
17304
|
+
const sets = t.spanSets ?? (t.spanSet ? [t.spanSet] : []);
|
|
17305
|
+
for (const ss of sets) totalSpans += ss.matched ?? ss.spans?.length ?? 0;
|
|
17306
|
+
}
|
|
17307
|
+
return {
|
|
17308
|
+
totalTraces,
|
|
17309
|
+
avgDurationMs: Math.round(avgDurationMs),
|
|
17310
|
+
errorCount: 0,
|
|
17311
|
+
totalCost: 0,
|
|
17312
|
+
totalTokens: 0,
|
|
17313
|
+
totalSpans: totalSpans || totalTraces
|
|
17314
|
+
};
|
|
17315
|
+
}
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
17318
|
+
|
|
17075
17319
|
//#endregion
|
|
17076
17320
|
//#region src/server/handlers/traces/index.ts
|
|
17077
17321
|
/**
|
|
17322
|
+
* Cached OTLP query client singleton (created once per endpoint config)
|
|
17323
|
+
*/
|
|
17324
|
+
let cachedOtlpClient = null;
|
|
17325
|
+
let cachedOtlpEndpoint = null;
|
|
17326
|
+
function getOtlpClient(otlp) {
|
|
17327
|
+
if (cachedOtlpClient && cachedOtlpEndpoint === otlp.endpoint) return cachedOtlpClient;
|
|
17328
|
+
cachedOtlpClient = createOtlpQueryClient(otlp);
|
|
17329
|
+
cachedOtlpEndpoint = otlp.endpoint;
|
|
17330
|
+
return cachedOtlpClient;
|
|
17331
|
+
}
|
|
17332
|
+
/**
|
|
17078
17333
|
* Parse ISO date string to Date object
|
|
17079
17334
|
*/
|
|
17080
17335
|
const isoDateString = zod_default.string().refine((val) => !isNaN(new Date(val).getTime()), { message: "Invalid date format. Expected ISO 8601 string." });
|
|
@@ -17119,30 +17374,40 @@ const app$8 = new hono.Hono().get("/", zv("query", zod_default.object({
|
|
|
17119
17374
|
endDate: isoDateString.optional(),
|
|
17120
17375
|
tags: zod_default.string().optional()
|
|
17121
17376
|
})), async (c) => {
|
|
17122
|
-
const
|
|
17377
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17123
17378
|
const query = c.req.valid("query");
|
|
17379
|
+
const params = {
|
|
17380
|
+
limit: query.limit,
|
|
17381
|
+
offset: query.offset,
|
|
17382
|
+
sessionId: query.sessionId,
|
|
17383
|
+
userId: query.userId,
|
|
17384
|
+
status: query.status,
|
|
17385
|
+
name: query.name,
|
|
17386
|
+
startDate: query.startDate ? parseStartDate(query.startDate) : void 0,
|
|
17387
|
+
endDate: query.endDate ? parseEndDate(query.endDate) : void 0,
|
|
17388
|
+
tags: parseTags(query.tags)
|
|
17389
|
+
};
|
|
17124
17390
|
try {
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
status: query.status,
|
|
17131
|
-
name: query.name,
|
|
17132
|
-
startDate: query.startDate ? parseStartDate(query.startDate) : void 0,
|
|
17133
|
-
endDate: query.endDate ? parseEndDate(query.endDate) : void 0,
|
|
17134
|
-
tags: parseTags(query.tags)
|
|
17135
|
-
});
|
|
17391
|
+
if (otlp) {
|
|
17392
|
+
const result$1 = await getOtlpClient(otlp).listTraces(params);
|
|
17393
|
+
return c.json(successResponse(result$1, 200));
|
|
17394
|
+
}
|
|
17395
|
+
const result = await c.get("db").listTraces(params);
|
|
17136
17396
|
return c.json(successResponse(result, 200));
|
|
17137
17397
|
} catch (error$47) {
|
|
17138
17398
|
console.error("Error fetching traces:", error$47);
|
|
17139
17399
|
return c.json(internalServerError("Failed to fetch traces", 500), 500);
|
|
17140
17400
|
}
|
|
17141
17401
|
}).get("/:traceId", zv("param", zod_default.object({ traceId: zod_default.string() })), async (c) => {
|
|
17142
|
-
const
|
|
17402
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17143
17403
|
const { traceId } = c.req.valid("param");
|
|
17144
17404
|
try {
|
|
17145
|
-
|
|
17405
|
+
if (otlp) {
|
|
17406
|
+
const result$1 = await getOtlpClient(otlp).getTraceWithSpans(traceId);
|
|
17407
|
+
if (!result$1) return c.json({ error: "Trace not found" }, 404);
|
|
17408
|
+
return c.json(successResponse(result$1, 200));
|
|
17409
|
+
}
|
|
17410
|
+
const result = await c.get("db").getTraceWithSpans(traceId);
|
|
17146
17411
|
if (!result) return c.json({ error: "Trace not found" }, 404);
|
|
17147
17412
|
return c.json(successResponse(result, 200));
|
|
17148
17413
|
} catch (error$47) {
|
|
@@ -17153,10 +17418,19 @@ const app$8 = new hono.Hono().get("/", zv("query", zod_default.object({
|
|
|
17153
17418
|
sessionId: zod_default.string().optional(),
|
|
17154
17419
|
userId: zod_default.string().optional()
|
|
17155
17420
|
})), async (c) => {
|
|
17156
|
-
const
|
|
17421
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17157
17422
|
const { startDate, endDate, sessionId, userId } = c.req.valid("query");
|
|
17158
17423
|
try {
|
|
17159
|
-
|
|
17424
|
+
if (otlp) {
|
|
17425
|
+
const data$1 = await getOtlpClient(otlp).getTraceStats({
|
|
17426
|
+
startDate,
|
|
17427
|
+
endDate,
|
|
17428
|
+
sessionId,
|
|
17429
|
+
userId
|
|
17430
|
+
});
|
|
17431
|
+
return c.json(successResponse(data$1, 200));
|
|
17432
|
+
}
|
|
17433
|
+
const data = await c.get("db").getTraceStats({
|
|
17160
17434
|
startDate,
|
|
17161
17435
|
endDate,
|
|
17162
17436
|
sessionId,
|
package/dist/index.mjs
CHANGED
|
@@ -17043,9 +17043,264 @@ const app$9 = new Hono().post("/", zv("json", zod_default.object({
|
|
|
17043
17043
|
});
|
|
17044
17044
|
var targeting_default = app$9;
|
|
17045
17045
|
|
|
17046
|
+
//#endregion
|
|
17047
|
+
//#region src/server/services/otlpQueryClient.ts
|
|
17048
|
+
function nanoToISOString(nanoStr) {
|
|
17049
|
+
const ms = Math.floor(Number(BigInt(nanoStr) / BigInt(1e6)));
|
|
17050
|
+
return new Date(ms).toISOString();
|
|
17051
|
+
}
|
|
17052
|
+
function nanoToMs(nanoStr) {
|
|
17053
|
+
return Math.floor(Number(BigInt(nanoStr) / BigInt(1e6)));
|
|
17054
|
+
}
|
|
17055
|
+
function extractAttr(attrs, keys) {
|
|
17056
|
+
if (!attrs) return void 0;
|
|
17057
|
+
for (const key of keys) {
|
|
17058
|
+
const kv = attrs.find((a) => a.key === key);
|
|
17059
|
+
if (kv) {
|
|
17060
|
+
const v = kv.value;
|
|
17061
|
+
if (v.stringValue !== void 0) return v.stringValue;
|
|
17062
|
+
if (v.intValue !== void 0) return Number(v.intValue);
|
|
17063
|
+
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
17064
|
+
}
|
|
17065
|
+
}
|
|
17066
|
+
}
|
|
17067
|
+
function attrsToRecord(attrs) {
|
|
17068
|
+
if (!attrs) return {};
|
|
17069
|
+
const out = {};
|
|
17070
|
+
for (const kv of attrs) {
|
|
17071
|
+
const v = kv.value;
|
|
17072
|
+
if (v.stringValue !== void 0) out[kv.key] = v.stringValue;
|
|
17073
|
+
else if (v.intValue !== void 0) out[kv.key] = Number(v.intValue);
|
|
17074
|
+
else if (v.doubleValue !== void 0) out[kv.key] = v.doubleValue;
|
|
17075
|
+
else if (v.boolValue !== void 0) out[kv.key] = v.boolValue;
|
|
17076
|
+
}
|
|
17077
|
+
return out;
|
|
17078
|
+
}
|
|
17079
|
+
function mapOtlpSpanToRow(span, resourceAttrs) {
|
|
17080
|
+
const allAttrs = [...resourceAttrs ?? [], ...span.attributes ?? []];
|
|
17081
|
+
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
17082
|
+
const endMs = span.endTimeUnixNano ? nanoToMs(span.endTimeUnixNano) : null;
|
|
17083
|
+
const durationMs = endMs !== null ? endMs - startMs : null;
|
|
17084
|
+
const provider = extractAttr(allAttrs, [
|
|
17085
|
+
"gen_ai.provider.name",
|
|
17086
|
+
"gen_ai.system",
|
|
17087
|
+
"ai.model.provider"
|
|
17088
|
+
]) ?? null;
|
|
17089
|
+
const model = extractAttr(allAttrs, ["gen_ai.request.model", "ai.model.id"]) ?? null;
|
|
17090
|
+
const promptTokens = Number(extractAttr(allAttrs, ["gen_ai.usage.input_tokens", "ai.usage.promptTokens"]) ?? 0);
|
|
17091
|
+
const completionTokens = Number(extractAttr(allAttrs, ["gen_ai.usage.output_tokens", "ai.usage.completionTokens"]) ?? 0);
|
|
17092
|
+
const input = extractAttr(allAttrs, [
|
|
17093
|
+
"ai.prompt.messages",
|
|
17094
|
+
"gen_ai.prompt",
|
|
17095
|
+
"ai.prompt"
|
|
17096
|
+
]) ?? null;
|
|
17097
|
+
const output = extractAttr(allAttrs, [
|
|
17098
|
+
"ai.response.text",
|
|
17099
|
+
"ai.response.object",
|
|
17100
|
+
"gen_ai.completion",
|
|
17101
|
+
"ai.response.toolCalls"
|
|
17102
|
+
]) ?? null;
|
|
17103
|
+
return {
|
|
17104
|
+
id: span.spanId,
|
|
17105
|
+
traceId: span.traceId,
|
|
17106
|
+
spanId: span.spanId,
|
|
17107
|
+
parentSpanId: span.parentSpanId || null,
|
|
17108
|
+
name: span.name,
|
|
17109
|
+
kind: span.kind ?? 1,
|
|
17110
|
+
status: span.status?.code ?? 0,
|
|
17111
|
+
statusMessage: span.status?.message ?? null,
|
|
17112
|
+
startTime: new Date(startMs).toISOString(),
|
|
17113
|
+
endTime: endMs !== null ? new Date(endMs).toISOString() : null,
|
|
17114
|
+
durationMs,
|
|
17115
|
+
provider,
|
|
17116
|
+
model,
|
|
17117
|
+
promptTokens,
|
|
17118
|
+
completionTokens,
|
|
17119
|
+
totalTokens: promptTokens + completionTokens,
|
|
17120
|
+
cost: 0,
|
|
17121
|
+
source: "otlp",
|
|
17122
|
+
input,
|
|
17123
|
+
output,
|
|
17124
|
+
attributes: attrsToRecord(allAttrs)
|
|
17125
|
+
};
|
|
17126
|
+
}
|
|
17127
|
+
function createOtlpQueryClient(otlpConfig) {
|
|
17128
|
+
const baseUrl = otlpConfig.endpoint.replace(/\/$/, "");
|
|
17129
|
+
const headers = {
|
|
17130
|
+
Accept: "application/json",
|
|
17131
|
+
...otlpConfig.headers
|
|
17132
|
+
};
|
|
17133
|
+
async function tempoFetch(path) {
|
|
17134
|
+
const url$1 = `${baseUrl}${path}`;
|
|
17135
|
+
logger.info(`[OtlpQuery] GET ${url$1}`);
|
|
17136
|
+
const res = await fetch(url$1, { headers });
|
|
17137
|
+
if (!res.ok) {
|
|
17138
|
+
const body = await res.text().catch(() => "");
|
|
17139
|
+
throw new Error(`OTLP query failed: ${res.status} ${res.statusText} — ${body}`);
|
|
17140
|
+
}
|
|
17141
|
+
return res.json();
|
|
17142
|
+
}
|
|
17143
|
+
return {
|
|
17144
|
+
async listTraces(params) {
|
|
17145
|
+
const limit = params.limit ?? 50;
|
|
17146
|
+
const offset = params.offset ?? 0;
|
|
17147
|
+
const qs = new URLSearchParams();
|
|
17148
|
+
qs.set("limit", String(limit + offset));
|
|
17149
|
+
if (params.startDate) qs.set("start", String(Math.floor(params.startDate.getTime() / 1e3)));
|
|
17150
|
+
if (params.endDate) qs.set("end", String(Math.floor(params.endDate.getTime() / 1e3)));
|
|
17151
|
+
const filters = [];
|
|
17152
|
+
if (params.name) filters.push(`name = "${params.name}"`);
|
|
17153
|
+
if (params.status) {
|
|
17154
|
+
const statusMap = {
|
|
17155
|
+
ok: "ok",
|
|
17156
|
+
error: "error",
|
|
17157
|
+
unset: "unset"
|
|
17158
|
+
};
|
|
17159
|
+
if (statusMap[params.status]) filters.push(`status = ${statusMap[params.status]}`);
|
|
17160
|
+
}
|
|
17161
|
+
if (params.tags) {
|
|
17162
|
+
for (const [key, values] of Object.entries(params.tags)) if (values.length > 0) filters.push(`span.${key} = "${values[0]}"`);
|
|
17163
|
+
}
|
|
17164
|
+
if (filters.length > 0) qs.set("q", `{ ${filters.join(" && ")} }`);
|
|
17165
|
+
const allTraces = ((await tempoFetch(`/api/search?${qs.toString()}`)).traces ?? []).map((t) => {
|
|
17166
|
+
const startMs = nanoToMs(t.startTimeUnixNano);
|
|
17167
|
+
const durationMs = t.durationMs ?? null;
|
|
17168
|
+
const endMs = durationMs !== null ? startMs + durationMs : null;
|
|
17169
|
+
let spanCount = 0;
|
|
17170
|
+
const sets = t.spanSets ?? (t.spanSet ? [t.spanSet] : []);
|
|
17171
|
+
for (const ss of sets) spanCount += ss.matched ?? ss.spans?.length ?? 0;
|
|
17172
|
+
if (t.serviceStats) spanCount = Object.values(t.serviceStats).reduce((sum, s) => sum + s.spanCount, 0);
|
|
17173
|
+
return {
|
|
17174
|
+
id: t.traceID,
|
|
17175
|
+
traceId: t.traceID,
|
|
17176
|
+
name: t.rootTraceName ?? null,
|
|
17177
|
+
sessionId: null,
|
|
17178
|
+
userId: null,
|
|
17179
|
+
status: "unset",
|
|
17180
|
+
startTime: new Date(startMs).toISOString(),
|
|
17181
|
+
endTime: endMs !== null ? new Date(endMs).toISOString() : null,
|
|
17182
|
+
durationMs,
|
|
17183
|
+
spanCount: spanCount || 1,
|
|
17184
|
+
totalInputTokens: 0,
|
|
17185
|
+
totalOutputTokens: 0,
|
|
17186
|
+
totalTokens: 0,
|
|
17187
|
+
totalCost: 0,
|
|
17188
|
+
tags: {},
|
|
17189
|
+
metadata: {},
|
|
17190
|
+
createdAt: new Date(startMs).toISOString(),
|
|
17191
|
+
updatedAt: new Date(startMs).toISOString()
|
|
17192
|
+
};
|
|
17193
|
+
});
|
|
17194
|
+
return {
|
|
17195
|
+
data: allTraces.slice(offset, offset + limit),
|
|
17196
|
+
total: allTraces.length,
|
|
17197
|
+
limit,
|
|
17198
|
+
offset
|
|
17199
|
+
};
|
|
17200
|
+
},
|
|
17201
|
+
async getTraceWithSpans(traceId) {
|
|
17202
|
+
let result;
|
|
17203
|
+
try {
|
|
17204
|
+
result = await tempoFetch(`/api/traces/${traceId}`);
|
|
17205
|
+
} catch (e) {
|
|
17206
|
+
logger.error(`[OtlpQuery] Failed to fetch trace ${traceId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
17207
|
+
return;
|
|
17208
|
+
}
|
|
17209
|
+
const resourceSpansList = result.resourceSpans ?? result.batches ?? [];
|
|
17210
|
+
const spans = [];
|
|
17211
|
+
const events = [];
|
|
17212
|
+
for (const rs of resourceSpansList) {
|
|
17213
|
+
const resourceAttrs = rs.resource?.attributes;
|
|
17214
|
+
const scopeSpansList = [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []];
|
|
17215
|
+
for (const ss of scopeSpansList) for (const otlpSpan of ss.spans ?? []) {
|
|
17216
|
+
spans.push(mapOtlpSpanToRow(otlpSpan, resourceAttrs));
|
|
17217
|
+
for (const event of otlpSpan.events ?? []) events.push({
|
|
17218
|
+
id: `${otlpSpan.spanId}-${event.name}-${event.timeUnixNano}`,
|
|
17219
|
+
traceId: otlpSpan.traceId,
|
|
17220
|
+
spanId: otlpSpan.spanId,
|
|
17221
|
+
name: event.name,
|
|
17222
|
+
timestamp: nanoToISOString(event.timeUnixNano),
|
|
17223
|
+
attributes: attrsToRecord(event.attributes)
|
|
17224
|
+
});
|
|
17225
|
+
}
|
|
17226
|
+
}
|
|
17227
|
+
if (spans.length === 0) return void 0;
|
|
17228
|
+
const rootSpan = spans.find((s) => !s.parentSpanId) ?? spans[0];
|
|
17229
|
+
const startTimes = spans.map((s) => new Date(s.startTime).getTime());
|
|
17230
|
+
const endTimes = spans.filter((s) => s.endTime).map((s) => new Date(s.endTime).getTime());
|
|
17231
|
+
const traceStart = Math.min(...startTimes);
|
|
17232
|
+
const traceEnd = endTimes.length > 0 ? Math.max(...endTimes) : null;
|
|
17233
|
+
const totalInputTokens = spans.reduce((sum, s) => sum + s.promptTokens, 0);
|
|
17234
|
+
const totalOutputTokens = spans.reduce((sum, s) => sum + s.completionTokens, 0);
|
|
17235
|
+
const hasError = spans.some((s) => s.status === 2);
|
|
17236
|
+
const hasOk = spans.some((s) => s.status === 1);
|
|
17237
|
+
const traceStatus = hasError ? "error" : hasOk ? "ok" : "unset";
|
|
17238
|
+
return {
|
|
17239
|
+
trace: {
|
|
17240
|
+
id: traceId,
|
|
17241
|
+
traceId,
|
|
17242
|
+
name: rootSpan.name,
|
|
17243
|
+
sessionId: null,
|
|
17244
|
+
userId: null,
|
|
17245
|
+
status: traceStatus,
|
|
17246
|
+
startTime: new Date(traceStart).toISOString(),
|
|
17247
|
+
endTime: traceEnd !== null ? new Date(traceEnd).toISOString() : null,
|
|
17248
|
+
durationMs: traceEnd !== null ? traceEnd - traceStart : null,
|
|
17249
|
+
spanCount: spans.length,
|
|
17250
|
+
totalInputTokens,
|
|
17251
|
+
totalOutputTokens,
|
|
17252
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
17253
|
+
totalCost: 0,
|
|
17254
|
+
tags: {},
|
|
17255
|
+
metadata: {},
|
|
17256
|
+
createdAt: new Date(traceStart).toISOString(),
|
|
17257
|
+
updatedAt: new Date(traceStart).toISOString()
|
|
17258
|
+
},
|
|
17259
|
+
spans,
|
|
17260
|
+
events
|
|
17261
|
+
};
|
|
17262
|
+
},
|
|
17263
|
+
async getTraceStats(params) {
|
|
17264
|
+
const qs = new URLSearchParams();
|
|
17265
|
+
qs.set("start", String(Math.floor(params.startDate.getTime() / 1e3)));
|
|
17266
|
+
qs.set("end", String(Math.floor(params.endDate.getTime() / 1e3)));
|
|
17267
|
+
qs.set("limit", "1000");
|
|
17268
|
+
const traces = (await tempoFetch(`/api/search?${qs.toString()}`)).traces ?? [];
|
|
17269
|
+
const totalTraces = traces.length;
|
|
17270
|
+
const durations = traces.map((t) => t.durationMs).filter((d) => d !== void 0);
|
|
17271
|
+
const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
17272
|
+
let totalSpans = 0;
|
|
17273
|
+
for (const t of traces) if (t.serviceStats) totalSpans += Object.values(t.serviceStats).reduce((sum, s) => sum + s.spanCount, 0);
|
|
17274
|
+
else {
|
|
17275
|
+
const sets = t.spanSets ?? (t.spanSet ? [t.spanSet] : []);
|
|
17276
|
+
for (const ss of sets) totalSpans += ss.matched ?? ss.spans?.length ?? 0;
|
|
17277
|
+
}
|
|
17278
|
+
return {
|
|
17279
|
+
totalTraces,
|
|
17280
|
+
avgDurationMs: Math.round(avgDurationMs),
|
|
17281
|
+
errorCount: 0,
|
|
17282
|
+
totalCost: 0,
|
|
17283
|
+
totalTokens: 0,
|
|
17284
|
+
totalSpans: totalSpans || totalTraces
|
|
17285
|
+
};
|
|
17286
|
+
}
|
|
17287
|
+
};
|
|
17288
|
+
}
|
|
17289
|
+
|
|
17046
17290
|
//#endregion
|
|
17047
17291
|
//#region src/server/handlers/traces/index.ts
|
|
17048
17292
|
/**
|
|
17293
|
+
* Cached OTLP query client singleton (created once per endpoint config)
|
|
17294
|
+
*/
|
|
17295
|
+
let cachedOtlpClient = null;
|
|
17296
|
+
let cachedOtlpEndpoint = null;
|
|
17297
|
+
function getOtlpClient(otlp) {
|
|
17298
|
+
if (cachedOtlpClient && cachedOtlpEndpoint === otlp.endpoint) return cachedOtlpClient;
|
|
17299
|
+
cachedOtlpClient = createOtlpQueryClient(otlp);
|
|
17300
|
+
cachedOtlpEndpoint = otlp.endpoint;
|
|
17301
|
+
return cachedOtlpClient;
|
|
17302
|
+
}
|
|
17303
|
+
/**
|
|
17049
17304
|
* Parse ISO date string to Date object
|
|
17050
17305
|
*/
|
|
17051
17306
|
const isoDateString = zod_default.string().refine((val) => !isNaN(new Date(val).getTime()), { message: "Invalid date format. Expected ISO 8601 string." });
|
|
@@ -17090,30 +17345,40 @@ const app$8 = new Hono().get("/", zv("query", zod_default.object({
|
|
|
17090
17345
|
endDate: isoDateString.optional(),
|
|
17091
17346
|
tags: zod_default.string().optional()
|
|
17092
17347
|
})), async (c) => {
|
|
17093
|
-
const
|
|
17348
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17094
17349
|
const query = c.req.valid("query");
|
|
17350
|
+
const params = {
|
|
17351
|
+
limit: query.limit,
|
|
17352
|
+
offset: query.offset,
|
|
17353
|
+
sessionId: query.sessionId,
|
|
17354
|
+
userId: query.userId,
|
|
17355
|
+
status: query.status,
|
|
17356
|
+
name: query.name,
|
|
17357
|
+
startDate: query.startDate ? parseStartDate(query.startDate) : void 0,
|
|
17358
|
+
endDate: query.endDate ? parseEndDate(query.endDate) : void 0,
|
|
17359
|
+
tags: parseTags(query.tags)
|
|
17360
|
+
};
|
|
17095
17361
|
try {
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
status: query.status,
|
|
17102
|
-
name: query.name,
|
|
17103
|
-
startDate: query.startDate ? parseStartDate(query.startDate) : void 0,
|
|
17104
|
-
endDate: query.endDate ? parseEndDate(query.endDate) : void 0,
|
|
17105
|
-
tags: parseTags(query.tags)
|
|
17106
|
-
});
|
|
17362
|
+
if (otlp) {
|
|
17363
|
+
const result$1 = await getOtlpClient(otlp).listTraces(params);
|
|
17364
|
+
return c.json(successResponse(result$1, 200));
|
|
17365
|
+
}
|
|
17366
|
+
const result = await c.get("db").listTraces(params);
|
|
17107
17367
|
return c.json(successResponse(result, 200));
|
|
17108
17368
|
} catch (error$47) {
|
|
17109
17369
|
console.error("Error fetching traces:", error$47);
|
|
17110
17370
|
return c.json(internalServerError("Failed to fetch traces", 500), 500);
|
|
17111
17371
|
}
|
|
17112
17372
|
}).get("/:traceId", zv("param", zod_default.object({ traceId: zod_default.string() })), async (c) => {
|
|
17113
|
-
const
|
|
17373
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17114
17374
|
const { traceId } = c.req.valid("param");
|
|
17115
17375
|
try {
|
|
17116
|
-
|
|
17376
|
+
if (otlp) {
|
|
17377
|
+
const result$1 = await getOtlpClient(otlp).getTraceWithSpans(traceId);
|
|
17378
|
+
if (!result$1) return c.json({ error: "Trace not found" }, 404);
|
|
17379
|
+
return c.json(successResponse(result$1, 200));
|
|
17380
|
+
}
|
|
17381
|
+
const result = await c.get("db").getTraceWithSpans(traceId);
|
|
17117
17382
|
if (!result) return c.json({ error: "Trace not found" }, 404);
|
|
17118
17383
|
return c.json(successResponse(result, 200));
|
|
17119
17384
|
} catch (error$47) {
|
|
@@ -17124,10 +17389,19 @@ const app$8 = new Hono().get("/", zv("query", zod_default.object({
|
|
|
17124
17389
|
sessionId: zod_default.string().optional(),
|
|
17125
17390
|
userId: zod_default.string().optional()
|
|
17126
17391
|
})), async (c) => {
|
|
17127
|
-
const
|
|
17392
|
+
const otlp = c.get("llmopsConfig")?.otlp;
|
|
17128
17393
|
const { startDate, endDate, sessionId, userId } = c.req.valid("query");
|
|
17129
17394
|
try {
|
|
17130
|
-
|
|
17395
|
+
if (otlp) {
|
|
17396
|
+
const data$1 = await getOtlpClient(otlp).getTraceStats({
|
|
17397
|
+
startDate,
|
|
17398
|
+
endDate,
|
|
17399
|
+
sessionId,
|
|
17400
|
+
userId
|
|
17401
|
+
});
|
|
17402
|
+
return c.json(successResponse(data$1, 200));
|
|
17403
|
+
}
|
|
17404
|
+
const data = await c.get("db").getTraceStats({
|
|
17131
17405
|
startDate,
|
|
17132
17406
|
endDate,
|
|
17133
17407
|
sessionId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmops/app",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7-beta.1",
|
|
4
4
|
"description": "LLMOps application with server and client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"react-hook-form": "^7.68.0",
|
|
78
78
|
"recharts": "^3.6.0",
|
|
79
79
|
"uuid": "^13.0.0",
|
|
80
|
-
"@llmops/core": "^0.6.
|
|
81
|
-
"@llmops/gateway": "^0.6.
|
|
80
|
+
"@llmops/core": "^0.6.7-beta.1",
|
|
81
|
+
"@llmops/gateway": "^0.6.7-beta.1"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"react": "^19.2.1",
|