feat(gateway): add observability sources, series, and service log RPCs

This commit is contained in:
William Valentin
2026-02-22 20:54:37 -08:00
parent cbc880c12a
commit ca463d5ca2
5 changed files with 1403 additions and 0 deletions
+120
View File
@@ -12,6 +12,7 @@ import { createConfigHandlers, redactConfig } from './config.js';
import { createPairingHandlers } from './pairing.js';
import type { LocalBackendStatus, LocalBackendControlResult } from './localBackends.js';
import type { DockerDependencyStatus, DockerDependencyControlResult } from './dockerDependencies.js';
import type { ObservabilitySource, ObservabilitySeriesSnapshot, ServiceLogSnapshot } from './observability.js';
import { PairingManager } from '../../channels/pairing.js';
import { LaneQueue } from '../lane-queue.js';
import { CanvasStore } from '../canvas-store.js';
@@ -375,6 +376,125 @@ describe('system handlers', () => {
expect(getPath(result.result, 'action')).toBe('restart');
});
it('system.observabilitySources returns empty list when callback is not provided', async () => {
const req: GatewayRequest = { id: 48, method: 'system.observabilitySources' };
const result = await handlers['system.observabilitySources'](req) as GatewayResponse;
expect(getPath(result.result, 'sources')).toEqual([]);
});
it('system.observabilitySources returns source list from callback', async () => {
const getObservabilitySources = vi.fn(async (): Promise<ObservabilitySource[]> => ([
{
id: 'systemd:flynn',
name: 'Flynn daemon',
kind: 'systemd_system',
runtime: 'systemd_system',
status: 'running',
graphCapable: true,
logCapable: true,
},
]));
const handlers = createSystemHandlers({
...deps,
getObservabilitySources,
});
const req: GatewayRequest = { id: 49, method: 'system.observabilitySources' };
const result = await handlers['system.observabilitySources'](req) as GatewayResponse;
expect(getObservabilitySources).toHaveBeenCalledTimes(1);
expect(getPath(result.result, 'sources', '0', 'id')).toBe('systemd:flynn');
});
it('system.observabilitySeries validates sourceIds parameter', async () => {
const handlers = createSystemHandlers({
...deps,
getObservabilitySeries: vi.fn(),
});
const result = await handlers['system.observabilitySeries']({
id: 50,
method: 'system.observabilitySeries',
params: { sourceIds: 'not-an-array' as unknown as string[] },
}) as GatewayError;
expect(result.error.code).toBe(ErrorCode.InvalidRequest);
});
it('system.observabilitySeries forwards query to callback', async () => {
const snapshot: ObservabilitySeriesSnapshot = {
generatedAt: 123,
windowMinutes: 60,
bucketSeconds: 30,
series: [
{
sourceId: 'systemd:flynn',
points: [{ ts: 100, stateCode: 3, healthCode: 2, errorCount: 0, restartCount: 1 }],
},
],
};
const getObservabilitySeries = vi.fn(async () => snapshot);
const handlers = createSystemHandlers({
...deps,
getObservabilitySeries,
});
const req: GatewayRequest = {
id: 51,
method: 'system.observabilitySeries',
params: { windowMinutes: 120, bucketSeconds: 60, sourceIds: ['systemd:flynn'] },
};
const result = await handlers['system.observabilitySeries'](req) as GatewayResponse;
expect(getObservabilitySeries).toHaveBeenCalledWith({
windowMinutes: 120,
bucketSeconds: 60,
sourceIds: ['systemd:flynn'],
});
expect(getPath(result.result, 'series', '0', 'points', '0', 'restartCount')).toBe(1);
});
it('system.serviceLogs validates required sourceId', async () => {
const handlers = createSystemHandlers({
...deps,
getServiceLogs: vi.fn(),
});
const result = await handlers['system.serviceLogs']({
id: 52,
method: 'system.serviceLogs',
params: { lines: 100 },
}) as GatewayError;
expect(result.error.code).toBe(ErrorCode.InvalidRequest);
});
it('system.serviceLogs forwards request to callback', async () => {
const snapshot: ServiceLogSnapshot = {
sourceId: 'docker:whisper',
fetchedAt: 123,
redacted: false,
truncated: false,
lines: [{ ts: 100, level: 'warn', text: 'queue depth high' }],
};
const getServiceLogs = vi.fn(async (): Promise<ServiceLogSnapshot> => snapshot);
const handlers = createSystemHandlers({
...deps,
getServiceLogs,
});
const req: GatewayRequest = {
id: 53,
method: 'system.serviceLogs',
params: { sourceId: 'docker:whisper', lines: 50, sinceSeconds: 600 },
};
const result = await handlers['system.serviceLogs'](req) as GatewayResponse;
expect(getServiceLogs).toHaveBeenCalledWith({
sourceId: 'docker:whisper',
lines: 50,
sinceSeconds: 600,
});
expect(getPath(result.result, 'lines', '0', 'text')).toBe('queue depth high');
});
it('system.presence returns empty result when getPresence is not provided', async () => {
const req: GatewayRequest = { id: 4, method: 'system.presence' };
const result = await handlers['system.presence'](req) as GatewayResponse;