feat(metrics): render the operator health snapshot as a report (#19)
Quality gates / quality (pull_request) Canceled after 0s
Quality gates / quality (pull_request) Canceled after 0s
The counters had an endpoint and no way to read them without jq. Adds a pure formatter plus a CLI that prints the app, queue, and model boundaries. The formatter is a function of the snapshot and the three frozen vocabularies, so a caller-supplied failure label cannot reach the terminal: unknown classes are simply not printed. Refusal paths are explicit — 404 means the route is off because no token is configured, 403 means the token was rejected — and the token is never echoed in the report or an error line. RED: tests/model-metrics-report-format.test.js failed with ERR_MODULE_NOT_FOUND before src/model-metrics-report.js existed; the refusal-path test failed with a generic 'answered 404' before the 404 branch was written. Tests: npm test 298/298, npm run test:metrics-report 8/8, syntax and diff gates clean, real-Chromium ui acceptance green with the privacy disclosure visible.
This commit is contained in:
1 parent
3e2253a052
commit
e08b537866
6 files changed
+311
-3
No files matched your search
@@ -0,0 +1,74 @@
|
||||
// Renders the operator snapshot from `src/model-metrics.js` as a plain-text report.
|
||||
//
|
||||
// This module is a pure function of the snapshot, on purpose: the report is built from numbers and
|
||||
// from the three frozen vocabularies, so there is no path by which request content could reach the
|
||||
// terminal even if the counters were compromised. Anything outside the enumerated vocabularies is
|
||||
// not printed — an unknown failure class is visible as a missing row, never as a new label.
|
||||
import { FAILURE_CLASSES, OUTCOMES } from './model-metrics.js';
|
||||
|
||||
const LABEL_WIDTH = 22;
|
||||
|
||||
function row(label, value) {
|
||||
return ` ${String(label).padEnd(LABEL_WIDTH)}${value}`;
|
||||
}
|
||||
|
||||
function duration(value) {
|
||||
if (!Number.isFinite(value) || value < 0) return 'n/a';
|
||||
if (value < 1_000) return `${Math.round(value)}ms`;
|
||||
const seconds = value / 1_000;
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ${Math.round(seconds % 60)}s`;
|
||||
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
}
|
||||
|
||||
function count(value) {
|
||||
return Number.isFinite(value) ? String(value) : '0';
|
||||
}
|
||||
|
||||
function readinessLine(model) {
|
||||
const readiness = model?.readiness ?? {};
|
||||
const phase = typeof readiness.phase === 'string' && readiness.phase ? readiness.phase : 'checking';
|
||||
return readiness.coldLoadMs === null || readiness.coldLoadMs === undefined
|
||||
? phase
|
||||
: `${phase} · cold load ${duration(readiness.coldLoadMs)}`;
|
||||
}
|
||||
|
||||
function abstentionLine(model) {
|
||||
const rate = model?.abstentionRate;
|
||||
const answered = (model?.outcomes?.suggestion ?? 0) + (model?.outcomes?.abstained ?? 0);
|
||||
if (rate === null || rate === undefined || answered === 0) return 'no answers yet';
|
||||
return `${(rate * 100).toFixed(1)}% of ${answered} answer${answered === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
export function formatOperatorReport(snapshot) {
|
||||
if (!snapshot || typeof snapshot !== 'object') throw new Error('a metrics snapshot is required');
|
||||
const app = snapshot.app ?? {};
|
||||
const queue = snapshot.queue ?? {};
|
||||
const model = snapshot.model ?? {};
|
||||
const latency = model.latency ?? {};
|
||||
const outcomes = OUTCOMES.map(name => `${name} ${count(model.outcomes?.[name])}`).join(' · ');
|
||||
|
||||
const lines = [
|
||||
`Timmy model health · ${snapshot.generatedAt ?? 'unknown time'} · schema ${snapshot.schema ?? '?'}`,
|
||||
'',
|
||||
'APP',
|
||||
row('uptime', duration(app.uptimeMs)),
|
||||
row('analyze requests', count(app.analyzeRequests)),
|
||||
row('refused before model', count(app.refusedBeforeModel)),
|
||||
'',
|
||||
'QUEUE',
|
||||
row('in flight', `${count(queue.inFlight)} / ${count(queue.limit)}`),
|
||||
row('accepted', count(queue.accepted)),
|
||||
row('at capacity', queue.atCapacity === true ? 'yes' : 'no'),
|
||||
'',
|
||||
'MODEL',
|
||||
row('readiness', readinessLine(model)),
|
||||
row('outcomes', outcomes),
|
||||
row('abstention', abstentionLine(model)),
|
||||
row('latency', `mean ${duration(latency.meanMs)} · max ${duration(latency.maxMs)} · samples ${count(latency.count)}`),
|
||||
...FAILURE_CLASSES.map(name => row(`failure ${name}`, count(model.failureClasses?.[name]))),
|
||||
];
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
Reference in new issue
Block a user