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,88 @@
|
||||
#!/usr/bin/env node
|
||||
// Operator report for the privacy-safe model counters (issue #19).
|
||||
//
|
||||
// It reads one snapshot from a running Timmy server and prints it. The token is a credential for the
|
||||
// operator surface, so it is read from the environment or argv and never echoed — not in errors, not
|
||||
// in the report, not in a stack trace.
|
||||
import { formatOperatorReport } from '../src/model-metrics-report.js';
|
||||
|
||||
const USAGE = `Usage: node scripts/model_metrics_report.mjs [options]
|
||||
|
||||
--url <origin> base URL of the Timmy server (default http://127.0.0.1:4173)
|
||||
--token <token> operator token (default: $TIMMY_METRICS_TOKEN)
|
||||
--json print the raw snapshot instead of the text report
|
||||
--timeout-ms <ms> request deadline (default 5000)
|
||||
--help show this text
|
||||
`;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
url: process.env.TIMMY_METRICS_URL || 'http://127.0.0.1:4173',
|
||||
token: process.env.TIMMY_METRICS_TOKEN || '',
|
||||
json: false,
|
||||
timeoutMs: 5_000,
|
||||
help: false,
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
options.help = true;
|
||||
} else if (arg === '--json') {
|
||||
options.json = true;
|
||||
} else if (arg === '--url') {
|
||||
options.url = argv[++index] ?? '';
|
||||
} else if (arg === '--token') {
|
||||
options.token = argv[++index] ?? '';
|
||||
} else if (arg === '--timeout-ms') {
|
||||
options.timeoutMs = Number(argv[++index]);
|
||||
} else {
|
||||
throw new Error(`unknown option: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function main(argv) {
|
||||
const options = parseArgs(argv);
|
||||
if (options.help) {
|
||||
process.stdout.write(USAGE);
|
||||
return 0;
|
||||
}
|
||||
if (!options.token) {
|
||||
throw new Error('operator token required: pass --token or set TIMMY_METRICS_TOKEN');
|
||||
}
|
||||
const endpoint = new URL('api/metrics', options.url.endsWith('/') ? options.url : `${options.url}/`);
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
headers: { authorization: `Bearer ${options.token}`, accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(Number.isFinite(options.timeoutMs) ? options.timeoutMs : 5_000),
|
||||
});
|
||||
} catch (error) {
|
||||
process.stderr.write(`Could not read ${endpoint.origin}/api/metrics: ${error?.message || 'request failed'}\n`);
|
||||
return 1;
|
||||
}
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// The route is absent rather than guarded: that is a deployment fact, not a credential problem.
|
||||
process.stderr.write('Metrics are disabled on this server: the route only exists when TIMMY_METRICS_TOKEN is set.\n');
|
||||
return 1;
|
||||
}
|
||||
if (response.status === 403) {
|
||||
process.stderr.write('The server rejected the operator token.\n');
|
||||
return 1;
|
||||
}
|
||||
process.stderr.write(`The server answered ${response.status} for /api/metrics.\n`);
|
||||
return 1;
|
||||
}
|
||||
const snapshot = await response.json();
|
||||
process.stdout.write(options.json ? `${JSON.stringify(snapshot, null, 2)}\n` : formatOperatorReport(snapshot));
|
||||
return 0;
|
||||
}
|
||||
|
||||
main(process.argv.slice(2)).then(code => {
|
||||
process.exitCode = code;
|
||||
}).catch(error => {
|
||||
process.stderr.write(`${error?.message || 'report failed'}\n`);
|
||||
process.exitCode = 2;
|
||||
});
|
||||
Reference in new issue
Block a user