Lumi/plugins/lumi_ai/backend/metrics.js
2026-06-12 11:54:46 +02:00

84 lines
4.4 KiB
JavaScript

const fs = require("fs");
const { resolveData } = require("./paths");
const historyFile = () => resolveData("metrics", "history.jsonl");
const stateFile = () => resolveData("metrics", "summary.json");
function getSummary() {
try { return JSON.parse(fs.readFileSync(stateFile(), "utf8")); }
catch { return { total_requests:0, successful:0, failed:0, refusals:0, tool_suggestions:0, tool_executions:0, tool_denials:0, confirmation_cancellations:0, timeout_count:0, runtime_crash_count:0, runtime_self_test_total:0, runtime_self_test_failed_total:0, runtime_start_attempt_total:0, runtime_start_failed_total:0, verified_downloads:0, failed_downloads:0, requests_by_role:{}, requests_by_scope:{}, requests_by_route:{}, runtime_exit_code_counts:{}, durations:[], queue_wait_total_ms:0 }; }
}
function record(entry) {
const summary = getSummary();
summary.requests_by_role ||= {};
summary.requests_by_scope ||= {};
summary.requests_by_route ||= {};
if (entry.kind === "request") {
summary.total_requests += 1;
if (entry.status === "success") summary.successful += 1;
if (entry.status === "failed") summary.failed += 1;
if (entry.status === "refused") summary.refusals += 1;
if (entry.role) summary.requests_by_role[entry.role] = (summary.requests_by_role[entry.role] || 0) + 1;
if (entry.scope) summary.requests_by_scope[entry.scope] = (summary.requests_by_scope[entry.scope] || 0) + 1;
}
if (entry.route_used) {
summary.requests_by_route[entry.route_used] = (summary.requests_by_route[entry.route_used] || 0) + 1;
}
if (entry.tool_requested) summary.tool_suggestions += 1;
if (entry.tool_executed) summary.tool_executions += 1;
if (entry.kind === "tool" && entry.status === "failed") summary.tool_denials += 1;
if (entry.kind === "tool" && entry.status === "cancelled") summary.confirmation_cancellations += 1;
if (entry.timeout) summary.timeout_count += 1;
if (entry.runtime_crash) summary.runtime_crash_count += 1;
if (entry.kind === "runtime_self_test") {
summary.runtime_self_test_total += 1;
if (entry.status === "failed") summary.runtime_self_test_failed_total += 1;
}
if (entry.kind === "runtime_start") {
if (entry.status === "attempt") summary.runtime_start_attempt_total += 1;
if (entry.status === "failed") summary.runtime_start_failed_total += 1;
}
if (entry.code) {
summary.runtime_exit_code_counts ||= {};
summary.runtime_exit_code_counts[entry.code] = (summary.runtime_exit_code_counts[entry.code] || 0) + 1;
}
if (entry.kind === "download" && entry.status === "success") summary.verified_downloads += 1;
if (entry.kind === "download" && entry.status === "failed") summary.failed_downloads += 1;
if (entry.duration_ms != null) summary.durations.push(entry.duration_ms);
summary.durations = summary.durations.slice(-500);
if (entry.queue_wait_ms) summary.queue_wait_total_ms += entry.queue_wait_ms;
fs.writeFileSync(stateFile(), JSON.stringify(summary, null, 2));
fs.appendFileSync(historyFile(), `${JSON.stringify({ timestamp:new Date().toISOString(), ...entry })}\n`);
}
function report() {
const s = getSummary(); const sorted=[...s.durations].sort((a,b)=>a-b);
return { ...s, average_response_ms: sorted.length ? Math.round(sorted.reduce((a,b)=>a+b,0)/sorted.length) : 0, median_response_ms: sorted.length ? sorted[Math.floor(sorted.length/2)] : 0 };
}
function history(limit=100) {
try { return fs.readFileSync(historyFile(),"utf8").trim().split(/\r?\n/).filter(Boolean).slice(-limit).reverse().map(JSON.parse); } catch { return []; }
}
function historyPage(page = 1, pageSize = 25) {
const safePage = Math.max(1, Number.parseInt(page, 10) || 1);
const safeSize = Math.max(1, Math.min(100, Number.parseInt(pageSize, 10) || 25));
try {
const rows = fs.readFileSync(historyFile(), "utf8").trim().split(/\r?\n/).filter(Boolean);
return paginateRows(rows, safePage, safeSize, JSON.parse);
} catch {
return { entries: [], page: 1, pages: 1, page_size: safeSize, total: 0 };
}
}
function paginateRows(rows, page = 1, pageSize = 25, map = (value) => value) {
const total = rows.length;
const pages = Math.max(1, Math.ceil(total / pageSize));
const current = Math.min(Math.max(1, page), pages);
const end = total - (current - 1) * pageSize;
const start = Math.max(0, end - pageSize);
return {
entries: rows.slice(start, end).reverse().map(map),
page: current,
pages,
page_size: pageSize,
total
};
}
module.exports = { record, report, history, historyPage, paginateRows };