rex_logger
Keywords: Logging, PSR-3, Exception-Handling, Debug, Error-Logging, System-Log, var/log/system.log, Best-Practices, Sicheres-Logging, Custom-Logger, Backend-Log-Seite
Übersicht
rex_logger implementiert PSR-3 Logger-Interface. Schreibt Exceptions, Fehler und Debug-Infos in var/log/system.log. Unterscheidet Log-Levels (emergency, alert, critical, error, warning, notice, info, debug). Shortcuts für Exceptions und PHP-Errors.
Methoden
| Methode | Parameter | Rückgabe | Beschreibung |
|---|---|---|---|
factory() |
- | rex_logger |
Gibt Logger-Instanz zurück |
log($level, $message, $context) |
string, string, array |
void |
PSR-3 Standard-Log mit Context-Interpolation |
logException($exception) |
Throwable |
void |
Loggt Exception mit Stacktrace |
logError($errno, $errstr, $errfile, $errline) |
int, string, string, int |
void |
Loggt PHP-Error (E_WARNING, E_NOTICE etc.) |
emergency($message, $context) |
string, array |
void |
System ist nicht nutzbar |
alert($message, $context) |
string, array |
void |
Sofortiges Handeln erforderlich |
critical($message, $context) |
string, array |
void |
Kritische Bedingungen |
error($message, $context) |
string, array |
void |
Fehler ohne Systemausfall |
warning($message, $context) |
string, array |
void |
Warnung |
notice($message, $context) |
string, array |
void |
Hinweis |
info($message, $context) |
string, array |
void |
Informationen |
debug($message, $context) |
string, array |
void |
Debug-Informationen |
Praxisbeispiele
Exception loggen
try {
$addon = rex_addon::get('nonexistent');
} catch (rex_exception $e) {
rex_logger::logException($e);
echo rex_view::error('Addon nicht gefunden');
}
// Wird in var/log/system.log geschrieben:
// [YYYY-MM-DD HH:MM:SS] CRITICAL: rex_exception: Addon "nonexistent" nicht gefunden
// Stack trace: ...
Addon Install/Uninstall Fehler
// install.php
try {
rex_sql_table::get('rex_myaddon_table')
->ensureColumn(new rex_sql_column('id', 'int(11)'))
->ensure();
} catch (Exception $e) {
rex_logger::logException($e);
return false; // Install fehlgeschlagen
}
PHP-Error explizit loggen
// Deprecated-Funktion nutzen, Error loggen
if (function_exists('old_function')) {
rex_logger::logError(
E_USER_DEPRECATED,
'old_function() is deprecated, use new_function()',
__FILE__,
__LINE__
);
}
PSR-3 Log-Levels
$logger = rex_logger::factory();
// Emergency - System down
$logger->emergency('Database server offline');
// Alert - Sofortige Aktion nötig
$logger->alert('Disk space < 5%');
// Critical - Kritisch aber System läuft
$logger->critical('Payment gateway timeout');
// Error - Fehler in Addon
$logger->error('Failed to send email to user');
// Warning - Warnung
$logger->warning('Deprecated API endpoint called');
// Notice - Hinweis
$logger->notice('User login from new IP');
// Info - Information
$logger->info('Cronjob completed successfully');
// Debug - Entwickler-Infos
$logger->debug('Cache hit for key: article_123');
Context-Interpolation
$logger = rex_logger::factory();
// {placeholders} werden durch $context ersetzt
$logger->error('User {user_id} failed login attempt #{attempt}', [
'user_id' => 42,
'attempt' => 3,
]);
// Log: User 42 failed login attempt #3
API-Call Fehler loggen
function fetchExternalData($url) {
try {
$response = file_get_contents($url);
if ($response === false) {
throw new RuntimeException('HTTP request failed');
}
return json_decode($response);
} catch (Exception $e) {
rex_logger::logException($e);
return null;
}
}
Datei-Upload Fehler
$upload = rex_file::upload($_FILES['file'], $targetPath);
if (!$upload['success']) {
rex_logger::factory()->error('File upload failed: {msg}', [
'msg' => $upload['message'],
'file' => $_FILES['file']['name'],
]);
echo rex_view::error($upload['message']);
}
Cronjob mit Logging
class MyCronjob extends rex_cronjob {
public function execute() {
try {
$processed = $this->processItems();
rex_logger::factory()->info('Cronjob processed {count} items', [
'count' => $processed,
]);
return true;
} catch (Exception $e) {
rex_logger::logException($e);
$this->setMessage($e->getMessage());
return false;
}
}
}
Pwned Password Check (yform_field)
try {
$response = file_get_contents("https://api.pwnedpasswords.com/...");
} catch (Exception $e) {
rex_logger::logError(
E_WARNING,
'Failed to connect to pwnedpasswords.com',
__FILE__,
__LINE__
);
}
Bulk-Rework Fehlerbehandlung (uploader)
foreach ($files as $file) {
try {
$this->processFile($file);
} catch (Exception $e) {
rex_logger::logException($e);
continue; // Nächste Datei verarbeiten
}
}
Statistics Addon - IP2Geo Fehler
try {
$geo = $this->fetchGeoData($ip);
} catch (Exception $e) {
rex_logger::logException($e);
return ['country' => 'Unknown', 'city' => 'Unknown'];
}
Media Manager - Responsive Fehler
if (!file_exists($sourcePath)) {
rex_logger::factory()->error('Source image not found: {path}', [
'path' => $sourcePath,
'media' => $mediaFile,
]);
return false;
}
MForm Handler Exception
try {
$value = $this->parseValue($_POST['field']);
} catch (InvalidArgumentException $e) {
rex_logger::logException($e);
$this->addError('Invalid field value');
}
Project Manager Server Connection
try {
$client = new GuzzleHttp\Client();
$response = $client->post($url, ['json' => $data]);
} catch (GuzzleHttp\Exception\RequestException $e) {
rex_logger::logException($e);
echo rex_view::error('Server nicht erreichbar');
}
Debugging mit Context-Array
$logger = rex_logger::factory();
$logger->debug('Article save attempt', [
'article_id' => $article->getId(),
'user' => rex::getUser()->getLogin(),
'changes' => $article->getModifiedFields(),
'timestamp' => date('Y-m-d H:i:s'),
]);
Addon Boot Fehler
// boot.php
try {
if (!class_exists('SomeRequiredClass')) {
throw new rex_exception('Required dependency missing');
}
$addon->includeFile('lib/autoload.php');
} catch (Exception $e) {
rex_logger::logException($e);
}
Log-File auslesen
// var/log/system.log Inhalt lesen
$logFile = rex_path::log('system.log');
$lines = file($logFile, FILE_IGNORE_NEW_LINES);
// Letzte 50 Zeilen ausgeben
$recent = array_slice($lines, -50);
echo '<pre>' . implode("\n", $recent) . '</pre>';
Custom Error Handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
rex_logger::logError($errno, $errstr, $errfile, $errline);
return true; // Error handled
});
// Trigger error
trigger_error('Custom warning', E_USER_WARNING);
Log-Rotation via Cronjob
class LogRotateCronjob extends rex_cronjob {
public function execute() {
$logPath = rex_path::log('system.log');
if (filesize($logPath) > 10 * 1024 * 1024) { // > 10MB
$backupPath = rex_path::log('system_' . date('Y-m-d') . '.log');
rename($logPath, $backupPath);
rex_logger::factory()->info('Log rotated to {file}', [
'file' => basename($backupPath),
]);
}
return true;
}
}
Conditional Logging (Debug-Mode)
if (rex::isDebugMode()) {
rex_logger::factory()->debug('Query executed: {sql}', [
'sql' => $sql->getQuery(),
'time' => $sql->getQueryTime() . 'ms',
]);
}
Try-Catch in Extension Point
rex_extension::register('PACKAGES_INCLUDED', function() {
try {
$config = rex_file::getCache('my_config.json');
$parsed = json_decode($config, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Invalid JSON in config');
}
return $parsed;
} catch (Exception $e) {
rex_logger::logException($e);
return [];
}
});
Batch-Processing mit Fehler-Tracking
$errors = 0;
$success = 0;
foreach ($items as $item) {
try {
$this->processItem($item);
$success++;
} catch (Exception $e) {
rex_logger::factory()->error('Item processing failed: {id}', [
'id' => $item->getId(),
'error' => $e->getMessage(),
]);
$errors++;
}
}
rex_logger::factory()->info('Batch complete: {success} success, {errors} errors', [
'success' => $success,
'errors' => $errors,
]);
Performance-Logging
$start = microtime(true);
// Lange Operation
$result = $this->heavyComputation();
$duration = microtime(true) - $start;
if ($duration > 1.0) {
rex_logger::factory()->warning('Slow operation detected: {duration}s', [
'duration' => round($duration, 2),
'operation' => 'heavyComputation',
]);
}
Best Practices
1. Eigene Logger-Seiten für AddOns
Für AddOns sollten eigene Log-Dateien erstellt werden, um sie von system.log zu trennen. Dies erleichtert das Debugging und die Wartung.
Eigene Log-Datei erstellen
// In boot.php oder lib/logger.php im AddOn
class MyAddonLogger {
private static $logFile;
public static function log($level, $message, $context = []) {
self::open();
// Context-Interpolation wie bei rex_logger
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
$message = strtr($message, $replace);
// Direkt in eigene Log-Datei schreiben
$logData = [ucfirst($level), $message];
self::$logFile->add($logData);
}
public static function open() {
if (!self::$logFile) {
self::$logFile = rex_log_file::factory(rex_path::log('myaddon.log'));
}
}
}
// Verwendung
MyAddonLogger::log('info', 'AddOn initialized');
MyAddonLogger::log('error', 'Failed to process item {id}', ['id' => 123]);
Backend-Seite für eigenes Log
// pages/log.php im AddOn
$func = rex_request('func', 'string');
$logFile = rex_path::log('myaddon.log');
$csrfToken = rex_csrf_token::factory('myaddon_log');
// Funktionen verarbeiten
if ($func && $csrfToken->isValid()) {
if ('delete' === $func && is_file($logFile)) {
if (rex_log_file::delete($logFile)) {
echo rex_view::success('Log gelöscht');
} else {
echo rex_view::error('Fehler beim Löschen');
}
} elseif ('download' === $func && is_file($logFile)) {
rex_response::sendFile($logFile, 'application/octet-stream', 'attachment');
exit;
}
} elseif ($func && !$csrfToken->isValid()) {
echo rex_view::error(rex_i18n::msg('csrf_token_invalid'));
}
// Log-Tabelle
$content = '<table class="table table-hover"><thead><tr>
<th>' . rex_i18n::msg('syslog_timestamp') . '</th>
<th>' . rex_i18n::msg('syslog_message') . '</th>
</tr></thead><tbody>';
if (file_exists($logFile)) {
$file = rex_log_file::factory($logFile);
foreach (new LimitIterator($file, 0, 100) as $entry) {
$data = $entry->getData();
$type = rex_escape($data[0]);
$message = rex_escape($data[1]);
$content .= '<tr>
<td><small>' . rex_formatter::intlDateTime($entry->getTimestamp()) . '</small><br>
<span class="label label-info">' . $type . '</span></td>
<td>' . nl2br($message) . '</td>
</tr>';
}
}
$content .= '</tbody></table>';
// Buttons mit CSRF-Schutz
$formElements = [];
$n = [];
$n['field'] = '<button class="btn btn-delete" type="submit" name="func" value="delete">Löschen</button>';
$formElements[] = $n;
if (is_file($logFile)) {
$n = [];
$n['field'] = '<a class="btn btn-save" href="' . rex_url::currentBackendPage(['func' => 'download'] + $csrfToken->getUrlParams()) . '">Download</a>';
$formElements[] = $n;
}
$fragment = new rex_fragment();
$fragment->setVar('elements', $formElements, false);
$buttons = $fragment->parse('core/form/submit.php');
// Sektion mit Buttons
$fragment = new rex_fragment();
$fragment->setVar('title', 'AddOn Log', false);
$fragment->setVar('content', $content, false);
$fragment->setVar('buttons', $buttons, false);
$section = $fragment->parse('core/page/section.php');
// Form mit CSRF-Token
echo '<form action="' . rex_url::currentBackendPage() . '" method="post">';
echo $csrfToken->getHiddenField();
echo $section;
echo '</form>';
2. Sicheres Logging
✅ RICHTIG: Context-Interpolation nutzen
Nutze immer das $context-Array für Variablen. Dies verhindert Log-Injection-Angriffe (z.B. durch Zeilenumbrüche in Benutzereingaben, die gefälschte Log-Einträge erzeugen könnten). Die Context-Interpolation führt jedoch keine HTML-Escapes durch. Beim Ausgeben im Backend muss – wie im Beispiel oben – weiterhin rex_escape() verwendet werden.
// RICHTIG: Parameter als Context übergeben
$logger->error('User {user_id} failed login from IP {ip}', [
'user_id' => $_POST['username'], // Verhindert Log-Injection
'ip' => $_SERVER['REMOTE_ADDR'],
]);
// RICHTIG: Objekte und Arrays im Context
$logger->info('Article {id} saved by {user}', [
'id' => $article->getId(),
'user' => rex::getUser()->getLogin(),
]);
// RICHTIG: Sensible Daten maskieren
$logger->debug('API call to {url}', [
'url' => $apiUrl,
'api_key' => substr($apiKey, 0, 4) . '***', // Nur erste 4 Zeichen
]);
❌ FALSCH: String-Concatenation
// FALSCH: Unsichere String-Concatenation
$logger->error('User ' . $_POST['username'] . ' failed'); // LOG-INJECTION-RISIKO!
// FALSCH: Manuelle Escaping-Versuche
$logger->error('Failed: ' . htmlspecialchars($userInput)); // Umständlich
// FALSCH: Sensible Daten im Klartext
$logger->debug('API Key: ' . $apiKey); // API-Key im Log!
Escaping in Log-Ausgabe
Bei der Ausgabe von Logs im Backend immer rex_escape() nutzen:
// Backend-Ausgabe von Log-Einträgen
foreach ($file as $entry) {
$data = $entry->getData();
$message = rex_escape($data[1]); // WICHTIG: Escaping!
echo '<div>' . nl2br($message) . '</div>';
}
Sensible Daten vermeiden
// ❌ FALSCH: Passwörter, API-Keys, Tokens im Log
$logger->debug('Login attempt', [
'password' => $password, // NIE!
]);
// ✅ RICHTIG: Keine sensiblen Daten
$logger->debug('Login attempt', [
'username' => $username,
'ip' => $_SERVER['REMOTE_ADDR'],
'timestamp' => time(),
]);
// ✅ RICHTIG: API-Keys maskieren
$logger->info('API call successful', [
'api_key_prefix' => substr($apiKey, 0, 4) . '***',
'endpoint' => $endpoint,
]);
3. Backend System-Log nutzen
Das System bietet unter System > Logdateien eine komfortable Backend-Oberfläche für system.log.
Funktionen der Log-Seite
- Letzte 100 Einträge anzeigen: Neuste Logs werden automatisch geladen
- Löschen: Gesamte Log-Datei löschen (nach Bestätigung)
- Download: Log-Datei als
.logherunterladen - Editor öffnen: Direkter Link zum Öffnen in konfigurierten Editor
- Zeitstempel: Formatierte Anzeige mit Datum/Uhrzeit
- Farbcodierung: Automatische Labels für Log-Levels (Error rot, Warning gelb, etc.)
- Datei-Links: Klickbare Links zu Fehlerquellen (mit Editor-Integration)
Suche in Logs
// Browser-Suche (Strg+F) in Backend-Ansicht nutzen
// Oder: programmatisch nach Einträgen suchen
$logFile = rex_logger::getPath();
$file = rex_log_file::factory($logFile);
$searchTerm = 'API';
$results = [];
foreach ($file as $entry) {
$data = $entry->getData();
if (str_contains($data[1], $searchTerm)) {
$results[] = $entry;
}
}
// Ergebnisse anzeigen
foreach ($results as $entry) {
echo $entry->getTimestamp() . ': ' . $entry->getData()[1] . PHP_EOL;
}
Log programmatisch leeren
// In eigenem AddOn-Backend
if ('clearLog' === rex_request('func', 'string')) {
rex_logger::close(); // File-Handle freigeben
if (rex_log_file::delete(rex_logger::getPath())) {
echo rex_view::success('Log gelöscht');
} else {
echo rex_view::error('Fehler beim Löschen');
}
}
Log-Rotation
Automatisches Rotieren bei Größenüberschreitung:
// In Cronjob oder boot.php
$logPath = rex_logger::getPath();
if (is_file($logPath) && filesize($logPath) > 10 * 1024 * 1024) { // > 10MB
rex_logger::close();
$backupPath = rex_path::log('system_' . date('Y-m-d_H-i-s') . '.log');
if (rename($logPath, $backupPath)) {
rex_logger::factory()->info('Log rotated to {file}', [
'file' => basename($backupPath),
]);
} else {
rex_logger::factory()->error('Failed to rotate log file');
}
}
Alte Logs aufräumen
// Cronjob: Logs älter als 30 Tage löschen
class rex_cronjob_log_cleanup extends rex_cronjob {
public function execute() {
$logDir = rex_path::log();
$files = glob($logDir . 'system_*.log');
$deleted = 0;
$failed = 0;
foreach ($files as $file) {
if (filemtime($file) < strtotime('-30 days')) {
if (@unlink($file)) {
$deleted++;
} else {
$failed++;
}
}
}
$message = "$deleted alte Log-Dateien gelöscht";
if ($failed > 0) {
$message .= ", $failed konnten nicht gelöscht werden";
}
$this->setMessage($message);
return true;
}
}