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)), + ); + } }