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

  1. Letzte 100 Einträge anzeigen: Neuste Logs werden automatisch geladen
  2. Löschen: Gesamte Log-Datei löschen (nach Bestätigung)
  3. Download: Log-Datei als .log herunterladen
  4. Editor öffnen: Direkter Link zum Öffnen in konfigurierten Editor
  5. Zeitstempel: Formatierte Anzeige mit Datum/Uhrzeit
  6. Farbcodierung: Automatische Labels für Log-Levels (Error rot, Warning gelb, etc.)
  7. 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;
    }
}