- {Object.entries(systemInfo.containers).map(([name, metrics]) => (
-
- ))}
+
+ {/* Group containers by type */}
+ {['application', 'database', 'cache', 'proxy', 'logging', 'monitoring', 'unknown'].map(type => {
+ const containersOfType = Object.entries(systemInfo.containers).filter(([name, metrics]) =>
+ metrics.type === type || (type === 'unknown' && !metrics.type)
+ );
+
+ if (containersOfType.length === 0) return null;
+
+ const typeLabels = {
+ application: '📱 Application Services',
+ database: '🗄️ Database Services',
+ cache: '⚡ Cache Services',
+ proxy: '🌐 Proxy & Load Balancers',
+ logging: '📋 Logging Services',
+ monitoring: '📊 Monitoring Services',
+ unknown: '📦 Other Services'
+ };
+
+ return (
+
+
+ {typeLabels[type]} ({containersOfType.length})
+
+
+ {containersOfType.map(([name, metrics]) => (
+
+ ))}
+
+
+ );
+ })}
)}
diff --git a/server/routes/management.js b/server/routes/management.js
index c83a468..a5b730c 100644
--- a/server/routes/management.js
+++ b/server/routes/management.js
@@ -132,76 +132,168 @@ router.get('/system-info', async (req, res) => {
let containerMetrics = {};
const containerEndpoints = [
- { name: 'drone-detection-backend', url: 'http://drone-detection-backend:3000/health/metrics' },
- { name: 'drone-detection-frontend', url: 'http://drone-detection-frontend:80/health/metrics' },
- { name: 'drone-detection-management', url: 'http://drone-detection-management:3001/health/metrics' }
+ // Application containers with custom health endpoints
+ { name: 'drone-detection-backend', url: 'http://drone-detection-backend:3000/health/metrics', type: 'app' },
+ { name: 'drone-detection-frontend', url: 'http://drone-detection-frontend:80/health/metrics', type: 'app' },
+ { name: 'drone-detection-management', url: 'http://drone-detection-management:3001/health/metrics', type: 'app' },
+
+ // Database containers - try standard health endpoints
+ { name: 'postgres', url: 'http://postgres:5432', type: 'database' },
+ { name: 'redis', url: 'http://redis:6379', type: 'cache' },
+
+ // Infrastructure containers
+ { name: 'nginx', url: 'http://nginx:80/nginx_status', type: 'proxy' },
+ { name: 'nginx-proxy-manager', url: 'http://nginx-proxy-manager:81/api/health', type: 'proxy' }
];
// Try internal container health endpoints first
try {
- const fetch = require('node-fetch');
+ const https = require('https');
+ const http = require('http');
+
const healthChecks = await Promise.allSettled(
- containerEndpoints.map(async ({ name, url }) => {
- const response = await fetch(url, { timeout: 3000 });
- const metrics = await response.json();
- return { name, metrics };
+ containerEndpoints.map(async ({ name, url, type }) => {
+ return new Promise((resolve, reject) => {
+ const urlObj = new URL(url);
+ const client = urlObj.protocol === 'https:' ? https : http;
+
+ const req = client.request({
+ hostname: urlObj.hostname,
+ port: urlObj.port,
+ path: urlObj.pathname,
+ method: 'GET',
+ timeout: 3000
+ }, (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ try {
+ const metrics = res.headers['content-type']?.includes('application/json')
+ ? JSON.parse(data)
+ : { status: 'healthy', raw: data };
+ resolve({ name, metrics: { ...metrics, type, source: 'health_endpoint' } });
+ } catch (e) {
+ resolve({ name, metrics: { status: 'responding', type, source: 'basic_check', data: data.substring(0, 100) } });
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ resolve({ name, metrics: { status: 'unreachable', type, error: error.message, source: 'health_check_failed' } });
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ resolve({ name, metrics: { status: 'timeout', type, source: 'health_check_failed' } });
+ });
+
+ req.end();
+ });
})
);
- healthChecks.forEach((result, index) => {
- const containerName = containerEndpoints[index].name;
+ healthChecks.forEach((result) => {
if (result.status === 'fulfilled') {
- containerMetrics[containerName] = result.value.metrics;
- } else {
- containerMetrics[containerName] = {
- status: 'health_check_failed',
- error: result.reason?.message || 'Health endpoint unavailable'
- };
+ containerMetrics[result.value.name] = result.value.metrics;
}
});
+
} catch (healthError) {
console.log('Container health checks failed, trying Docker stats...');
+ }
+
+ // Fallback to Docker stats for ALL containers (not just our apps)
+ try {
+ const { stdout } = await execAsync('docker stats --no-stream --format "table {{.Container}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}\\t{{.NetIO}}\\t{{.BlockIO}}"');
+ const lines = stdout.trim().split('\n').slice(1);
- // Fallback to Docker stats if health endpoints fail
- try {
- const { stdout } = await execAsync('docker stats --no-stream --format "table {{.Container}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}\\t{{.NetIO}}\\t{{.BlockIO}}"');
- const lines = stdout.trim().split('\n').slice(1);
-
- containerMetrics = lines.reduce((acc, line) => {
- const [container, cpu, memUsage, memPerc, netIO, blockIO] = line.split('\t');
- if (container.includes('drone-detection') || container.includes('uamils')) {
- acc[container] = {
+ lines.forEach(line => {
+ const [container, cpu, memUsage, memPerc, netIO, blockIO] = line.split('\t');
+ if (container && cpu) {
+ // Determine container type
+ let type = 'unknown';
+ if (container.includes('postgres') || container.includes('mysql') || container.includes('mongo')) type = 'database';
+ else if (container.includes('redis') || container.includes('memcached')) type = 'cache';
+ else if (container.includes('nginx') || container.includes('proxy') || container.includes('traefik')) type = 'proxy';
+ else if (container.includes('drone-detection') || container.includes('uamils')) type = 'application';
+ else if (container.includes('elasticsearch') || container.includes('kibana') || container.includes('logstash')) type = 'logging';
+ else if (container.includes('prometheus') || container.includes('grafana')) type = 'monitoring';
+
+ // If we don't have health endpoint data, use docker stats
+ if (!containerMetrics[container]) {
+ containerMetrics[container] = {
cpu: cpu,
memory: { usage: memUsage, percentage: memPerc },
network: netIO,
disk: blockIO,
+ type: type,
source: 'docker_stats'
};
+ } else {
+ // Enhance existing health data with docker stats
+ containerMetrics[container] = {
+ ...containerMetrics[container],
+ cpu: cpu,
+ memory: { usage: memUsage, percentage: memPerc },
+ network: netIO,
+ disk: blockIO
+ };
}
- return acc;
- }, {});
- } catch (dockerError) {
- // Try container inspection via docker compose
+ }
+ });
+ } catch (dockerError) {
+ console.log('Docker stats failed, trying docker compose...');
+
+ // Try container inspection via docker compose
+ try {
+ const { stdout: composeStatus } = await execAsync('docker-compose ps --format json');
+ const containers = JSON.parse(`[${composeStatus.split('\n').filter(line => line.trim()).join(',')}]`);
+
+ containers.forEach(container => {
+ if (container.Name && !containerMetrics[container.Name]) {
+ let type = 'unknown';
+ const name = container.Name.toLowerCase();
+ if (name.includes('postgres') || name.includes('mysql') || name.includes('mongo')) type = 'database';
+ else if (name.includes('redis') || name.includes('memcached')) type = 'cache';
+ else if (name.includes('nginx') || name.includes('proxy')) type = 'proxy';
+ else if (name.includes('drone-detection') || name.includes('uamils')) type = 'application';
+
+ containerMetrics[container.Name] = {
+ status: container.State,
+ health: container.Health || 'unknown',
+ ports: container.Ports,
+ type: type,
+ source: 'docker_compose'
+ };
+ }
+ });
+ } catch (composeError) {
+ // Final fallback - try to detect containers via process list
try {
- const { stdout: composeStatus } = await execAsync('docker-compose ps --format json');
- const containers = JSON.parse(`[${composeStatus.split('\n').filter(line => line.trim()).join(',')}]`);
+ const { stdout: processes } = await execAsync('ps aux | grep -E "(postgres|redis|nginx|docker)" | grep -v grep');
+ const processLines = processes.split('\n').filter(line => line.trim());
- containerMetrics = containers.reduce((acc, container) => {
- if (container.Name && (container.Name.includes('drone-detection') || container.Name.includes('uamils'))) {
- acc[container.Name] = {
- status: container.State,
- health: container.Health || 'unknown',
- ports: container.Ports,
- source: 'docker_compose'
- };
- }
- return acc;
- }, {});
- } catch (composeError) {
+ const detectedServices = {};
+ processLines.forEach(line => {
+ if (line.includes('postgres')) detectedServices['postgres-process'] = { status: 'running', type: 'database', source: 'process_list' };
+ if (line.includes('redis')) detectedServices['redis-process'] = { status: 'running', type: 'cache', source: 'process_list' };
+ if (line.includes('nginx')) detectedServices['nginx-process'] = { status: 'running', type: 'proxy', source: 'process_list' };
+ });
+
+ if (Object.keys(detectedServices).length > 0) {
+ containerMetrics = { ...containerMetrics, ...detectedServices };
+ } else {
+ containerMetrics = {
+ error: 'All container monitoring methods failed',
+ attempts: ['health_endpoints', 'docker_stats', 'docker_compose', 'process_list'],
+ lastError: composeError.message
+ };
+ }
+ } catch (processError) {
containerMetrics = {
error: 'All container monitoring methods failed',
- attempts: ['health_endpoints', 'docker_stats', 'docker_compose'],
- lastError: composeError.message
+ attempts: ['health_endpoints', 'docker_stats', 'docker_compose', 'process_list'],
+ lastError: processError.message
};
}
}