- 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.
296 lines
8.4 KiB
PHP
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
|
|
}
|
|
}
|