commit cc9c903d6d39f52ecc48f93ad0fa6f4fb84dcb41 Author: Engineer Date: Thu Jul 2 05:21:08 2026 +0000 Fix: Use correct health check endpoints for LiteLLM and Letta Root cause: Status API was using wrong health endpoints: - LiteLLM /health requires API key auth (returns 401) -> changed to /health/readiness (no auth) - Letta /api/health returns 404 -> changed to /v1/health/ (correct endpoint) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b47c9d5 --- /dev/null +++ b/Dockerfile @@ -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=10s --retries=3 CMD curl -sf http://localhost:3100/health || exit 1 +CMD ["node", "server.js"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..879e09f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..6650646 --- /dev/null +++ b/server.js @@ -0,0 +1,118 @@ +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/readiness', critical: true }, + { name: 'letta', url: 'http://letta:8283/v1/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(); + 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 + }); + } + 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 v1.1 - connectivity fix applied'); +});