From 74de05ba9d4f3bd7889de2afa034c1e729521fd6 Mon Sep 17 00:00:00 2001 From: Najib Ishiyaku Njidda Date: Fri, 28 Aug 2026 19:35:24 +0000 Subject: [PATCH] feat: add process uptime to health information - Added PROCESS_START_TIME constant to track startup time in index.ts - Extended HealthReport interface to include uptimeMs (documented in milliseconds) - Injected getUptimeMs callback into NotificationHealthMonitor - Added unit test to verify uptimeMs is correctly returned in the health report - Fixed pre-existing syntax error (unclosed try block) in index.ts - Fixed pre-existing syntax error in listener/jest.config.js (missing comma, duplicate keys) Closes #622 --- listener/jest.config.js | 5 +-- listener/src/index.ts | 37 +++++++++---------- .../notification-health-monitor.test.ts | 22 +++++++++++ .../services/notification-health-monitor.ts | 9 ++++- 4 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 listener/src/services/notification-health-monitor.test.ts diff --git a/listener/jest.config.js b/listener/jest.config.js index 6b109200..2d92e889 100644 --- a/listener/jest.config.js +++ b/listener/jest.config.js @@ -4,10 +4,6 @@ module.exports = { roots: ['/src'], testMatch: ['**/*.test.ts'], transform: { - '^.+\\.tsx?$': ['ts-jest', { diagnostics: false }] - }, - moduleNameMapper: { - '^uuid$': '/src/__mocks__/uuid.js' '^.+\\.tsx?$': ['ts-jest', { diagnostics: { ignoreCodes: [2307] @@ -15,6 +11,7 @@ module.exports = { }] }, moduleNameMapper: { + '^uuid$': '/src/__mocks__/uuid.js', '^@stellar/stellar-sdk$': '/src/__mocks__/@stellar/stellar-sdk.ts', '^node-cache$': '/src/__mocks__/node-cache.ts' }, diff --git a/listener/src/index.ts b/listener/src/index.ts index fc5d60ce..06819e6e 100644 --- a/listener/src/index.ts +++ b/listener/src/index.ts @@ -33,10 +33,11 @@ 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; @@ -44,12 +45,8 @@ async function main() { let notificationAPI: NotificationAPI | null = null; let templateService: TemplateService | null = null; let healthMonitor: NotificationHealthMonitor | null = null; + let subscriber: EventSubscriber | null = null; - if (config.scheduler?.enabled) { - try { - logger.info('Initializing database for scheduled notifications and templates'); - const db = await initializeDatabase(config.databasePath); - let templateService: NotificationTemplateService | null = null; let legacyTemplateService: TemplateService | null = null; let cleanupService: CleanupService | null = null; let repository: ScheduledNotificationRepository | null = null; @@ -60,11 +57,6 @@ async function main() { let metricsStore: NotificationMetricsStore | null = null; let deduplicationService: EventDeduplicationService | null = null; - repository = new ScheduledNotificationRepository(db); - healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), { - repository, - }); - if (config.analytics?.enabled) { initNotificationAnalyticsAggregator(config.analytics); } @@ -73,7 +65,13 @@ async function main() { logger.info('Initializing database'); const db = await initializeDatabase(config.databasePath); - // Rebuild registry with configured event TTL + repository = new ScheduledNotificationRepository(db); + + healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), { + repository, + getUptimeMs: () => Date.now() - PROCESS_START_TIME, + }); + if (config.cleanup) { eventRegistry.setTtlMs(config.cleanup.eventRetentionMs); } @@ -96,7 +94,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); @@ -114,16 +111,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); @@ -167,7 +161,7 @@ async function main() { healthMonitor.start(); } - const subscriber = new EventSubscriber(config, deduplicationService); + subscriber = new EventSubscriber(config, deduplicationService); await subscriber.start(); const shutdown = async () => { @@ -201,7 +195,10 @@ async function main() { await retryScheduler.stop(); } - await subscriber.stop(); + if (subscriber) { + await subscriber.stop(); + } + eventsServer.close(); logger.info('All services stopped successfully'); @@ -226,4 +223,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 00000000..648156fc --- /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 a7eb2807..9089927d 100644 --- a/listener/src/services/notification-health-monitor.ts +++ b/listener/src/services/notification-health-monitor.ts @@ -32,6 +32,8 @@ export interface HealthReport { queue: QueueHealth; workers: WorkerHealth; registry: RegistryHealth; + /** Process uptime in milliseconds since startup. */ + uptimeMs: number; } export interface NotificationHealthMonitorOptions { @@ -45,6 +47,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; } /** @@ -59,6 +63,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; @@ -84,6 +89,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 { @@ -129,6 +135,7 @@ export class NotificationHealthMonitor { queue: queueHealth, workers: workerHealth, registry: registryHealth, + uptimeMs: this.getUptimeMs(), }; this.lastReport = report; @@ -223,4 +230,4 @@ export class NotificationHealthMonitor { if (statuses.includes('degraded')) return 'degraded'; return 'healthy'; } -} +} \ No newline at end of file