Initial commit: APEX Status API v1.0 - Platform health monitoring
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache curl
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install --production
|
||||
COPY server.js ./
|
||||
EXPOSE 3100
|
||||
HEALTHCHECK --interval=30s --timeout=5s CMD curl -sf http://localhost:3100/health || exit 1
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "apex-status-api",
|
||||
"version": "1.0.0",
|
||||
"description": "APEX OS Platform Health Monitor",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "node test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
const express = require('express');
|
||||
const { execSync } = require('child_process');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3100;
|
||||
const START_TIME = Date.now();
|
||||
|
||||
function exec(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { timeout: 10000, encoding: 'utf-8' }).trim();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
try {
|
||||
const containers = JSON.parse(exec('curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json?all=true') || '[]');
|
||||
const running = containers.filter(c => c.State === 'running').length;
|
||||
const total = containers.length;
|
||||
const unhealthy = containers.filter(c => c.Status && c.Status.includes('unhealthy')).length;
|
||||
const status = unhealthy > 0 ? 'degraded' : (running === total ? 'healthy' : 'partial');
|
||||
res.json({
|
||||
status,
|
||||
platform: 'APEX OS',
|
||||
uptime_seconds: Math.floor((Date.now() - START_TIME) / 1000),
|
||||
containers: { running, total, unhealthy },
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ status: 'error', error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/containers', (req, res) => {
|
||||
try {
|
||||
const raw = exec('curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json?all=true');
|
||||
const containers = JSON.parse(raw || '[]').map(c => ({
|
||||
name: (c.Names || ['/unknown'])[0].replace('/', ''),
|
||||
image: c.Image,
|
||||
state: c.State,
|
||||
status: c.Status,
|
||||
created: new Date(c.Created * 1000).toISOString(),
|
||||
ports: (c.Ports || []).map(p => p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}` : `${p.PrivatePort}`).filter(Boolean)
|
||||
}));
|
||||
res.json({ count: containers.length, containers });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/resources', (req, res) => {
|
||||
try {
|
||||
const memRaw = exec('free -m') || '';
|
||||
const memLine = memRaw.split('\n').find(l => l.startsWith('Mem:'));
|
||||
const memParts = memLine ? memLine.split(/\s+/) : [];
|
||||
const diskRaw = exec('df -h /') || '';
|
||||
const diskLine = diskRaw.split('\n')[1] || '';
|
||||
const diskParts = diskLine.split(/\s+/);
|
||||
const loadRaw = exec('cat /proc/loadavg') || '0 0 0';
|
||||
const loadParts = loadRaw.split(' ');
|
||||
res.json({
|
||||
memory: {
|
||||
total_mb: parseInt(memParts[1]) || 0,
|
||||
used_mb: parseInt(memParts[2]) || 0,
|
||||
available_mb: parseInt(memParts[6]) || 0,
|
||||
usage_percent: memParts[1] ? Math.round((memParts[2] / memParts[1]) * 100) : 0
|
||||
},
|
||||
disk: {
|
||||
total: diskParts[1] || 'unknown',
|
||||
used: diskParts[2] || 'unknown',
|
||||
available: diskParts[3] || 'unknown',
|
||||
usage_percent: diskParts[4] || 'unknown'
|
||||
},
|
||||
load: {
|
||||
avg_1m: parseFloat(loadParts[0]) || 0,
|
||||
avg_5m: parseFloat(loadParts[1]) || 0,
|
||||
avg_15m: parseFloat(loadParts[2]) || 0
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/services', async (req, res) => {
|
||||
const services = [
|
||||
{ name: 'litellm', url: 'http://litellm:4000/health', critical: true },
|
||||
{ name: 'letta', url: 'http://letta:8283/api/health', critical: false },
|
||||
{ name: 'n8n', url: 'http://n8n:5678/healthz', critical: true },
|
||||
{ name: 'gitea', url: 'http://gitea:3000/api/v1/version', critical: true },
|
||||
{ name: 'grafana', url: 'http://grafana:3000/api/health', critical: false },
|
||||
{ name: 'ollama', url: 'http://ollama:11434/api/tags', critical: true }
|
||||
];
|
||||
const results = [];
|
||||
for (const svc of services) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const response = exec(`curl -sf -m 5 ${svc.url}`);
|
||||
results.push({ name: svc.name, status: response ? 'healthy' : 'unreachable', response_ms: Date.now() - start, critical: svc.critical });
|
||||
} catch {
|
||||
results.push({ name: svc.name, status: 'unreachable', response_ms: Date.now() - start, critical: svc.critical });
|
||||
}
|
||||
}
|
||||
const healthy = results.filter(r => r.status === 'healthy').length;
|
||||
res.json({
|
||||
overall: healthy === results.length ? 'all_healthy' : 'degraded',
|
||||
healthy_count: healthy,
|
||||
total_count: results.length,
|
||||
services: results,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`APEX Status API running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user