Files
michaelschiemer/src/Framework/Logging/PhpErrorLogInterceptor.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

296 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Logging;
use App\Framework\Attributes\Singleton;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Interceptor für PHP's native error_log() - Elegant und transparent
*/
#[Singleton]
final class PhpErrorLogInterceptor
{
private $interceptStream = null;
private ?string $originalErrorLog = null;
private bool $isInstalled = false;
public function __construct(
private Logger $logger
) {
}
/**
* Installiert den Interceptor - fängt alle error_log() Aufrufe ab
*/
public function install(): void
{
if ($this->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
}
}