Files

143 lines
4.6 KiB
PHP

<?php
/**
* Absturzprotokoll (25.08.2026).
*
* Der Shop schaltet in index.php bewusst alles ab:
* ini_set('display_errors', 0); error_reporting(0);
* Das ist fuer die Anzeige richtig -- nur wurde damit auch das Protokollieren still
* gelegt. Ueber 130 HTTP-500 in zehn Tagen standen nirgends: nicht in error_log.txt,
* nicht im Apache-Log. Gefunden wurden sie erst, als Google sich beschwerte.
*
* DIESE DATEI AENDERT NICHTS AM VERHALTEN DES SHOPS.
* register_shutdown_function() und error_get_last() arbeiten unabhaengig von
* error_reporting -- kein ini_set, kein eigener Exception-Handler (der wuerde aus einem
* sauberen HTTP 500 eine halb gerenderte 200er-Seite machen). Ungefangene Exceptions
* erzeugen ihrerseits einen E_ERROR und landen deshalb ebenfalls hier.
*
* Geschrieben wird in eine EIGENE Datei, nicht in error_log.txt -- dort gehen zwischen
* ein paar tausend Bot-Meldungen pro Tag genau die Zeilen unter, um die es geht.
*
* Auswertung:
* tail -n 40 logs/fatal.log
* grep -c FATAL logs/fatal.log
* awk -F' | ' '{print $4}' logs/fatal.log | sort | uniq -c | sort -rn # haeufigste Stellen
*/
if (!function_exists('intelectra_fatal_log_init')) {
function intelectra_fatal_log_init() {
register_shutdown_function('intelectra_fatal_log_shutdown');
}
function intelectra_fatal_log_shutdown() {
$fehler = error_get_last();
if (!is_array($fehler)) {
return;
}
// Nur echte Abstuerze. Warnungen und Notices interessieren hier nicht -- der
// letzte error_get_last() waere sonst bei fast jedem Request gefuellt.
$fatal = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING,
E_COMPILE_ERROR, E_COMPILE_WARNING, E_USER_ERROR);
if (!in_array($fehler['type'], $fatal, true)) {
return;
}
try {
$datei = __DIR__ . '/../logs/fatal.log';
// Gleiche Stelle nicht im Sekundentakt mitschreiben: eine Bot-Welle auf eine
// kaputte Seite wuerde das Log sonst in Minuten unbrauchbar machen.
if (intelectra_fatal_log_schon_gesehen($fehler)) {
return;
}
// Bei 5 MB einmal wegrotieren. Mehr als zwei Generationen braucht es nicht --
// wer hier nachsieht, sucht den Fehler von heute.
if (@filesize($datei) > 5242880) {
@rename($datei, $datei . '.1');
}
$anfrage = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '-';
$methode = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '-';
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '-';
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-';
$agent = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 120) : '-';
// Die Meldung einer ungefangenen Exception bringt den Stack-Trace mit. Der ist
// das Wertvollste daran, aber mehrzeilig -- fuer eine Zeile pro Fehler wird er
// auf die ersten Ebenen eingedampft.
$meldung = preg_replace('/\s+/', ' ', trim($fehler['message']));
if (strlen($meldung) > 600) {
$meldung = substr($meldung, 0, 600) . ' …';
}
$zeile = sprintf(
"[%s] FATAL | %s | %s:%d | %s %s | ref=%s | ip=%s | ua=%s\n",
date('Y-m-d H:i:s'),
$meldung,
$fehler['file'],
$fehler['line'],
$methode,
$anfrage,
$referer,
$ip,
$agent
);
@file_put_contents($datei, $zeile, FILE_APPEND | LOCK_EX);
} catch (Throwable $e) {
// Ein Protokoll, das selbst Fehler wirft, ist schlimmer als keins.
}
}
/**
* Wurde genau dieser Fehler gerade eben schon protokolliert?
*
* Schluessel ist Datei + Zeile + Meldung, Sperrfrist 60 Sekunden. Die Merkliste liegt
* neben dem Log und wird nur im Fehlerfall angefasst -- im Normalbetrieb kostet diese
* Datei nichts.
*/
function intelectra_fatal_log_schon_gesehen($fehler) {
$merkdatei = __DIR__ . '/../logs/.fatal_seen.json';
$schluessel = md5($fehler['file'] . ':' . $fehler['line'] . ':' . $fehler['message']);
$jetzt = time();
$gesehen = array();
if (is_readable($merkdatei)) {
$roh = @file_get_contents($merkdatei);
if ($roh) {
$dekodiert = @json_decode($roh, true);
if (is_array($dekodiert)) {
$gesehen = $dekodiert;
}
}
}
if (isset($gesehen[$schluessel]) && ($jetzt - (int)$gesehen[$schluessel]) < 60) {
return true;
}
// Abgelaufene Eintraege raus, damit die Datei nicht waechst.
foreach ($gesehen as $k => $zeitpunkt) {
if (($jetzt - (int)$zeitpunkt) > 3600) {
unset($gesehen[$k]);
}
}
$gesehen[$schluessel] = $jetzt;
if (count($gesehen) > 200) {
$gesehen = array_slice($gesehen, -200, 200, true);
}
@file_put_contents($merkdatei, json_encode($gesehen), LOCK_EX);
return false;
}
}
/* EOF */