From 03156fe1bd615adf1227048b5a515ec2de2d2662 Mon Sep 17 00:00:00 2001 From: Akargi Date: Wed, 16 Sep 2026 07:43:18 +0100 Subject: [PATCH] Add real app health check --- src/app.controller.ts | 7 +++++- src/app.service.spec.ts | 35 +++++++++++++++++++++++--- src/app.service.ts | 56 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index d9b4a5b..9ea4739 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -19,7 +19,12 @@ export class AppController { } @Get('health') - getHealth(): { status: string; timestamp: string } { + async getHealth(): Promise<{ + status: string; + timestamp: string; + database: { status: 'up' | 'down'; details?: string }; + redis: { status: 'up' | 'down'; details?: string }; + }> { return this.appService.getHealth(); } } diff --git a/src/app.service.spec.ts b/src/app.service.spec.ts index 4d3449c..0986a31 100644 --- a/src/app.service.spec.ts +++ b/src/app.service.spec.ts @@ -4,7 +4,14 @@ describe('AppService', () => { let service: AppService; beforeEach(() => { - service = new AppService(); + service = new AppService( + { + query: jest.fn().mockResolvedValue([{ '?column?': 1 }]), + } as any, + { + ping: jest.fn().mockResolvedValue('PONG'), + } as any, + ); }); it('reports service info', () => { @@ -13,9 +20,29 @@ describe('AppService', () => { expect(info.name).toContain('InterChangableTrade'); }); - it('reports health with a timestamp', () => { - const health = service.getHealth(); - expect(health.status).toBe('healthy'); + it('reports app health with live dependency checks', async () => { + const health = await service.getHealth(); + + expect(health.status).toBe('ok'); + expect(health.database).toEqual({ status: 'up' }); + expect(health.redis).toEqual({ status: 'up' }); expect(() => new Date(health.timestamp)).not.toThrow(); }); + + it('marks app health as degraded when dependencies fail', async () => { + const failingService = new AppService( + { + query: jest.fn().mockRejectedValue(new Error('db down')), + } as any, + { + ping: jest.fn().mockRejectedValue(new Error('redis down')), + } as any, + ); + + const health = await failingService.getHealth(); + + expect(health.status).toBe('degraded'); + expect(health.database.status).toBe('down'); + expect(health.redis.status).toBe('down'); + }); }); diff --git a/src/app.service.ts b/src/app.service.ts index d18fcf9..4806abf 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,7 +1,23 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import Redis from 'ioredis'; +import { REDIS_CLIENT } from './redis/redis.module'; + +export type AppHealthStatus = 'ok' | 'degraded'; + +export type DependencyHealth = { + status: 'up' | 'down'; + details?: string; +}; @Injectable() export class AppService { + constructor( + @InjectDataSource() private readonly dataSource: DataSource, + @Inject(REDIS_CLIENT) private readonly redisClient: Redis, + ) {} + getInfo(): { name: string; status: string } { return { name: 'InterChangableTrade-Core', @@ -9,10 +25,42 @@ export class AppService { }; } - getHealth(): { status: string; timestamp: string } { + async getHealth(): Promise<{ + status: AppHealthStatus; + timestamp: string; + database: DependencyHealth; + redis: DependencyHealth; + }> { + const timestamp = new Date().toISOString(); + + let databaseStatus: DependencyHealth = { status: 'down' }; + let redisStatus: DependencyHealth = { status: 'down' }; + + try { + await this.dataSource.query('SELECT 1'); + databaseStatus = { status: 'up' }; + } catch (error) { + databaseStatus = { + status: 'down', + details: error instanceof Error ? error.message : 'Database check failed', + }; + } + + try { + await this.redisClient.ping(); + redisStatus = { status: 'up' }; + } catch (error) { + redisStatus = { + status: 'down', + details: error instanceof Error ? error.message : 'Redis check failed', + }; + } + return { - status: 'healthy', - timestamp: new Date().toISOString(), + status: databaseStatus.status === 'up' && redisStatus.status === 'up' ? 'ok' : 'degraded', + timestamp, + database: databaseStatus, + redis: redisStatus, }; } }