feat: Server-Konsolen-Mitschnitt (24h, wie docker logs) als Download im Logs-Bereich

This commit is contained in:
2026-07-18 17:04:13 +02:00
parent 6209c38a83
commit 8419a02e00
4 changed files with 91 additions and 0 deletions

57
backend/src/consoleLog.js Normal file
View File

@@ -0,0 +1,57 @@
const fs = require('fs');
// Persistenter Pfad (selbes Volume wie die DB), übersteht Container-Neustarts
const LOG_FILE = process.env.CONSOLE_LOG_FILE || '/data/app-console.log';
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
const origLog = console.log.bind(console);
const origError = console.error.bind(console);
const origWarn = console.warn.bind(console);
function stringifyArg(a) {
if (typeof a === 'string') return a;
if (a instanceof Error) return a.stack || a.message;
try { return JSON.stringify(a); } catch { return String(a); }
}
function appendLine(level, args) {
try {
const msg = args.map(stringifyArg).join(' ');
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] [${level}] ${msg}\n`);
} catch {
// Darf niemals die eigentliche Konsolen-Ausgabe verhindern
}
}
// Entfernt Zeilen, die älter als MAX_AGE_MS sind. Zeilen ohne erkennbaren
// Zeitstempel (z.B. Fortsetzungszeilen eines mehrzeiligen Stacktraces) werden
// bewusst behalten, um nichts mittendrin abzuschneiden.
function pruneOldLines() {
try {
if (!fs.existsSync(LOG_FILE)) return;
const content = fs.readFileSync(LOG_FILE, 'utf8');
const cutoff = Date.now() - MAX_AGE_MS;
const lines = content.split('\n').filter(line => {
const m = line.match(/^\[([^\]]+)\]/);
if (!m) return true;
const t = new Date(m[1]).getTime();
return Number.isNaN(t) || t >= cutoff;
});
fs.writeFileSync(LOG_FILE, lines.join('\n'));
} catch (e) {
origError('Konsolen-Log-Aufräumen fehlgeschlagen:', e.message);
}
}
function installConsoleCapture() {
console.log = (...args) => { origLog(...args); appendLine('LOG', args); };
console.error = (...args) => { origError(...args); appendLine('ERROR', args); };
console.warn = (...args) => { origWarn(...args); appendLine('WARN', args); };
pruneOldLines(); // einmal direkt beim Start
setInterval(pruneOldLines, 60 * 60 * 1000); // danach stündlich
}
function getLogFilePath() { return LOG_FILE; }
module.exports = { installConsoleCapture, getLogFilePath, pruneOldLines };

View File

@@ -1,3 +1,5 @@
require('./consoleLog').installConsoleCapture();
const express = require('express');
const path = require('path');
const fs = require('fs');

View File

@@ -590,4 +590,14 @@ router.delete('/logs/:id', authenticate, requireAdmin, (req, res) => {
res.json({ ok: true });
});
// GET /console-log/download Live-Konsolen-Mitschnitt (letzte 24h) als
// Datei herunterladen (Admin)
router.get('/console-log/download', authenticate, requireAdmin, (req, res) => {
const { getLogFilePath } = require('../consoleLog');
const filePath = getLogFilePath();
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Noch keine Logs vorhanden' });
const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
res.download(filePath, `dickendock-console-${stamp}.log`);
});
module.exports = router;