ti-pote/apps/backend/src/migrations/1744540100000-AddDeviceLogs.ts
ordinarthur 096f772da8 feat: add health telemetry and centralized log system (Phase 2 & 3)
- 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>
2026-04-13 21:11:53 +02:00

41 lines
1.4 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDeviceLogs1744540100000 implements MigrationInterface {
name = 'AddDeviceLogs1744540100000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "device_logs" (
"id" UUID DEFAULT uuid_generate_v4() NOT NULL,
"device_id" UUID NOT NULL,
"level" SMALLINT NOT NULL,
"msg" TEXT NOT NULL,
"logger_name" VARCHAR(64),
"context" JSONB,
"logged_at" TIMESTAMPTZ NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT "PK_device_logs" PRIMARY KEY ("id"),
CONSTRAINT "FK_device_logs_device"
FOREIGN KEY ("device_id") REFERENCES "devices"("id") ON DELETE CASCADE
);
-- Primary query pattern: recent logs for a device
CREATE INDEX "IDX_device_logs_device_logged"
ON "device_logs" ("device_id", "logged_at" DESC);
-- Filter by level (e.g. show only errors)
CREATE INDEX "IDX_device_logs_device_level"
ON "device_logs" ("device_id", "level");
-- Purge old logs
CREATE INDEX "IDX_device_logs_logged"
ON "device_logs" ("logged_at");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "device_logs";`);
}
}