feat(mam-api): aggregate containers from all nodes + proxy logs
This commit is contained in:
parent
a90adb5b52
commit
2f13c8d8b1
1 changed files with 76 additions and 39 deletions
|
|
@ -96,46 +96,83 @@ router.get('/', async (req, res, next) => {
|
|||
|
||||
router.get('/containers', async (req, res, next) => {
|
||||
try {
|
||||
const containers = await dockerRequest('/containers/json?all=true');
|
||||
if (!Array.isArray(containers)) return res.json([]);
|
||||
const out = await Promise.all(containers.map(async c => {
|
||||
const rawName = (c.Names[0] || '').replace(/^\//, '');
|
||||
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
|
||||
const ports = (c.Ports || [])
|
||||
.filter(p => p.PublicPort)
|
||||
.map(p => `${p.PublicPort}→${p.PrivatePort}`)
|
||||
.join(', ');
|
||||
// Live memory usage requires a per-container stats call (the list endpoint
|
||||
// doesn't include it). One extra Docker call each, but the list is small.
|
||||
// memory_stats.usage includes page cache; subtract it to match `docker stats`.
|
||||
let memBytes = null;
|
||||
if (c.State === 'running') {
|
||||
try {
|
||||
const stats = await dockerRequest(`/containers/${c.Id}/stats?stream=false`);
|
||||
const ms = stats && stats.memory_stats;
|
||||
if (ms && typeof ms.usage === 'number') {
|
||||
const cache = (ms.stats && ms.stats.cache) || 0;
|
||||
memBytes = ms.usage - cache;
|
||||
}
|
||||
} catch (_) { memBytes = null; }
|
||||
const nodesRes = await pool.query(
|
||||
`SELECT id, hostname, api_url,
|
||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||
FROM cluster_nodes
|
||||
ORDER BY registered_at ASC`
|
||||
);
|
||||
|
||||
const tasks = nodesRes.rows.map(async node => {
|
||||
const isOnline = Number(node.stale_seconds) < 120;
|
||||
if (!isOnline) return [];
|
||||
|
||||
const localHostname = process.env.NODE_HOSTNAME || os.hostname();
|
||||
const isLocal = node.hostname === localHostname || !node.api_url;
|
||||
|
||||
try {
|
||||
let rawContainers = [];
|
||||
if (isLocal) {
|
||||
rawContainers = await dockerRequest('/containers/json?all=true') || [];
|
||||
} else {
|
||||
const resp = await fetch(`${node.api_url}/containers`, {
|
||||
headers: agentAuthHeaders(),
|
||||
signal: AbortSignal.timeout(4000),
|
||||
});
|
||||
if (resp.ok) rawContainers = await resp.json();
|
||||
}
|
||||
|
||||
if (!Array.isArray(rawContainers)) return [];
|
||||
|
||||
return rawContainers.map(c => {
|
||||
const rawName = (c.Names && c.Names[0] || '').replace(/^\//, '');
|
||||
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
|
||||
return {
|
||||
id: c.Id.slice(0, 12),
|
||||
name,
|
||||
image: (c.Image || '').replace(/^sha256:/, '').slice(0, 40),
|
||||
state: c.State,
|
||||
status: c.Status,
|
||||
uptime: (c.Status || '').replace(/\s*\(.*\)/, '').trim(),
|
||||
healthy: (c.Status || '').includes('healthy'),
|
||||
node_hostname: node.hostname,
|
||||
node_id: node.id,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`[cluster] failed to fetch containers from ${node.hostname}:`, err.message);
|
||||
return [];
|
||||
}
|
||||
return {
|
||||
id: c.Id.slice(0, 12),
|
||||
name,
|
||||
image: (c.Image || '').replace(/^sha256:/, '').slice(0, 40),
|
||||
state: c.State,
|
||||
uptime: (c.Status || '').replace(/\s*\(.*\)/, '').trim(),
|
||||
healthy: (c.Status || '').includes('healthy'),
|
||||
ports,
|
||||
cpu: 0,
|
||||
memBytes,
|
||||
};
|
||||
}));
|
||||
res.json(out);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
const flattened = results.flat();
|
||||
res.json(flattened);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
router.get('/containers/:nodeId/:containerId/logs', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { nodeId, containerId } = req.params;
|
||||
const node = await resolveNode(nodeId);
|
||||
if (!node) return res.status(404).json({ error: 'Node not found' });
|
||||
|
||||
const localHostname = process.env.NODE_HOSTNAME || os.hostname();
|
||||
const isLocal = node.hostname === localHostname || !node.api_url;
|
||||
|
||||
if (isLocal) {
|
||||
const logs = await dockerRequest(`/containers/${containerId}/logs?stdout=1&stderr=1&tail=200×tamps=1`);
|
||||
res.json({ logs: logs || '(no logs)' });
|
||||
} else {
|
||||
const resp = await fetch(`${node.api_url}/sidecar/${containerId}/logs`, {
|
||||
headers: agentAuthHeaders(),
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch remote logs' });
|
||||
const data = await resp.json();
|
||||
res.json(data);
|
||||
}
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
router.post('/containers/:nameOrId/restart', async (req, res, next) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue