- Robot-client: TelemetryReporter collects system metrics (CPU, RAM, disk, WiFi) and sends them to backend every 60s via WebSocket - Robot-client: LogForwarder buffers Pino logs and flushes them in batches every 5s to the backend via WebSocket - Backend: HealthReport entity + HealthTelemetryService with alert thresholds (CPU >80°C, RAM >90%, disk >90%, load >3.0, heap >85%) - Backend: DeviceLog entity + LogIngestionService with EventEmitter2 for SSE - Backend: REST endpoints GET /devices/:id/health/reports and /alerts - Backend: REST endpoint GET /devices/:id/logs with filtering (level, logger, search) - Backend: SSE endpoint GET /admin/logs/stream for real-time log streaming - Migrations for health_reports and device_logs tables with proper indexes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import pino from 'pino';
|
|
import type { LogForwarder, LogEntry } from '../services/log-forwarder.js';
|
|
|
|
let _logForwarder: LogForwarder | null = null;
|
|
|
|
/**
|
|
* Register a LogForwarder instance.
|
|
* Once registered, all loggers created via `createLogger` will
|
|
* also forward their output to the cloud backend.
|
|
*/
|
|
export function setLogForwarder(forwarder: LogForwarder): void {
|
|
_logForwarder = forwarder;
|
|
}
|
|
|
|
export function createLogger(name: string, level = 'info') {
|
|
const logger = pino({
|
|
name,
|
|
level,
|
|
transport:
|
|
process.env.NODE_ENV !== 'production'
|
|
? { target: 'pino-pretty', options: { colorize: true } }
|
|
: undefined,
|
|
});
|
|
|
|
// Wrap the logger to also forward logs when a forwarder is set.
|
|
// We hook into Pino's internal write by using `onChild` isn't available,
|
|
// so we use a proxy on the logging methods.
|
|
return new Proxy(logger, {
|
|
get(target, prop, receiver) {
|
|
const value = Reflect.get(target, prop, receiver);
|
|
|
|
// Intercept logging methods: trace, debug, info, warn, error, fatal
|
|
if (
|
|
_logForwarder &&
|
|
typeof prop === 'string' &&
|
|
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(prop) &&
|
|
typeof value === 'function'
|
|
) {
|
|
return (...args: unknown[]) => {
|
|
// Call original
|
|
(value as (...a: unknown[]) => void).apply(target, args);
|
|
|
|
// Forward to the cloud
|
|
try {
|
|
const levelNum = target.levels.values[prop] ?? 30;
|
|
let msg = '';
|
|
let extra: Record<string, unknown> = {};
|
|
|
|
if (typeof args[0] === 'string') {
|
|
msg = args[0];
|
|
} else if (typeof args[0] === 'object' && args[0] !== null) {
|
|
extra = args[0] as Record<string, unknown>;
|
|
if (typeof args[1] === 'string') {
|
|
msg = args[1];
|
|
}
|
|
}
|
|
|
|
const entry: LogEntry = {
|
|
level: levelNum,
|
|
time: Date.now(),
|
|
msg,
|
|
name,
|
|
...extra,
|
|
};
|
|
|
|
_logForwarder!.ingest(entry);
|
|
} catch {
|
|
// Never let forwarding break the app
|
|
}
|
|
};
|
|
}
|
|
|
|
return value;
|
|
},
|
|
});
|
|
}
|
|
|
|
export type Logger = pino.Logger;
|