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
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:
54
src/mcpd/tests/error-log-buffer.test.ts
Normal file
54
src/mcpd/tests/error-log-buffer.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ErrorLogBuffer } from '../src/services/error-log-buffer.js';
|
||||
|
||||
const line = (o: Record<string, unknown>): string => JSON.stringify(o) + '\n';
|
||||
|
||||
describe('ErrorLogBuffer', () => {
|
||||
it('keeps error/fatal records and drops info/warn', () => {
|
||||
const b = new ErrorLogBuffer();
|
||||
b.recordLine(line({ level: 30, time: 1, msg: 'info' }));
|
||||
b.recordLine(line({ level: 40, time: 2, msg: 'warn' }));
|
||||
b.recordLine(line({ level: 50, time: 3, msg: 'an error' }));
|
||||
b.recordLine(line({ level: 60, time: 4, msg: 'fatal', kind: 'BACKEND_TOKEN_DEAD' }));
|
||||
const recent = b.recent();
|
||||
expect(recent.map((e) => e.msg)).toEqual(['fatal', 'an error']); // most-recent first
|
||||
expect(recent[0]?.kind).toBe('BACKEND_TOKEN_DEAD');
|
||||
});
|
||||
|
||||
it('flattens an err object to its message', () => {
|
||||
const b = new ErrorLogBuffer();
|
||||
b.recordLine(line({ level: 50, time: 1, msg: 'boom', err: { type: 'Error', message: 'OpenBao write … 403' } }));
|
||||
expect(b.recent()[0]?.err).toBe('OpenBao write … 403');
|
||||
});
|
||||
|
||||
it('ignores blank and non-JSON lines', () => {
|
||||
const b = new ErrorLogBuffer();
|
||||
b.recordLine('');
|
||||
b.recordLine('not json at all');
|
||||
b.recordLine(' ');
|
||||
expect(b.recent()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('caps at capacity (ring buffer)', () => {
|
||||
const b = new ErrorLogBuffer(3);
|
||||
for (let i = 1; i <= 5; i++) b.recordLine(line({ level: 50, time: i, msg: `e${i}` }));
|
||||
const recent = b.recent(10);
|
||||
expect(recent.map((e) => e.msg)).toEqual(['e5', 'e4', 'e3']); // oldest two evicted
|
||||
});
|
||||
|
||||
it('respects the limit argument', () => {
|
||||
const b = new ErrorLogBuffer();
|
||||
for (let i = 1; i <= 10; i++) b.recordLine(line({ level: 50, time: i }));
|
||||
expect(b.recent(2)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles multiple records in one chunk via the stream', (ctx) => new Promise<void>((resolve) => {
|
||||
const b = new ErrorLogBuffer();
|
||||
const s = b.stream();
|
||||
s.write(line({ level: 50, time: 1, msg: 'a' }) + line({ level: 30, time: 2 }) + line({ level: 60, time: 3, msg: 'b' }));
|
||||
s.end(() => {
|
||||
expect(b.recent().map((e) => e.msg)).toEqual(['b', 'a']);
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
});
|
||||
Reference in New Issue
Block a user