feat(errors): mcpd error-log ring buffer + 'mcpctl errors'
Some checks failed
CI/CD / lint (pull_request) Successful in 1m1s
CI/CD / typecheck (pull_request) Successful in 1m2s
CI/CD / test (pull_request) Successful in 1m21s
CI/CD / smoke (pull_request) Failing after 1m50s
CI/CD / build (pull_request) Successful in 5m12s
CI/CD / publish (pull_request) Has been skipped

Operators can now see recent mcpd error/fatal logs without kubectl:
- mcpd tees level>=error pino records into an in-memory ring buffer
  (src/mcpd/src/services/error-log-buffer.ts; wired via pino.multistream in
  server.ts so stdout logging is unchanged). Captures structured errors incl.
  fatal kinds like BACKEND_TOKEN_DEAD.
- GET /api/v1/logs/errors?limit=N (RBAC: 'logs' operation).
- CLI: 'mcpctl errors [-n N]' renders TIME/LEVEL/DETAIL, most-recent-first.

Buffer unit tests (6); full suite 2223 passing. Needs a deploy to go live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-06-16 23:25:55 +01:00
parent 9f4b8c9149
commit 25da5a3a22
11 changed files with 239 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
import { Command } from 'commander';
import type { ApiClient } from '../api-client.js';
export interface ErrorsCommandDeps {
client: ApiClient;
log: (...args: string[]) => void;
}
interface ErrorEntry {
time: number;
level: number;
msg?: string;
kind?: string;
reqId?: string;
err?: string;
}
/** pino numeric level → short label. */
function levelLabel(level: number): string {
if (level >= 60) return 'FATAL';
if (level >= 50) return 'ERROR';
return String(level);
}
function fmtTime(ms: number): string {
if (!ms) return '-';
// Local HH:MM:SS — enough to correlate with "it broke just now".
const d = new Date(ms);
const p = (n: number): string => String(n).padStart(2, '0');
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
export function createErrorsCommand(deps?: Partial<ErrorsCommandDeps>): Command {
const log = deps?.log ?? ((...args: string[]): void => { console.log(...args); });
return new Command('errors')
.description('Show recent mcpd error/fatal log entries')
.option('-n, --limit <count>', 'max entries to show (default 50)', '50')
.action(async (opts: { limit: string }) => {
const client = deps?.client;
if (!client) throw new Error('errors: no API client configured');
const limit = Number(opts.limit) > 0 ? Number(opts.limit) : 50;
const res = await client.get<{ errors: ErrorEntry[] }>(`/api/v1/logs/errors?limit=${limit}`);
const errors = res.errors ?? [];
if (errors.length === 0) {
log('No recent mcpd errors. 🎉');
return;
}
log(`TIME LEVEL DETAIL`);
for (const e of errors) {
const detail = [e.kind, e.msg ?? e.err].filter(Boolean).join(' — ') || '(no message)';
log(`${fmtTime(e.time).padEnd(9)} ${levelLabel(e.level).padEnd(6)} ${detail}`);
}
log(`\n${errors.length} entr${errors.length === 1 ? 'y' : 'ies'} (most recent first; in-memory, cleared on mcpd restart)`);
});
}

View File

@@ -26,6 +26,7 @@ import { createRotateCommand } from './commands/rotate.js';
import { createReviewCommand } from './commands/review.js';
import { createSkillsCommand } from './commands/skills.js';
import { createPasswdCommand } from './commands/passwd.js';
import { createErrorsCommand } from './commands/errors.js';
import { ApiClient, ApiError } from './api-client.js';
import { loadConfig } from './config/index.js';
import { loadCredentials } from './auth/index.js';
@@ -263,6 +264,11 @@ export function createProgram(): Command {
log: (...args) => console.log(...args),
}));
program.addCommand(createErrorsCommand({
client,
log: (...args) => console.log(...args),
}));
program.addCommand(createBackupCommand({
client,
log: (...args) => console.log(...args),