agmission/Development/server/controllers/health.js

317 lines
9.3 KiB
JavaScript

'use strict';
/**
* Simple Health Check Controller
* Provides basic health monitoring for partner integration
*/
const { PartnerSystemUser } = require('../model/partner');
const partnerSyncService = require('../services/partner_sync_service');
const mongoose = require('mongoose');
const { SyncStatus, HealthStatus } = require('../helpers/constants');
const env = require('../helpers/env');
const { getQueueStats, getDLQConnection, closeConnection } = require('../helpers/dlq_queue_setup');
/**
* Check DLQ (Dead Letter Queue) health for multiple queues
* @param {Array<string>} queueNames - Array of queue names to check (defaults to partner_tasks)
*/
async function checkDLQHealth(queueNames = [env.QUEUE_NAME_PARTNER]) {
let connection, channel;
try {
// Connect to RabbitMQ using helper
connection = await getDLQConnection();
channel = await connection.createChannel();
const threshold = env.DLQ_ALERT_THRESHOLD;
const critical = env.DLQ_ALERT_CRITICAL;
// Check all queues
const queues = {};
let overallMessageCount = 0;
let worstStatus = HealthStatus.HEALTHY;
for (const queueName of queueNames) {
const dlqName = `${queueName}_dlq`;
try {
const stats = await getQueueStats(channel, dlqName);
const messageCount = stats.messageCount;
overallMessageCount += messageCount;
// Determine status for this queue
let status = HealthStatus.HEALTHY;
let message = 'Operating normally';
if (messageCount >= critical) {
status = HealthStatus.UNHEALTHY;
message = `Critical: ${messageCount} messages (threshold: ${critical})`;
} else if (messageCount >= threshold) {
status = HealthStatus.DEGRADED;
message = `Warning: ${messageCount} messages (threshold: ${threshold})`;
}
// Track worst status
if (status === HealthStatus.UNHEALTHY) {
worstStatus = HealthStatus.UNHEALTHY;
} else if (status === HealthStatus.DEGRADED && worstStatus === HealthStatus.HEALTHY) {
worstStatus = HealthStatus.DEGRADED;
}
queues[queueName] = {
status,
message,
dlqName,
messageCount,
consumerCount: stats.consumerCount
};
} catch (error) {
queues[queueName] = {
status: HealthStatus.UNHEALTHY,
error: error.message,
message: 'Unable to check DLQ'
};
worstStatus = HealthStatus.UNHEALTHY;
}
}
// Overall summary
let overallMessage = 'All DLQs operating normally';
if (worstStatus === HealthStatus.UNHEALTHY) {
overallMessage = `Critical: ${overallMessageCount} total messages across ${queueNames.length} queue(s)`;
} else if (worstStatus === HealthStatus.DEGRADED) {
overallMessage = `Warning: ${overallMessageCount} total messages across ${queueNames.length} queue(s)`;
}
return {
status: worstStatus,
message: overallMessage,
totalMessages: overallMessageCount,
threshold,
critical,
consumerEnabled: env.DLQ_CONSUMER_ENABLED,
retentionDays: env.DLQ_RETENTION_DAYS,
queues
};
} catch (error) {
return {
status: HealthStatus.UNHEALTHY,
error: error.message,
message: 'Unable to connect to RabbitMQ'
};
} finally {
await closeConnection(connection, channel);
}
}
/**
* Basic health check endpoint
*/
async function healthCheck_get(req, res) {
try {
const health = {
timestamp: new Date(),
overall: HealthStatus.HEALTHY,
components: {}
};
// Check database connectivity
try {
const dbState = mongoose.connection.readyState;
const dbStatus = dbState === 1 ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY;
health.components.database = {
status: dbStatus,
state: dbState,
message: dbState === 1 ? 'Connected' : 'Disconnected'
};
} catch (error) {
health.components.database = {
status: HealthStatus.UNHEALTHY,
error: error.message
};
health.overall = HealthStatus.UNHEALTHY;
}
// Check partner system users
try {
const activeUsers = await PartnerSystemUser.countDocuments({ active: true });
const errorUsers = await PartnerSystemUser.countDocuments({ syncStatus: SyncStatus.ERROR });
const errorRate = activeUsers > 0 ? (errorUsers / activeUsers * 100) : 0;
const status = errorRate > 50 ? HealthStatus.DEGRADED : HealthStatus.HEALTHY;
health.components.partnerUsers = {
status,
activeUsers,
errorUsers,
errorRate: Math.round(errorRate * 100) / 100
};
if (status === HealthStatus.DEGRADED) {
health.overall = HealthStatus.DEGRADED;
}
} catch (error) {
health.components.partnerUsers = {
status: HealthStatus.UNHEALTHY,
error: error.message
};
health.overall = HealthStatus.UNHEALTHY;
}
// Check partner services
try {
const partnerHealth = await partnerSyncService.healthCheck();
health.components.partnerServices = partnerHealth;
if (partnerHealth.overall !== HealthStatus.HEALTHY) {
health.overall = partnerHealth.overall;
}
} catch (error) {
health.components.partnerServices = {
status: HealthStatus.UNHEALTHY,
error: error.message
};
health.overall = HealthStatus.UNHEALTHY;
}
// Check application health
const memoryUsage = process.memoryUsage();
const uptime = process.uptime();
const memoryThreshold = 1024 * 1024 * 1024; // 1GB
health.components.application = {
status: memoryUsage.heapUsed < memoryThreshold ? HealthStatus.HEALTHY : HealthStatus.DEGRADED,
memoryUsedMB: Math.round(memoryUsage.heapUsed / 1024 / 1024),
uptimeHours: Math.round(uptime / 3600 * 100) / 100,
nodeVersion: process.version
};
if (health.components.application.status === HealthStatus.DEGRADED && health.overall === HealthStatus.HEALTHY) {
health.overall = HealthStatus.DEGRADED;
}
// Check DLQ (Dead Letter Queue) health
try {
const dlqHealth = await checkDLQHealth();
health.components.dlq = dlqHealth;
if (dlqHealth.status === HealthStatus.UNHEALTHY) {
health.overall = HealthStatus.UNHEALTHY;
} else if (dlqHealth.status === HealthStatus.DEGRADED && health.overall === HealthStatus.HEALTHY) {
health.overall = HealthStatus.DEGRADED;
}
} catch (error) {
health.components.dlq = {
status: HealthStatus.UNHEALTHY,
error: error.message
};
// Don't fail overall health if DLQ check fails
}
// Set HTTP status code based on health
const statusCode = health.overall === HealthStatus.HEALTHY ? 200 :
health.overall === HealthStatus.DEGRADED ? 200 : 503;
res.status(statusCode).json(health);
} catch (error) {
res.status(503).json({
timestamp: new Date(),
overall: HealthStatus.UNHEALTHY,
error: error.message
});
}
}
/**
* Partner statistics endpoint
*/
async function partnerStats_get(req, res) {
try {
const stats = {
timestamp: new Date(),
partners: {},
summary: {}
};
// Get partner statistics
const partnerStats = await PartnerSystemUser.aggregate([
{
$lookup: {
from: 'users',
localField: 'partner',
foreignField: '_id',
as: 'partner'
}
},
{
$unwind: '$partner'
},
{
$group: {
_id: '$partner.partnerCode',
totalUsers: { $sum: 1 },
activeUsers: {
$sum: { $cond: [{ $eq: ['$active', true] }, 1, 0] }
},
errorUsers: {
$sum: { $cond: [{ $eq: ['$syncStatus', SyncStatus.ERROR] }, 1, 0] }
},
lastSyncUsers: {
$sum: {
$cond: [
{
$gte: [
'$lastSyncAt',
new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
]
},
1,
0
]
}
}
}
}
]);
// Process partner statistics
for (const partnerStat of partnerStats) {
stats.partners[partnerStat._id] = {
totalUsers: partnerStat.totalUsers,
activeUsers: partnerStat.activeUsers,
errorUsers: partnerStat.errorUsers,
lastSyncUsers: partnerStat.lastSyncUsers,
errorRate: partnerStat.activeUsers > 0 ?
Math.round((partnerStat.errorUsers / partnerStat.activeUsers) * 100 * 100) / 100 : 0
};
}
// Calculate summary
stats.summary = {
totalPartners: Object.keys(stats.partners).length,
totalPartnerUsers: partnerStats.reduce((sum, p) => sum + p.totalUsers, 0),
activePartnerUsers: partnerStats.reduce((sum, p) => sum + p.activeUsers, 0),
recentErrors: partnerStats.reduce((sum, p) => sum + p.errorUsers, 0),
availableServices: partnerSyncService.getAvailablePartners()
};
res.json(stats);
} catch (error) {
res.status(500).json({
error: error.message,
timestamp: new Date()
});
}
}
module.exports = {
healthCheck_get,
partnerStats_get
};