From 96ae867288efb70f83d38e8ac91c47364cc5a9b9 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Fri, 17 Apr 2026 08:22:11 +0530 Subject: [PATCH] feat: server log streaming via SSE for desktop log panel - LogStreamService: singleton that extends ConsoleLogger, captures all NestJS log output into an RxJS Subject while preserving stdout - main.ts: uses LogStreamService.instance as app logger - supervisor.controller.ts: new @Sse('logs/stream') endpoint pipes log entries (timestamp, level, context, message) to connected clients Co-Authored-By: Claude Opus 4.6 (1M context) --- src/logging/log-stream.service.ts | 52 +++++++++++++++++++++++++ src/main.ts | 4 +- src/supervisor/supervisor.controller.ts | 11 ++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/logging/log-stream.service.ts diff --git a/src/logging/log-stream.service.ts b/src/logging/log-stream.service.ts new file mode 100644 index 0000000..7a29cb4 --- /dev/null +++ b/src/logging/log-stream.service.ts @@ -0,0 +1,52 @@ +import { ConsoleLogger } from '@nestjs/common'; +import { Subject } from 'rxjs'; + +export type LogEntry = { + timestamp: string; + level: 'log' | 'error' | 'warn' | 'debug' | 'verbose'; + context: string; + message: string; +}; + +// Singleton — created once in main.ts, accessed by the SSE controller +// via LogStreamService.instance. NestJS DI isn't available at bootstrap +// time (the logger is created before the container), so we use a static +// instance instead of @Injectable(). +export class LogStreamService extends ConsoleLogger { + static readonly instance = new LogStreamService(); + readonly logSubject = new Subject(); + + private emit(level: LogEntry['level'], message: unknown, context?: string) { + this.logSubject.next({ + timestamp: new Date().toISOString(), + level, + context: context ?? this.context ?? '', + message: typeof message === 'string' ? message : JSON.stringify(message), + }); + } + + log(message: unknown, context?: string) { + super.log(message, context); + this.emit('log', message, context); + } + + error(message: unknown, stack?: string, context?: string) { + super.error(message, stack, context); + this.emit('error', message, context); + } + + warn(message: unknown, context?: string) { + super.warn(message, context); + this.emit('warn', message, context); + } + + debug(message: unknown, context?: string) { + super.debug(message, context); + this.emit('debug', message, context); + } + + verbose(message: unknown, context?: string) { + super.verbose(message, context); + this.emit('verbose', message, context); + } +} diff --git a/src/main.ts b/src/main.ts index 1f8e6cd..59cd362 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,9 +3,11 @@ import type { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; +import { LogStreamService } from './logging/log-stream.service'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const logger = LogStreamService.instance; + const app = await NestFactory.create(AppModule, { logger }); const config = app.get(ConfigService); app.enableCors({ diff --git a/src/supervisor/supervisor.controller.ts b/src/supervisor/supervisor.controller.ts index 8fdaa7b..2e9d370 100644 --- a/src/supervisor/supervisor.controller.ts +++ b/src/supervisor/supervisor.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Body, Query, Sse, Logger } from '@nestjs/common'; import { Observable, filter, map } from 'rxjs'; import { SupervisorService } from './supervisor.service'; +import { LogStreamService } from '../logging/log-stream.service'; @Controller('api/supervisor') export class SupervisorController { @@ -76,4 +77,14 @@ export class SupervisorController { } as MessageEvent)), ); } + + @Sse('logs/stream') + streamLogs(): Observable { + this.logger.log('[SSE] Log stream opened'); + return LogStreamService.instance.logSubject.pipe( + map(entry => ({ + data: JSON.stringify(entry), + } as MessageEvent)), + ); + } }