diff --git a/management/src/pages/System.jsx b/management/src/pages/System.jsx index abe6363..d4a9c3e 100644 --- a/management/src/pages/System.jsx +++ b/management/src/pages/System.jsx @@ -98,63 +98,119 @@ const System = () => { ) } - const ContainerCard = ({ name, metrics }) => ( -
-

- {name.replace('drone-detection-', '').replace('uamils-', '')} -

- {metrics.error || metrics.status === 'health_check_failed' ? ( -
-
- {metrics.status === 'health_check_failed' ? 'Health Check Failed' : 'Not Available'} -
-
{metrics.error || metrics.message}
-
- ) : metrics.source === 'docker_compose' ? ( -
-
- Status - - {metrics.status} + const ContainerCard = ({ name, metrics }) => { + const getTypeIcon = (type) => { + switch (type) { + case 'database': return '🗄️'; + case 'cache': return '⚡'; + case 'proxy': return '🌐'; + case 'application': return '📱'; + case 'logging': return '📋'; + case 'monitoring': return '📊'; + default: return '📦'; + } + }; + + const getTypeColor = (type) => { + switch (type) { + case 'database': return 'border-l-blue-500'; + case 'cache': return 'border-l-yellow-500'; + case 'proxy': return 'border-l-green-500'; + case 'application': return 'border-l-purple-500'; + case 'logging': return 'border-l-orange-500'; + case 'monitoring': return 'border-l-red-500'; + default: return 'border-l-gray-500'; + } + }; + + return ( +
+
+

+ {getTypeIcon(metrics.type)} + {name.replace('drone-detection-', '').replace('uamils-', '')} +

+ {metrics.type && ( + + {metrics.type} -
-
- Health - {metrics.health} -
-
- Ports - {metrics.ports || 'N/A'} -
-
Source: Docker Compose
+ )}
- ) : ( -
-
- CPU - {metrics.cpu} + + {metrics.error || metrics.status === 'health_check_failed' || metrics.status === 'unreachable' ? ( +
+
+ {metrics.status === 'health_check_failed' ? 'Health Check Failed' : + metrics.status === 'unreachable' ? 'Unreachable' : + metrics.status === 'timeout' ? 'Timeout' : 'Not Available'} +
+
{metrics.error || metrics.message}
-
- Memory - {metrics.memory?.percentage || metrics.memory} + ) : metrics.status === 'responding' ? ( +
+
+ Status + Responding +
+
+ Basic connectivity confirmed +
-
- Network I/O - {metrics.network} -
-
- Disk I/O - {metrics.disk} -
- {metrics.source && ( + ) : metrics.source === 'docker_compose' || metrics.source === 'process_list' ? ( +
+
+ Status + + {metrics.status} + +
+ {metrics.health && ( +
+ Health + {metrics.health} +
+ )} + {metrics.ports && ( +
+ Ports + {metrics.ports} +
+ )}
Source: {metrics.source.replace('_', ' ').toUpperCase()}
- )} -
- )} -
- ) +
+ ) : ( +
+
+ CPU + {metrics.cpu} +
+
+ Memory + {metrics.memory?.percentage || metrics.memory} +
+
+ Network I/O + {metrics.network} +
+
+ Disk I/O + {metrics.disk} +
+ {metrics.source && ( +
+ Source: {metrics.source.replace('_', ' ').toUpperCase()} +
+ )} +
+ )} +
+ ); + }; const SSLCard = ({ domain, ssl }) => (
@@ -336,10 +392,38 @@ const System = () => {
{systemInfo.containers.message}
) : ( -
- {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 }; } }