import express, { Request, Response } from 'express';
import { DatabaseManager } from '../database';
import { CacheManager } from '../cache';
import { Logger } from '../utils/logger';
import { HealthCheckError } from '../middleware/errorHandler';
import { config } from '../config';
export interface HealthCheckResult {
status: 'healthy' | 'unhealthy';
timestamp: string;
uptime: number;
version: string;
checks: {
[component: string]: {
status: 'healthy' | 'unhealthy';
responseTime: number;
error?: string;
details?: any;
};
};
}
export class HealthService {
constructor(
private database: DatabaseManager,
private cache: CacheManager
) {}
async checkHealth(): Promise<HealthCheckResult> {
const startTime = Date.now();
const checks: HealthCheckResult['checks'] = {};
let overallStatus: 'healthy' | 'unhealthy' = 'healthy';
// Check database
try {
const dbStart = Date.now();
const dbHealthy = await this.database.ping();
const dbTime = Date.now() - dbStart;
checks.database = {
status: dbHealthy ? 'healthy' : 'unhealthy',
responseTime: dbTime
};
if (!dbHealthy) {
overallStatus = 'unhealthy';
checks.database.error = 'Database connection failed';
}
Logger.health('database', checks.database.status, { responseTime: dbTime });
} catch (error) {
overallStatus = 'unhealthy';
checks.database = {
status: 'unhealthy',
responseTime: 0,
error: error instanceof Error ? error.message : 'Unknown database error'
};
Logger.health('database', 'unhealthy', { error });
}
// Check cache
try {
const cacheStart = Date.now();
const cacheHealthy = await this.cache.ping();
const cacheTime = Date.now() - cacheStart;
checks.cache = {
status: cacheHealthy ? 'healthy' : 'unhealthy',
responseTime: cacheTime
};
if (!cacheHealthy) {
checks.cache.error = 'Cache connection failed';
// Cache is not critical, so don't mark overall as unhealthy
}
Logger.health('cache', checks.cache.status, { responseTime: cacheTime });
} catch (error) {
checks.cache = {
status: 'unhealthy',
responseTime: 0,
error: error instanceof Error ? error.message : 'Unknown cache error'
};
Logger.health('cache', 'unhealthy', { error });
}
// Check memory usage
const memUsage = process.memoryUsage();
const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
const memUsagePercent = Math.round((memUsage.heapUsed / memUsage.heapTotal) * 100);
checks.memory = {
status: memUsagePercent > 90 ? 'unhealthy' : 'healthy',
responseTime: 0,
details: {
used: `${memUsedMB}MB`,
total: `${memTotalMB}MB`,
usage: `${memUsagePercent}%`,
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`
}
};
if (memUsagePercent > 90) {
overallStatus = 'unhealthy';
checks.memory.error = 'High memory usage';
}
// Check disk space (if possible)
try {
const stats = await import('fs').then(fs => fs.promises.statSync('.'));
checks.disk = {
status: 'healthy',
responseTime: 0,
details: {
available: 'N/A' // Would need additional logic to check disk space
}
};
} catch (error) {
checks.disk = {
status: 'healthy', // Don't fail on disk check
responseTime: 0,
error: 'Could not check disk space'
};
}
// Check environment
const requiredEnvVars = ['DATABASE_URL', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'JWT_SECRET'];
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
checks.environment = {
status: missingEnvVars.length === 0 ? 'healthy' : 'unhealthy',
responseTime: 0,
details: {
nodeVersion: process.version,
platform: process.platform,
uptime: process.uptime()
}
};
if (missingEnvVars.length > 0) {
overallStatus = 'unhealthy';
checks.environment.error = `Missing environment variables: ${missingEnvVars.join(', ')}`;
}
return {
status: overallStatus,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: config.mcp.version,
checks
};
}
async checkComponent(component: string): Promise<HealthCheckResult['checks'][string]> {
const startTime = Date.now();
try {
switch (component) {
case 'database':
const dbHealthy = await this.database.ping();
return {
status: dbHealthy ? 'healthy' : 'unhealthy',
responseTime: Date.now() - startTime,
error: dbHealthy ? undefined : 'Database connection failed'
};
case 'cache':
const cacheHealthy = await this.cache.ping();
return {
status: cacheHealthy ? 'healthy' : 'unhealthy',
responseTime: Date.now() - startTime,
error: cacheHealthy ? undefined : 'Cache connection failed'
};
default:
throw new HealthCheckError(component, 'Unknown component');
}
} catch (error) {
return {
status: 'unhealthy',
responseTime: Date.now() - startTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
}
// Create router
export function createHealthRouter(healthService: HealthService): express.Router {
const router = express.Router();
// Main health check endpoint
router.get('/', async (req: Request, res: Response) => {
try {
const health = await healthService.checkHealth();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
} catch (error) {
Logger.error('Health check failed', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Health check failed'
});
}
});
// Readiness probe (for Kubernetes)
router.get('/ready', async (req: Request, res: Response) => {
try {
const health = await healthService.checkHealth();
// Ready if database is healthy (cache is optional)
const ready = health.checks.database?.status === 'healthy';
if (ready) {
res.status(200).json({
status: 'ready',
timestamp: new Date().toISOString()
});
} else {
res.status(503).json({
status: 'not ready',
timestamp: new Date().toISOString(),
reason: 'Database not available'
});
}
} catch (error) {
res.status(503).json({
status: 'not ready',
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Readiness check failed'
});
}
});
// Liveness probe (for Kubernetes)
router.get('/live', (req: Request, res: Response) => {
// Simple liveness check - if we can respond, we're alive
res.status(200).json({
status: 'alive',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Individual component health checks
router.get('/:component', async (req: Request, res: Response) => {
try {
const { component } = req.params;
const result = await healthService.checkComponent(component);
const statusCode = result.status === 'healthy' ? 200 : 503;
res.status(statusCode).json({
component,
...result,
timestamp: new Date().toISOString()
});
} catch (error) {
if (error instanceof HealthCheckError) {
res.status(404).json({
component: req.params.component,
status: 'unknown',
error: error.message,
timestamp: new Date().toISOString()
});
} else {
res.status(503).json({
component: req.params.component,
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Component check failed',
timestamp: new Date().toISOString()
});
}
}
});
return router;
}