isInstalled) { return; } // Aktuelles error_log Setting speichern $this->originalErrorLog = ini_get('error_log') ?: null; // Temporären Stream für Interception erstellen $this->interceptStream = fopen('php://temp', 'r+'); if ($this->interceptStream === false) { throw new \RuntimeException('Could not create intercept stream'); } // PHP error_log auf unseren Stream umleiten $streamPath = stream_get_meta_data($this->interceptStream)['uri']; ini_set('error_log', $streamPath); // Stream-Monitoring in separatem Process starten $this->startStreamMonitoring(); $this->isInstalled = true; } /** * Deinstalliert den Interceptor und stellt original error_log wieder her */ public function uninstall(): void { if (! $this->isInstalled) { return; } // Original error_log Setting wiederherstellen if ($this->originalErrorLog !== null) { ini_set('error_log', $this->originalErrorLog); } else { ini_restore('error_log'); } // Stream schließen if ($this->interceptStream !== null) { fclose($this->interceptStream); $this->interceptStream = null; } $this->isInstalled = false; } /** * Startet das Stream-Monitoring für error_log Interception */ private function startStreamMonitoring(): void { if ($this->interceptStream === null) { return; } // Nicht-blockierender Stream stream_set_blocking($this->interceptStream, false); // Register shutdown function um letzten Content zu lesen register_shutdown_function(function () { $this->readAndForwardLogs(); }); // Für CLI: Monitoring-Loop in Background if (php_sapi_name() === 'cli') { $this->startCliMonitoring(); } else { // Für Web: Output Buffer Hook $this->startWebMonitoring(); } } /** * CLI Monitoring - Background Stream Reading */ private function startCliMonitoring(): void { // Registriere Tick-Handler für periodisches Lesen declare(ticks=1); register_tick_function(function () { $this->readAndForwardLogs(); }); } /** * Web Monitoring - Output Buffer Hook */ private function startWebMonitoring(): void { // Output buffer callback um am Ende der Request zu lesen $callback = function (string $buffer): string { $this->readAndForwardLogs(); return $buffer; }; \App\Framework\OutputBuffer\OutputBuffer::start( \App\Framework\OutputBuffer\OutputBufferConfig::forWebMonitoring($callback) ); } /** * Liest Logs aus dem Stream und leitet sie an Framework Logger weiter */ private function readAndForwardLogs(): void { if ($this->interceptStream === null) { return; } // Stream position zurücksetzen rewind($this->interceptStream); // Alles aus dem Stream lesen $content = stream_get_contents($this->interceptStream); if ($content === false || $content === '') { return; } // Stream für nächste Writes leeren ftruncate($this->interceptStream, 0); rewind($this->interceptStream); // Content in einzelne Log-Zeilen aufteilen $lines = array_filter(explode("\n", trim($content))); foreach ($lines as $line) { $this->forwardLogLine(trim($line)); } } /** * Leitet eine einzelne Log-Zeile an Framework Logger weiter */ private function forwardLogLine(string $line): void { if (empty($line)) { return; } // Parse PHP error_log Format: [timestamp] message $context = $this->parseErrorLogLine($line); // Bestimme Log Level basierend auf Content $level = $this->determineLogLevel($line); // An Framework Logger weiterleiten $this->logger->error->log($level, $line, $context); // Optional: Auch an original error_log weiterleiten if ($this->originalErrorLog !== null && $this->originalErrorLog !== '') { $this->forwardToOriginalErrorLog($line); } } /** * Parst eine error_log Zeile und extrahiert Context */ private function parseErrorLogLine(string $line): LogContext { $caller = $this->extractCaller(); $context = LogContext::empty() ->withData([ 'source' => 'php_error_log', 'original_line' => $line, 'caller' => $caller, 'intercepted_at' => date('Y-m-d H:i:s'), ]); // Parse PHP Error Format wenn möglich if (preg_match('/^\[([^\]]+)\]\s+(.+)$/', $line, $matches)) { $context = $context->withData([ 'original_timestamp' => $matches[1], 'parsed_message' => $matches[2], ]); } return $context; } /** * Bestimmt das passende Log Level für eine Nachricht */ private function determineLogLevel(string $line): LogLevel { $lowerLine = strtolower($line); return match (true) { str_contains($lowerLine, 'fatal') || str_contains($lowerLine, 'emergency') => LogLevel::EMERGENCY, str_contains($lowerLine, 'critical') || str_contains($lowerLine, 'alert') => LogLevel::CRITICAL, str_contains($lowerLine, 'error') => LogLevel::ERROR, str_contains($lowerLine, 'warning') || str_contains($lowerLine, 'warn') => LogLevel::WARNING, str_contains($lowerLine, 'notice') => LogLevel::NOTICE, str_contains($lowerLine, 'info') => LogLevel::INFO, str_contains($lowerLine, 'debug') => LogLevel::DEBUG, default => LogLevel::ERROR, // Default für error_log }; } /** * Extrahiert Aufrufer-Informationen aus Backtrace */ private function extractCaller(): array { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // Suche nach dem ersten Aufruf außerhalb des Interceptors foreach ($trace as $frame) { if ( isset($frame['file']) && ! str_contains($frame['file'], 'PhpErrorLogInterceptor.php') && ($frame['function'] ?? '') !== 'error_log' ) { return [ 'file' => $frame['file'], 'line' => $frame['line'] ?? 0, 'function' => $frame['function'] ?? 'unknown', 'class' => $frame['class'] ?? null, ]; } } return ['file' => 'unknown', 'line' => 0, 'function' => 'unknown', 'class' => null]; } /** * Leitet Log an originales error_log weiter (optional) */ private function forwardToOriginalErrorLog(string $line): void { if ($this->originalErrorLog === null) { return; } // Temporär original error_log wiederherstellen $current = ini_get('error_log'); ini_set('error_log', $this->originalErrorLog); // An original error_log senden \error_log($line); // Wieder auf unseren Stream umstellen ini_set('error_log', $current); } /** * Prüft ob der Interceptor installiert ist */ public function isInstalled(): bool { return $this->isInstalled; } /** * Destruktor - stellt sicher dass alles sauber aufgeräumt wird */ public function __destruct() { $this->readAndForwardLogs(); // Letzte Logs lesen } }