@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.
Files changed (3) hide show
  1. package/dist/index.cjs +290 -16
  2. package/dist/index.mjs +290 -16
  3. 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 db = c.get("db");
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
- const result = await db.listTraces({
17126
- limit: query.limit,
17127
- offset: query.offset,
17128
- sessionId: query.sessionId,
17129
- userId: query.userId,
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 db = c.get("db");
17402
+ const otlp = c.get("llmopsConfig")?.otlp;
17143
17403
  const { traceId } = c.req.valid("param");
17144
17404
  try {
17145
- const result = await db.getTraceWithSpans(traceId);
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 db = c.get("db");
17421
+ const otlp = c.get("llmopsConfig")?.otlp;
17157
17422
  const { startDate, endDate, sessionId, userId } = c.req.valid("query");
17158
17423
  try {
17159
- const data = await db.getTraceStats({
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 db = c.get("db");
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
- const result = await db.listTraces({
17097
- limit: query.limit,
17098
- offset: query.offset,
17099
- sessionId: query.sessionId,
17100
- userId: query.userId,
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 db = c.get("db");
17373
+ const otlp = c.get("llmopsConfig")?.otlp;
17114
17374
  const { traceId } = c.req.valid("param");
17115
17375
  try {
17116
- const result = await db.getTraceWithSpans(traceId);
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 db = c.get("db");
17392
+ const otlp = c.get("llmopsConfig")?.otlp;
17128
17393
  const { startDate, endDate, sessionId, userId } = c.req.valid("query");
17129
17394
  try {
17130
- const data = await db.getTraceStats({
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.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.6",
81
- "@llmops/gateway": "^0.6.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",