diff --git a/docs/health-endpoints.md b/docs/health-endpoints.md index c105817..0a05dd9 100644 --- a/docs/health-endpoints.md +++ b/docs/health-endpoints.md @@ -70,3 +70,13 @@ Nested grouping follows the same convention: - `database`: `status`, `responseTime` (when connected) - `syncing`: `status`, `latestIndexedLedger`, `observedHeadLedger`, `syncLagLedgers` - `services`: ordered as `API Server`, `Database`, `Chain Sync` +- `timeouts`: `database_timeout_ms`, `cache_timeout_ms` + +### Detailed health fields + +The `/api/v1/health/detailed` response also includes an explicit `timeouts` object with public-safe dependency timeout values: + +- `database_timeout_ms` (number) — configured database query timeout in milliseconds. +- `cache_timeout_ms` (number) — configured public cache timeout in milliseconds. + +These values are intentionally numeric-only and do not include connection strings, hostnames, credentials, or other internal topology details. diff --git a/src/modules/health/health.controllers.integration.test.ts b/src/modules/health/health.controllers.integration.test.ts index 74fdb0b..5998055 100644 --- a/src/modules/health/health.controllers.integration.test.ts +++ b/src/modules/health/health.controllers.integration.test.ts @@ -6,6 +6,7 @@ jest.mock('../../config', () => ({ MODE: 'production', PORT: 3000, INDEXER_HEARTBEAT_STALE_THRESHOLD_MS: 300000, + DB_QUERY_TIMEOUT_MS: 5000, }, appConfig: { allowedOrigins: [], @@ -164,4 +165,19 @@ describe('healthCheck() — simulated database failure in production mode', () = expect(dbService).toBeDefined(); expect(dbService.status).toBe('unhealthy'); }); + + it('includes public-safe timeout metadata in detailed health output', async () => { + queryRawMock.mockResolvedValue([{ '?column?': 1 }]); + + const res = mockResponse(); + await healthCheck(mockRequest(), res); + + expect(res.body).toHaveProperty('timeouts'); + expect(res.body.timeouts).toEqual({ + database_timeout_ms: 5000, + cache_timeout_ms: 300000, + }); + expect(typeof res.body.timeouts.database_timeout_ms).toBe('number'); + expect(typeof res.body.timeouts.cache_timeout_ms).toBe('number'); + }); }); diff --git a/src/modules/health/health.controllers.test.ts b/src/modules/health/health.controllers.test.ts index 3a3a5df..048c65e 100644 --- a/src/modules/health/health.controllers.test.ts +++ b/src/modules/health/health.controllers.test.ts @@ -7,6 +7,7 @@ jest.mock('../../config', () => ({ MODE: 'test', PORT: 3000, INDEXER_HEARTBEAT_STALE_THRESHOLD_MS: 300000, + DB_QUERY_TIMEOUT_MS: 5000, }, appConfig: { allowedOrigins: [], @@ -24,6 +25,7 @@ jest.mock('../../utils/indexer-cursor-staleness.utils', () => ({ })); import { + healthCheck, indexerHeartbeatCheck, recordIndexerHeartbeat, readinessCheck, @@ -31,6 +33,7 @@ import { import { indexerHeartbeat } from '../../utils/heartbeat.service'; import { checkIndexerCursorStalenessFromStore } from '../../utils/indexer-cursor-staleness.utils'; import { prisma } from '../../utils/prisma.utils'; +import { PUBLIC_ENDPOINT_CACHE_SECONDS } from '../../constants/public-endpoint-cache.constants'; const checkCursorStalenessMock = checkIndexerCursorStalenessFromStore as jest.MockedFunction< @@ -211,4 +214,26 @@ describe('Readiness Controller', () => { expect(typeof res.body.latencyMs).toBe('number'); }); }); + + describe('healthCheck()', () => { + beforeEach(() => { + queryRawMock.mockReset(); + }); + + it('includes sanitized dependency timeout metadata in detailed health output', async () => { + queryRawMock.mockResolvedValue([{ '?column?': 1 }]); + const res = mockResponse(); + + await healthCheck(mockRequest(), res); + + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty('timeouts'); + expect(res.body.timeouts).toEqual({ + database_timeout_ms: 5000, + cache_timeout_ms: PUBLIC_ENDPOINT_CACHE_SECONDS.short * 1000, + }); + expect(typeof res.body.timeouts.database_timeout_ms).toBe('number'); + expect(typeof res.body.timeouts.cache_timeout_ms).toBe('number'); + }); + }); }); diff --git a/src/modules/health/health.controllers.ts b/src/modules/health/health.controllers.ts index 361e620..f1de289 100644 --- a/src/modules/health/health.controllers.ts +++ b/src/modules/health/health.controllers.ts @@ -42,6 +42,11 @@ interface ReadinessCheck { error?: string; } +interface HealthTimeouts { + database_timeout_ms: number; + cache_timeout_ms: number; +} + interface HealthStatus { success: boolean; message: string; @@ -57,6 +62,7 @@ interface HealthStatus { platform: string; nodeVersion: string; }; + timeouts: HealthTimeouts; database?: { status: 'connected' | 'disconnected'; responseTime?: number; @@ -98,6 +104,11 @@ export const healthCheck = async (_: Request, res: Response): Promise => { const syncStatus = await getChainSyncStatus(); + const healthTimeouts: HealthTimeouts = { + database_timeout_ms: envConfig.DB_QUERY_TIMEOUT_MS, + cache_timeout_ms: PUBLIC_ENDPOINT_CACHE_SECONDS.short * 1000, + }; + const healthData: HealthStatus = { success: true, message: 'Access Layer server is running', @@ -117,6 +128,7 @@ export const healthCheck = async (_: Request, res: Response): Promise => { platform: process.platform, nodeVersion: process.version, }, + timeouts: healthTimeouts, database: dbStatus, syncing: syncStatus || undefined, services: [