mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 06:07:21 +02:00
64 lines
3.5 KiB
JavaScript
64 lines
3.5 KiB
JavaScript
const { spawn, execFileSync } = require("node:child_process");
|
|
const { createInterface } = require("node:readline");
|
|
|
|
function logSummary() {
|
|
return { completed: 0, interrupted: 0, completed5xx: 0, fatalErrors: 0, errors: 0,
|
|
warnings: 0, tokenRefreshFailures: 0, missingFiles: 0, rateLimited: 0, connectionResets: 0 };
|
|
}
|
|
function observeLine(summary, line) {
|
|
// Older releases rely on Node's stderr. New releases also emit a redacted
|
|
// process_fatal_error event before Node terminates.
|
|
if (/ECONNRESET|socket hang up/i.test(line)) summary.connectionResets++;
|
|
const match = /^\S+ (INFO|WARN|ERROR) \[([^\]]+)\] .*? (\{.*\})$/.exec(line);
|
|
if (!match) {
|
|
if (/Unhandled ['"]error['"] event|unhandled(?:promise)?rejection|FATAL ERROR|heap out of memory/i.test(line)) summary.fatalErrors++;
|
|
return;
|
|
}
|
|
let data;
|
|
try { data = JSON.parse(match[3]); } catch { return; }
|
|
const [, level, scope] = match;
|
|
if (level === "ERROR") summary.errors++;
|
|
if (level === "WARN") summary.warnings++;
|
|
if (data.code === "process_fatal_error" || /startup failed/i.test(line)) summary.fatalErrors++;
|
|
if (data.code === "token_refresh_failed") summary.tokenRefreshFailures++;
|
|
if (scope === "anonymized-file" && data.code === "file_not_found") summary.missingFiles++;
|
|
if (scope === "requests" && typeof data.ms === "number") {
|
|
if (data.outcome === "completed") { summary.completed++; if (data.status >= 500) summary.completed5xx++; }
|
|
else summary.interrupted++;
|
|
if (data.status === 429) summary.rateLimited++;
|
|
}
|
|
}
|
|
async function readLogs(name, since) {
|
|
const summary = logSummary();
|
|
const child = spawn("docker", ["logs", "--since", since, name], { stdio: ["ignore", "pipe", "pipe"] });
|
|
const completion = new Promise((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("close", code => code === 0 ? resolve() : reject(new Error("docker logs failed")));
|
|
});
|
|
for (const output of [child.stdout, child.stderr]) {
|
|
createInterface({ input: output }).on("line", line => observeLine(summary, line));
|
|
}
|
|
await completion;
|
|
return summary;
|
|
}
|
|
function containerSummary(container) {
|
|
return { name: container.Name.replace(/^\//, ""), healthy: container.State.Health?.Status === "healthy",
|
|
running: container.State.Running, restartCount: container.RestartCount,
|
|
oomKilled: container.State.OOMKilled, startedAt: container.State.StartedAt };
|
|
}
|
|
async function main(minutes) {
|
|
if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440) throw new Error("Minutes must be between 1 and 1440");
|
|
const since = new Date(Date.now() - minutes * 60000).toISOString();
|
|
const names = execFileSync("docker", ["ps", "-a", "--filter", "label=com.docker.compose.project=anonymous_github", "--format", "{{.Names}}"], { encoding: "utf8" })
|
|
.trim().split("\n").filter(name => /^anonymous_github-(anonymous_github|streamer)-\d+$/.test(name));
|
|
if (!names.length) throw new Error("No application containers found");
|
|
const containers = JSON.parse(execFileSync("docker", ["inspect", ...names], { encoding: "utf8" }));
|
|
const services = [];
|
|
for (const container of containers) services.push({ ...containerSummary(container), logs: await readLogs(container.Name, since) });
|
|
console.log(JSON.stringify({ checkedAt: new Date().toISOString(), minutes, services }, null, 2));
|
|
}
|
|
module.exports = { logSummary, observeLine, containerSummary };
|
|
if (require.main === module) main(Number(process.argv[2] || 15)).catch(() => {
|
|
console.error("Production log check failed; check Docker access and the requested window."); process.exitCode = 1;
|
|
});
|