From d401b85a37965f907bddae7afdbf145daab68abb Mon Sep 17 00:00:00 2001 From: Georgy Malkov Date: Tue, 27 Oct 2026 20:45:40 +0300 Subject: [PATCH 1/2] feat: add healthcheck status log suppression --- lib/constants.ts | 1 + lib/core.ts | 1 + lib/dispatcher.ts | 26 +++++++++------- lib/types.ts | 4 +++ tests/dispatcher.test.js | 67 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/lib/constants.ts b/lib/constants.ts index 81824cd..916e9bc 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -19,6 +19,7 @@ export const defaultDispatcherOptions: PDOptions = { healthcheckInterval: 5000, healthcheckTimeout: 700, suppressStatusLogs: false, + suppressHealthcheckStatusLogs: false, beforeTerminate: () => Promise.resolve(), topologyMode: 'primary-replica', }; diff --git a/lib/core.ts b/lib/core.ts index a363b99..523d3ca 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -21,6 +21,7 @@ export interface CoreDBDispatcherOptions { healthcheckInterval?: number; healthcheckTimeout?: number; suppressStatusLogs?: boolean; + suppressHealthcheckStatusLogs?: boolean; beforeTerminate?: () => Promise; topologyMode?: TopologyMode; } diff --git a/lib/dispatcher.ts b/lib/dispatcher.ts index a1b8221..13a5e38 100644 --- a/lib/dispatcher.ts +++ b/lib/dispatcher.ts @@ -229,18 +229,20 @@ export class PGDispatcher { Promise.all(checkups).then(() => { // Connections hold shared current state; this is not an isolated per-cycle result. const status = this.getHealthcheckStatus(); - this.logger.info({ - message: 'Database current status', - data: { - ...(this.isProxyMode ? {topologyMode: this.options.topologyMode} : {}), - connections: this.connections.map((connection) => ({ - host: connection.host, - ...(this.isProxyMode ? {} : {primary: connection.primary}), - healthy: connection.healthy, - latency: connection.latency, - })), - }, - }); + if (!this.options.suppressHealthcheckStatusLogs) { + this.logger.info({ + message: 'Database current status', + data: { + ...(this.isProxyMode ? {topologyMode: this.options.topologyMode} : {}), + connections: this.connections.map((connection) => ({ + host: connection.host, + ...(this.isProxyMode ? {} : {primary: connection.primary}), + healthy: connection.healthy, + latency: connection.latency, + })), + }, + }); + } this.notifyHealthcheck(status); }); }; diff --git a/lib/types.ts b/lib/types.ts index 99becfc..cec05a8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -33,6 +33,10 @@ export interface PDOptions { healthcheckInterval: number; healthcheckTimeout: number; suppressStatusLogs: boolean; + /** + * suppresses only [Database current status] logs, preserving dispatcher errors and other logs + */ + suppressHealthcheckStatusLogs?: boolean; beforeTerminate: () => Promise; topologyMode: TopologyMode; } diff --git a/tests/dispatcher.test.js b/tests/dispatcher.test.js index c90ebe4..ff0ef5a 100644 --- a/tests/dispatcher.test.js +++ b/tests/dispatcher.test.js @@ -147,6 +147,73 @@ describe('PGDispatcher topology modes', () => { }); }); +describe('PGDispatcher healthcheck status logging', () => { + test('logs database status by default', async () => { + const {dispatcher, logger} = createDispatcher([ + createKnex(successfulCheckup({pg_is_in_recovery: false})), + ]); + + await dispatcher.ready(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(defaultDispatcherOptions.suppressHealthcheckStatusLogs).toBe(false); + expect(logger.info).toHaveBeenCalledWith( + 'Database current status', + expect.objectContaining({connections: expect.any(Array)}), + ); + }); + + test.each(['primary-replica', 'proxy'])( + 'suppresses only database status in %s mode and preserves callbacks', + async (topologyMode) => { + const onHealthcheck = jest.fn(); + const {dispatcher, logger} = createDispatcher( + [ + createKnex(successfulCheckup({pg_is_in_recovery: false})), + createKnex(failedCheckup), + ], + {suppressHealthcheckStatusLogs: true, topologyMode}, + onHealthcheck, + ); + + await dispatcher.ready(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(logger.info.mock.calls.map(([message]) => message)).not.toContain( + 'Database current status', + ); + expect(loggedErrorMessages(logger)).toContain('Proxy unavailable'); + expect(onHealthcheck).toHaveBeenCalledWith({ + topologyMode, + connections: [ + expect.objectContaining({host: 'database-0.example', healthy: true}), + expect.objectContaining({host: 'database-1.example', healthy: false}), + ], + }); + + await dispatcher.terminate(); + + expect(logger.info).toHaveBeenCalledWith('Connection successfully terminated', { + host: 'database-0.example', + }); + }, + ); + + test('the existing suppression option suppresses all info and error logs', async () => { + const {dispatcher, logger} = createDispatcher([createKnex(failedCheckup)], { + suppressStatusLogs: true, + suppressHealthcheckStatusLogs: false, + }); + + await dispatcher.ready(); + await new Promise((resolve) => setImmediate(resolve)); + await dispatcher.terminate(); + + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); + describe('PGDispatcher healthcheck callback', () => { test('reports a snapshot for every primary and replica connection', async () => { const onHealthcheck = jest.fn(); From 3b2c16e776459506693dc0ab213bf278ef9a5db3 Mon Sep 17 00:00:00 2001 From: Georgy Malkov Date: Tue, 27 Oct 2026 20:49:02 +0300 Subject: [PATCH 2/2] test: simplify healthcheck log suppression coverage --- tests/dispatcher.test.js | 44 +++------------------------------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/tests/dispatcher.test.js b/tests/dispatcher.test.js index ff0ef5a..eacd0df 100644 --- a/tests/dispatcher.test.js +++ b/tests/dispatcher.test.js @@ -163,54 +163,16 @@ describe('PGDispatcher healthcheck status logging', () => { ); }); - test.each(['primary-replica', 'proxy'])( - 'suppresses only database status in %s mode and preserves callbacks', - async (topologyMode) => { - const onHealthcheck = jest.fn(); - const {dispatcher, logger} = createDispatcher( - [ - createKnex(successfulCheckup({pg_is_in_recovery: false})), - createKnex(failedCheckup), - ], - {suppressHealthcheckStatusLogs: true, topologyMode}, - onHealthcheck, - ); - - await dispatcher.ready(); - await new Promise((resolve) => setImmediate(resolve)); - - expect(logger.info.mock.calls.map(([message]) => message)).not.toContain( - 'Database current status', - ); - expect(loggedErrorMessages(logger)).toContain('Proxy unavailable'); - expect(onHealthcheck).toHaveBeenCalledWith({ - topologyMode, - connections: [ - expect.objectContaining({host: 'database-0.example', healthy: true}), - expect.objectContaining({host: 'database-1.example', healthy: false}), - ], - }); - - await dispatcher.terminate(); - - expect(logger.info).toHaveBeenCalledWith('Connection successfully terminated', { - host: 'database-0.example', - }); - }, - ); - - test('the existing suppression option suppresses all info and error logs', async () => { + test('suppresses database status while preserving healthcheck errors', async () => { const {dispatcher, logger} = createDispatcher([createKnex(failedCheckup)], { - suppressStatusLogs: true, - suppressHealthcheckStatusLogs: false, + suppressHealthcheckStatusLogs: true, }); await dispatcher.ready(); await new Promise((resolve) => setImmediate(resolve)); - await dispatcher.terminate(); expect(logger.info).not.toHaveBeenCalled(); - expect(logger.error).not.toHaveBeenCalled(); + expect(loggedErrorMessages(logger)).toContain('Proxy unavailable'); }); });