Implementierung von Lebenszyklusprotokollierung und Serverstartstatistiken
All checks were successful
Deploy SingleChat / deploy (push) Successful in 48s
All checks were successful
Deploy SingleChat / deploy (push) Successful in 48s
This commit is contained in:
@@ -2,6 +2,7 @@ import crypto from 'crypto';
|
|||||||
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { getServerStartStats } from './server-lifecycle.js';
|
||||||
|
|
||||||
const TIMEOUT_SECONDS = 1800; // 30 Minuten
|
const TIMEOUT_SECONDS = 1800; // 30 Minuten
|
||||||
const MAX_ACTIVE_VIDEO_CONNECTIONS = 3;
|
const MAX_ACTIVE_VIDEO_CONNECTIONS = 3;
|
||||||
@@ -609,27 +610,6 @@ function logClientLogin(client, __dirname) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAndLogStart(__dirname) {
|
|
||||||
try {
|
|
||||||
const logsDir = join(__dirname, '../logs');
|
|
||||||
// Erstelle logs-Verzeichnis falls es nicht existiert
|
|
||||||
if (!existsSync(logsDir)) {
|
|
||||||
try {
|
|
||||||
mkdirSync(logsDir, { recursive: true });
|
|
||||||
console.log(`[Log] Logs-Verzeichnis erstellt: ${logsDir}`);
|
|
||||||
} catch (mkdirError) {
|
|
||||||
console.error(`[Log] Fehler beim Erstellen des Logs-Verzeichnisses: ${mkdirError.message}`);
|
|
||||||
return; // Beende Funktion, wenn Verzeichnis nicht erstellt werden kann
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const logPath = join(logsDir, 'starts.log');
|
|
||||||
const logEntry = `${new Date().toISOString()}\n`;
|
|
||||||
appendFileSync(logPath, logEntry, 'utf-8');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Loggen des Starts:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupBroadcast(io, __dirname) {
|
export function setupBroadcast(io, __dirname) {
|
||||||
// Länderliste beim Start laden
|
// Länderliste beim Start laden
|
||||||
let countriesMap = {};
|
let countriesMap = {};
|
||||||
@@ -728,23 +708,8 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStartsLogStats() {
|
|
||||||
const path = join(ensureLogsDir(__dirname), 'starts.log');
|
|
||||||
if (!existsSync(path)) {
|
|
||||||
return { count: 0, first: null, last: null };
|
|
||||||
}
|
|
||||||
const lines = readFileSync(path, 'utf-8')
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => l.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return { count: 0, first: null, last: null };
|
|
||||||
}
|
|
||||||
return { count: lines.length, first: lines[0], last: lines[lines.length - 1] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sammelt alle verfügbaren Kennzahlen: Live-Server, starts.log, vollständige Auswertung von logins.log
|
* Sammelt alle verfügbaren Kennzahlen: Live-Server, echte Serverstarts und vollständige Login-Auswertung.
|
||||||
* (entspricht inhaltlich den /stat-Unterbefehlen in einer Tabelle).
|
* (entspricht inhaltlich den /stat-Unterbefehlen in einer Tabelle).
|
||||||
*/
|
*/
|
||||||
function buildFullAllStatsRows(records) {
|
function buildFullAllStatsRows(records) {
|
||||||
@@ -763,7 +728,7 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
totalMessages += Array.isArray(msgs) ? msgs.length : 0;
|
totalMessages += Array.isArray(msgs) ? msgs.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const starts = readStartsLogStats();
|
const starts = getServerStartStats(__dirname);
|
||||||
|
|
||||||
push('— Live (Server) —', '');
|
push('— Live (Server) —', '');
|
||||||
push('Nutzer mit Chat-Login und aktiver Verbindung', liveOnline);
|
push('Nutzer mit Chat-Login und aktiver Verbindung', liveOnline);
|
||||||
@@ -771,13 +736,17 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
push('Client-Sitzungen im Speicher', clients.size);
|
push('Client-Sitzungen im Speicher', clients.size);
|
||||||
push('Unterhaltungen im Speicher (Paare)', conversations.size);
|
push('Unterhaltungen im Speicher (Paare)', conversations.size);
|
||||||
push('Nachrichten-Einträge gesamt (RAM)', totalMessages);
|
push('Nachrichten-Einträge gesamt (RAM)', totalMessages);
|
||||||
push('Server-Starts protokolliert (starts.log)', starts.count);
|
push('Server-Starts protokolliert (server-lifecycle.jsonl)', starts.count);
|
||||||
if (starts.first) {
|
if (starts.first) {
|
||||||
push('Erster Start (Timestamp)', starts.first);
|
push('Erster Server-Start (Timestamp)', starts.first.timestamp);
|
||||||
}
|
}
|
||||||
if (starts.last) {
|
if (starts.last) {
|
||||||
push('Letzter Start (Timestamp)', starts.last);
|
push('Letzter Server-Start (Timestamp)', starts.last.timestamp);
|
||||||
|
push('Anlass des letzten Server-Starts', starts.last.reason);
|
||||||
}
|
}
|
||||||
|
starts.latest.forEach((start, index) => {
|
||||||
|
push(`Server-Start ${index + 1} (UTC)`, `${start.timestamp} — ${start.reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
push('— Login-Protokoll (logins.log, UTC) —', '');
|
push('— Login-Protokoll (logins.log, UTC) —', '');
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
@@ -887,7 +856,7 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
['/stat ages', 'Jüngster und ältester Nutzer'],
|
['/stat ages', 'Jüngster und ältester Nutzer'],
|
||||||
['/stat names', 'Häufigkeit der verwendeten Namen'],
|
['/stat names', 'Häufigkeit der verwendeten Namen'],
|
||||||
['/stat countries', 'Häufigkeit der Länder'],
|
['/stat countries', 'Häufigkeit der Länder'],
|
||||||
['/all-stats', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung']
|
['/all-stats', 'Alle Kennzahlen: Live-Server + echte Serverstarts + komplette Login-Auswertung']
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1148,7 +1117,7 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
['/logout-admin', 'Admin-/Command-Login beenden'],
|
['/logout-admin', 'Admin-/Command-Login beenden'],
|
||||||
['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'],
|
['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'],
|
||||||
['/stat help', 'Hilfe zu Statistikbefehlen anzeigen'],
|
['/stat help', 'Hilfe zu Statistikbefehlen anzeigen'],
|
||||||
['/all-stats', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung'],
|
['/all-stats', 'Alle Kennzahlen: Live-Server + echte Serverstarts + komplette Login-Auswertung'],
|
||||||
['/kick <username>', 'Benutzer aus dem Chat werfen'],
|
['/kick <username>', 'Benutzer aus dem Chat werfen'],
|
||||||
['/help oder /?', 'Diese Hilfe anzeigen']
|
['/help oder /?', 'Diese Hilfe anzeigen']
|
||||||
]);
|
]);
|
||||||
@@ -1670,7 +1639,6 @@ export function setupBroadcast(io, __dirname) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logClientLogin(client, __dirname);
|
logClientLogin(client, __dirname);
|
||||||
checkAndLogStart(__dirname);
|
|
||||||
|
|
||||||
// Benutzerliste an alle senden
|
// Benutzerliste an alle senden
|
||||||
broadcastUserList();
|
broadcastUserList();
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { dirname, join } from 'path';
|
|||||||
import { setupBroadcast } from './broadcast.js';
|
import { setupBroadcast } from './broadcast.js';
|
||||||
import { setupRoutes } from './routes.js';
|
import { setupRoutes } from './routes.js';
|
||||||
import { setupSEORoutes } from './routes-seo.js';
|
import { setupSEORoutes } from './routes-seo.js';
|
||||||
|
import { installLifecycleHandlers, logServerStart } from './server-lifecycle.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
installLifecycleHandlers(__dirname, server);
|
||||||
|
|
||||||
// Umgebungsvariablen
|
// Umgebungsvariablen
|
||||||
const NODE_ENV = process.env.NODE_ENV || 'development';
|
const NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
@@ -233,6 +235,7 @@ if (IS_PRODUCTION) {
|
|||||||
const HOST = process.env.HOST || '127.0.0.1';
|
const HOST = process.env.HOST || '127.0.0.1';
|
||||||
|
|
||||||
server.listen(PORT, HOST, () => {
|
server.listen(PORT, HOST, () => {
|
||||||
|
logServerStart(__dirname);
|
||||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||||
console.log(`Umgebung: ${NODE_ENV}`);
|
console.log(`Umgebung: ${NODE_ENV}`);
|
||||||
console.log(`CORS erlaubt für: ${allowedOrigins.join(', ')}`);
|
console.log(`CORS erlaubt für: ${allowedOrigins.join(', ')}`);
|
||||||
|
|||||||
111
server/server-lifecycle.js
Normal file
111
server/server-lifecycle.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const LIFECYCLE_LOG_FILE = 'server-lifecycle.jsonl';
|
||||||
|
|
||||||
|
function getLogPath(__dirname) {
|
||||||
|
const logsDir = join(__dirname, '../logs');
|
||||||
|
if (!existsSync(logsDir)) {
|
||||||
|
mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return join(logsDir, LIFECYCLE_LOG_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendEvent(__dirname, event) {
|
||||||
|
const entry = { timestamp: new Date().toISOString(), ...event };
|
||||||
|
try {
|
||||||
|
appendFileSync(getLogPath(__dirname), `${JSON.stringify(entry)}\n`, 'utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Lifecycle] Ereignis konnte nicht protokolliert werden:', error.message);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEvents(__dirname) {
|
||||||
|
try {
|
||||||
|
const logPath = getLogPath(__dirname);
|
||||||
|
if (!existsSync(logPath)) return [];
|
||||||
|
return readFileSync(logPath, 'utf-8')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((event) => event && typeof event.timestamp === 'string' && typeof event.type === 'string');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Lifecycle] Protokoll konnte nicht gelesen werden:', error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logServerStart(__dirname) {
|
||||||
|
const events = readEvents(__dirname);
|
||||||
|
const previous = events.at(-1);
|
||||||
|
let reason = 'Erststart oder vorheriger Shutdown ist bereits protokolliert';
|
||||||
|
|
||||||
|
// SIGKILL, OOM und Stromausfälle geben dem Node-Prozess keine Gelegenheit,
|
||||||
|
// einen Shutdown zu schreiben. Beim nächsten Start machen wir das transparent.
|
||||||
|
if (previous?.type === 'start') {
|
||||||
|
appendEvent(__dirname, {
|
||||||
|
type: 'stop',
|
||||||
|
reason: 'Abrupte Beendigung: kein sauberer Shutdown protokolliert',
|
||||||
|
previousPid: previous.pid
|
||||||
|
});
|
||||||
|
reason = 'Neustart nach abrupter Beendigung der vorherigen Instanz';
|
||||||
|
} else if (previous?.type === 'stop' && previous.reason?.startsWith('Kontrollierter Shutdown')) {
|
||||||
|
reason = `Kontrollierter Neustart nach: ${previous.reason}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appendEvent(__dirname, { type: 'start', pid: process.pid, reason });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logServerStop(__dirname, reason, details = {}) {
|
||||||
|
return appendEvent(__dirname, { type: 'stop', pid: process.pid, reason, ...details });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerStartStats(__dirname, limit = 5) {
|
||||||
|
const starts = readEvents(__dirname).filter((event) => event.type === 'start');
|
||||||
|
return {
|
||||||
|
count: starts.length,
|
||||||
|
first: starts[0] || null,
|
||||||
|
last: starts.at(-1) || null,
|
||||||
|
latest: starts.slice(-limit).reverse()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installLifecycleHandlers(__dirname, httpServer) {
|
||||||
|
let shuttingDown = false;
|
||||||
|
|
||||||
|
const gracefulShutdown = (signal) => {
|
||||||
|
if (shuttingDown) return;
|
||||||
|
shuttingDown = true;
|
||||||
|
logServerStop(
|
||||||
|
__dirname,
|
||||||
|
`Kontrollierter Shutdown per ${signal} (manuell, Deployment oder System-Neustart)`
|
||||||
|
);
|
||||||
|
httpServer.close(() => process.exit(0));
|
||||||
|
setTimeout(() => process.exit(0), 10_000).unref();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
logServerStop(__dirname, 'Absturz durch unbehandelte Ausnahme', {
|
||||||
|
error: String(error?.stack || error?.message || error)
|
||||||
|
});
|
||||||
|
console.error('[Lifecycle] Unbehandelte Ausnahme:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
logServerStop(__dirname, 'Absturz durch unbehandelte Promise-Ablehnung', {
|
||||||
|
error: String(reason?.stack || reason?.message || reason)
|
||||||
|
});
|
||||||
|
console.error('[Lifecycle] Unbehandelte Promise-Ablehnung:', reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user