diff --git a/listener/src/index.ts b/listener/src/index.ts index 9023415..23cf91b 100644 --- a/listener/src/index.ts +++ b/listener/src/index.ts @@ -34,16 +34,19 @@ import { EventDeduplicationService } from './services/event-deduplication-servic dotenv.config(); +// Track process startup time for uptime calculation +const PROCESS_START_TIME = Date.now(); + async function main() { const config = loadConfig(); - // Validate all config values before starting any services (#494). - // This throws a descriptive ConfigError listing every problem found. validateConfig(config); let scheduler: NotificationScheduler | null = null; let retryScheduler: RetryScheduler | null = null; let notificationAPI: NotificationAPI | null = null; let healthMonitor: NotificationHealthMonitor | null = null; + let subscriber: EventSubscriber | null = null; + let templateService: NotificationTemplateService | null = null; let legacyTemplateService: TemplateService | null = null; let cleanupService: CleanupService | null = null; @@ -64,6 +67,12 @@ async function main() { const db = await initializeDatabase(config.databasePath); repository = new ScheduledNotificationRepository(db); + + healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), { + repository, + getUptimeMs: () => Date.now() - PROCESS_START_TIME, + }); + healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), { repository, }); @@ -91,7 +100,6 @@ async function main() { logger.info('Notification metrics runner started successfully'); } - // Archive service: moves old notifications to the archive table. const archiveCfg = loadArchiveConfig(); archiveStore = new ArchiveStore(db); archiveService = new ArchiveService(db, archiveCfg); @@ -109,16 +117,13 @@ async function main() { templateService = new NotificationTemplateService(templateRepository); if (config.scheduler?.enabled) { - repository = new ScheduledNotificationRepository(db); notificationAPI = new NotificationAPI(repository); - // Initialize legacy template service const legacyTemplateRepo = new TemplateRepository(db); legacyTemplateService = new TemplateService(legacyTemplateRepo); logger.info('Template service initialized successfully'); - // Initialize scheduler with Discord service if available let discordService: DiscordNotificationService | null = null; if (config.discord) { discordService = new DiscordNotificationService(config.discord); @@ -163,6 +168,7 @@ async function main() { healthMonitor.start(); } + subscriber = new EventSubscriber(config, deduplicationService); const subscriber = new EventSubscriber(config, deduplicationService ?? undefined); await subscriber.start(); @@ -207,8 +213,11 @@ async function main() { await retryScheduler.stop(); } + if (subscriber) { await subscriber.stop(); - eventsServer.close(); + } + + eventsServer.close(); logger.info('Graceful shutdown completed successfully', { signal }); process.exit(0); @@ -240,4 +249,4 @@ main().catch((err) => { logger.error('Error starting service', { error: err }); } process.exit(1); -}); +}); \ No newline at end of file diff --git a/listener/src/services/notification-health-monitor.test.ts b/listener/src/services/notification-health-monitor.test.ts new file mode 100644 index 0000000..648156f --- /dev/null +++ b/listener/src/services/notification-health-monitor.test.ts @@ -0,0 +1,22 @@ +import { NotificationHealthMonitor } from './notification-health-monitor'; + +describe('NotificationHealthMonitor', () => { + it('should include uptimeMs in the health report', () => { + const mockNow = () => 1000; + const mockGetUptimeMs = () => 5000; + + const monitor = new NotificationHealthMonitor(null, null, { + now: mockNow, + getUptimeMs: mockGetUptimeMs, + }); + + monitor.start(); + const report = monitor.getLastReport(); + + expect(report).not.toBeNull(); + expect(report!.uptimeMs).toBe(5000); + expect(report!.status).toBe('healthy'); + + monitor.stop(); + }); +}); diff --git a/listener/src/services/notification-health-monitor.ts b/listener/src/services/notification-health-monitor.ts index 2dd1106..2771d21 100644 --- a/listener/src/services/notification-health-monitor.ts +++ b/listener/src/services/notification-health-monitor.ts @@ -52,6 +52,8 @@ export interface HealthReport { queue: QueueHealth; workers: WorkerHealth; registry: RegistryHealth; + /** Process uptime in milliseconds since startup. */ + uptimeMs: number; polling: PollingHealth; } @@ -66,6 +68,8 @@ export interface NotificationHealthMonitorOptions { now?: () => number; /** Optional repository used to surface DLQ depth in the health report. */ repository?: ScheduledNotificationRepository | null; + /** Function to calculate uptime in milliseconds. */ + getUptimeMs?: () => number; } /** @@ -80,6 +84,7 @@ export class NotificationHealthMonitor { private readonly stallThresholdCycles: number; private readonly maxProcessingDelayMs: number; private readonly now: () => number; + private readonly getUptimeMs: () => number; private queue: EventProcessingQueue | null; private workerManager: WorkerManager | null; @@ -105,6 +110,7 @@ export class NotificationHealthMonitor { this.stallThresholdCycles = options.stallThresholdCycles ?? 3; this.maxProcessingDelayMs = options.maxProcessingDelayMs ?? 60_000; this.now = options.now ?? Date.now; + this.getUptimeMs = options.getUptimeMs ?? (() => 0); } start(): void { @@ -151,6 +157,7 @@ export class NotificationHealthMonitor { queue: queueHealth, workers: workerHealth, registry: registryHealth, + uptimeMs: this.getUptimeMs(), polling: pollingHealth, }; @@ -261,4 +268,4 @@ export class NotificationHealthMonitor { if (statuses.includes('degraded')) return 'degraded'; return 'healthy'; } -} +} \ No newline at end of file