chore: complete update
This commit is contained in:
107
src/Framework/Logging/DefaultLogger.php
Normal file
107
src/Framework/Logging/DefaultLogger.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* Einfacher Logger für das Framework.
|
||||
*/
|
||||
#[Singleton]
|
||||
final readonly class DefaultLogger implements Logger
|
||||
{
|
||||
/**
|
||||
* @param LogLevel $minLevel Minimales Level, das geloggt werden soll
|
||||
* @param array<LogHandler> $handlers Array von Log-Handlern
|
||||
* @param ProcessorManager $processorManager Optional: Processor Manager für die Verarbeitung
|
||||
*/
|
||||
public function __construct(
|
||||
private LogLevel $minLevel = LogLevel::DEBUG,
|
||||
/** @var LogHandler[] */
|
||||
private array $handlers = [],
|
||||
private ProcessorManager $processorManager = new ProcessorManager(),
|
||||
) {}
|
||||
|
||||
public function debug(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, $message, $context);
|
||||
}
|
||||
|
||||
public function info(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::INFO, $message, $context);
|
||||
}
|
||||
|
||||
public function notice(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::NOTICE, $message, $context);
|
||||
}
|
||||
|
||||
public function warning(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
public function error(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
public function critical(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
public function alert(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::ALERT, $message, $context);
|
||||
}
|
||||
|
||||
public function emergency(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(LogLevel::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Nachricht mit beliebigem Level erstellen
|
||||
*
|
||||
* @param LogLevel $level Log-Level
|
||||
* @param string $message Log-Nachricht
|
||||
* @param array $context Kontext-Daten für Platzhalter-Ersetzung
|
||||
*/
|
||||
public function log(LogLevel $level, string $message, array $context = []): void
|
||||
{
|
||||
// Log-Record erstellen
|
||||
$record = new LogRecord(
|
||||
message: $message,
|
||||
context: $context,
|
||||
level: $level,
|
||||
timestamp: new \DateTimeImmutable(timezone: new DateTimeZone('Europe/Berlin')),
|
||||
);
|
||||
|
||||
// Record durch alle Processors verarbeiten
|
||||
$processedRecord = $this->processorManager->processRecord($record);
|
||||
|
||||
// Alle Handler durchlaufen
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler->isHandling($processedRecord)) {
|
||||
$handler->handle($processedRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Konfiguration des Loggers zurück
|
||||
*/
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'minLevel' => $this->minLevel->value,
|
||||
'handlers' => array_map(fn(LogHandler $h) => get_class($h), $this->handlers),
|
||||
'processors' => $this->processorManager->getProcessorList(),
|
||||
];
|
||||
}
|
||||
}
|
||||
123
src/Framework/Logging/Handlers/ConsoleHandler.php
Normal file
123
src/Framework/Logging/Handlers/ConsoleHandler.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Handler für die Ausgabe von Log-Einträgen in der Konsole.
|
||||
*/
|
||||
class ConsoleHandler implements LogHandler
|
||||
{
|
||||
/**
|
||||
* @var LogLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
*/
|
||||
private LogLevel $minLevel;
|
||||
|
||||
/**
|
||||
* @var bool Ob der Handler nur im Debug-Modus aktiv ist
|
||||
*/
|
||||
private bool $debugOnly;
|
||||
|
||||
/**
|
||||
* @var string Format für die Ausgabe
|
||||
*/
|
||||
private string $outputFormat;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen ConsoleHandler
|
||||
*
|
||||
* @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
* @param bool $debugOnly Ob der Handler nur im Debug-Modus aktiv ist
|
||||
* @param string $outputFormat Format für die Ausgabe
|
||||
*/
|
||||
public function __construct(
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
bool $debugOnly = true,
|
||||
string $outputFormat = '{color}[{level_name}]{reset} {timestamp} {request_id}{message}',
|
||||
private LogLevel $stderrLevel = LogLevel::WARNING,
|
||||
) {
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
$this->debugOnly = $debugOnly;
|
||||
$this->outputFormat = $outputFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll
|
||||
*/
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
// Nur im CLI-Modus aktiv - NIE bei Web-Requests!
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional: Debug-Modus-Check nur in CLI
|
||||
if ($this->debugOnly && !filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return $record->getLevel()->value >= $this->minLevel->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Eintrag
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$logLevel = $record->getLevel();
|
||||
$color = $logLevel->getConsoleColor()->value;
|
||||
$reset = ConsoleColor::RESET->value;
|
||||
|
||||
// Request-ID-Teil erstellen, falls vorhanden
|
||||
$requestId = $record->hasExtra('request_id')
|
||||
? "[{$record->getExtra('request_id')}] "
|
||||
: '';
|
||||
|
||||
// Werte für Platzhalter im Format
|
||||
$values = [
|
||||
'{color}' => $color,
|
||||
'{reset}' => $reset,
|
||||
'{level_name}' => $record->getLevel()->getName(),
|
||||
'{timestamp}' => $record->getFormattedTimestamp(),
|
||||
'{request_id}' => $requestId,
|
||||
'{message}' => $record->getMessage(),
|
||||
'{channel}' => $record->getChannel() ? "[{$record->getChannel()}] " : '',
|
||||
];
|
||||
|
||||
// Formatierte Ausgabe erstellen
|
||||
$output = strtr($this->outputFormat, $values) . PHP_EOL;
|
||||
|
||||
// Fehler und Warnungen auf stderr, alles andere auf stdout
|
||||
if ($record->getLevel()->value >= $this->stderrLevel->value) {
|
||||
// WARNING, ERROR, CRITICAL, ALERT, EMERGENCY -> stderr
|
||||
file_put_contents('php://stderr', $output);
|
||||
} else {
|
||||
// DEBUG, INFO, NOTICE -> stdout
|
||||
echo $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimales Log-Level setzen
|
||||
*/
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabeformat setzen
|
||||
*/
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
157
src/Framework/Logging/Handlers/FileHandler.php
Normal file
157
src/Framework/Logging/Handlers/FileHandler.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Handler für die Ausgabe von Log-Einträgen in Dateien.
|
||||
*/
|
||||
class FileHandler implements LogHandler
|
||||
{
|
||||
/**
|
||||
* @var LogLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
*/
|
||||
private LogLevel $minLevel;
|
||||
|
||||
/**
|
||||
* @var string Pfad zur Log-Datei
|
||||
*/
|
||||
private string $logFile;
|
||||
|
||||
/**
|
||||
* @var string Format für die Ausgabe
|
||||
*/
|
||||
private string $outputFormat;
|
||||
|
||||
/**
|
||||
* @var int Datei-Modi für die Log-Datei
|
||||
*/
|
||||
private int $fileMode;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen FileHandler
|
||||
*
|
||||
* @param string $logFile Pfad zur Log-Datei
|
||||
* @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
* @param string $outputFormat Format für die Ausgabe
|
||||
* @param int $fileMode Datei-Modi für die Log-Datei
|
||||
*/
|
||||
public function __construct(
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
string $outputFormat = '[{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
int $fileMode = 0644
|
||||
) {
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
$this->outputFormat = $outputFormat;
|
||||
$this->fileMode = $fileMode;
|
||||
|
||||
// Stelle sicher, dass das Verzeichnis existiert
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll
|
||||
*/
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
return $record->getLevel()->value >= $this->minLevel->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Eintrag
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
// Request-ID-Teil erstellen, falls vorhanden
|
||||
$requestId = $record->hasExtra('request_id')
|
||||
? "[{$record->getExtra('request_id')}] "
|
||||
: '';
|
||||
|
||||
// Channel-Teil erstellen, falls vorhanden
|
||||
$channel = $record->getChannel()
|
||||
? "[{$record->getChannel()}] "
|
||||
: '';
|
||||
|
||||
// Werte für Platzhalter im Format
|
||||
$values = [
|
||||
'{level_name}' => $record->getLevel()->getName(),
|
||||
'{timestamp}' => $record->getFormattedTimestamp(),
|
||||
'{request_id}' => $requestId,
|
||||
'{channel}' => $channel,
|
||||
'{message}' => $record->getMessage(),
|
||||
];
|
||||
|
||||
// Formatierte Ausgabe erstellen
|
||||
$output = strtr($this->outputFormat, $values) . PHP_EOL;
|
||||
|
||||
// In die Datei schreiben
|
||||
$this->write($output);
|
||||
|
||||
// Bei kritischen Fehlern Cache leeren, um sicherzustellen, dass der Eintrag geschrieben wird
|
||||
if ($record->getLevel()->value >= LogLevel::CRITICAL->value) {
|
||||
$this->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt einen String in die Log-Datei
|
||||
*/
|
||||
protected function write(string $output): void
|
||||
{
|
||||
file_put_contents($this->logFile, $output, FILE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert Cache und stellt sicher, dass alles geschrieben wurde
|
||||
*/
|
||||
protected function flush(): void
|
||||
{
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass ein Verzeichnis existiert
|
||||
*/
|
||||
private function ensureDirectoryExists(string $dir): void
|
||||
{
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimales Log-Level setzen
|
||||
*/
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabeformat setzen
|
||||
*/
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Datei setzen
|
||||
*/
|
||||
public function setLogFile(string $logFile): self
|
||||
{
|
||||
$this->logFile = $logFile;
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
137
src/Framework/Logging/Handlers/JsonFileHandler.php
Normal file
137
src/Framework/Logging/Handlers/JsonFileHandler.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Handler für die Ausgabe von Log-Einträgen als JSON in Dateien.
|
||||
* Besonders nützlich für maschinelle Verarbeitung und Log-Aggregatoren.
|
||||
*/
|
||||
class JsonFileHandler implements LogHandler
|
||||
{
|
||||
/**
|
||||
* @var LogLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
*/
|
||||
private LogLevel $minLevel;
|
||||
|
||||
/**
|
||||
* @var string Pfad zur Log-Datei
|
||||
*/
|
||||
private string $logFile;
|
||||
|
||||
/**
|
||||
* @var array Liste der Felder, die in der JSON-Ausgabe enthalten sein sollen
|
||||
*/
|
||||
private array $includedFields;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen JsonFileHandler
|
||||
*
|
||||
* @param string $logFile Pfad zur Log-Datei
|
||||
* @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||
* @param array|null $includedFields Liste der Felder, die in der JSON-Ausgabe enthalten sein sollen (null = alle)
|
||||
*/
|
||||
public function __construct(
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::INFO,
|
||||
?array $includedFields = null
|
||||
) {
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
|
||||
// Standardfelder, falls nicht anders angegeben
|
||||
$this->includedFields = $includedFields ?? [
|
||||
'timestamp',
|
||||
'level_name',
|
||||
'message',
|
||||
'context',
|
||||
'extra',
|
||||
'channel',
|
||||
];
|
||||
|
||||
// Stelle sicher, dass das Verzeichnis existiert
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll
|
||||
*/
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
return $record->getLevel()->value >= $this->minLevel->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Eintrag
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
// Alle Daten des Records als Array holen
|
||||
$data = $record->toArray();
|
||||
|
||||
// Nur die gewünschten Felder behalten
|
||||
if (!empty($this->includedFields)) {
|
||||
$data = array_intersect_key($data, array_flip($this->includedFields));
|
||||
}
|
||||
|
||||
// Zeitstempel als ISO 8601 formatieren für bessere Interoperabilität
|
||||
if (isset($data['datetime']) && $data['datetime'] instanceof \DateTimeInterface) {
|
||||
$data['timestamp_iso'] = $data['datetime']->format(\DateTimeInterface::ATOM);
|
||||
unset($data['datetime']); // DateTime-Objekt entfernen
|
||||
}
|
||||
|
||||
// Als JSON formatieren und in die Datei schreiben
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR) . PHP_EOL;
|
||||
$this->write($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt einen String in die Log-Datei
|
||||
*/
|
||||
protected function write(string $output): void
|
||||
{
|
||||
file_put_contents($this->logFile, $output, FILE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass ein Verzeichnis existiert
|
||||
*/
|
||||
private function ensureDirectoryExists(string $dir): void
|
||||
{
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimales Log-Level setzen
|
||||
*/
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Liste der Felder, die in der JSON-Ausgabe enthalten sein sollen
|
||||
*/
|
||||
public function setIncludedFields(array $fields): self
|
||||
{
|
||||
$this->includedFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Datei setzen
|
||||
*/
|
||||
public function setLogFile(string $logFile): self
|
||||
{
|
||||
$this->logFile = $logFile;
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
29
src/Framework/Logging/Handlers/QueuedLogHandler.php
Normal file
29
src/Framework/Logging/Handlers/QueuedLogHandler.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
use App\Framework\Logging\ProcessLogCommand;
|
||||
use App\Framework\Queue\Queue;
|
||||
|
||||
final readonly class QueuedLogHandler implements LogHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Queue $queue,
|
||||
#private LogLevel $minLevel = LogLevel::INFO,
|
||||
){}
|
||||
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$job = new ProcessLogCommand($record);
|
||||
$this->queue->push($job);
|
||||
}
|
||||
}
|
||||
95
src/Framework/Logging/Handlers/SyslogHandler.php
Normal file
95
src/Framework/Logging/Handlers/SyslogHandler.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Handler für Syslog-basiertes Logging
|
||||
*/
|
||||
final class SyslogHandler implements LogHandler
|
||||
{
|
||||
private bool $isOpen = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $ident = 'php-app',
|
||||
private readonly int $facility = LOG_USER,
|
||||
private readonly LogLevel $minLevel = LogLevel::DEBUG
|
||||
) {}
|
||||
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
return $record->getLevel()->value >= $this->minLevel->value;
|
||||
}
|
||||
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$this->openSyslog();
|
||||
|
||||
$priority = $this->mapLogLevelToSyslogPriority($record->getLevel());
|
||||
$message = $this->formatMessage($record);
|
||||
|
||||
syslog($priority, $message);
|
||||
}
|
||||
|
||||
private function openSyslog(): void
|
||||
{
|
||||
if (!$this->isOpen) {
|
||||
openlog($this->ident, LOG_PID | LOG_PERROR, $this->facility);
|
||||
$this->isOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function formatMessage(LogRecord $record): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
// Request-ID falls vorhanden
|
||||
if ($record->hasExtra('request_id')) {
|
||||
$parts[] = "[{$record->getExtra('request_id')}]";
|
||||
}
|
||||
|
||||
// Channel falls vorhanden
|
||||
if ($record->getChannel()) {
|
||||
$parts[] = "[{$record->getChannel()}]";
|
||||
}
|
||||
|
||||
// Hauptnachricht
|
||||
$parts[] = $record->getMessage();
|
||||
|
||||
// Context-Daten falls vorhanden
|
||||
$context = $record->getContext();
|
||||
if (!empty($context)) {
|
||||
$parts[] = 'Context: ' . json_encode($context, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappt Framework LogLevel auf Syslog-Prioritäten
|
||||
*/
|
||||
private function mapLogLevelToSyslogPriority(LogLevel $level): int
|
||||
{
|
||||
return match ($level) {
|
||||
LogLevel::EMERGENCY => LOG_EMERG,
|
||||
LogLevel::ALERT => LOG_ALERT,
|
||||
LogLevel::CRITICAL => LOG_CRIT,
|
||||
LogLevel::ERROR => LOG_ERR,
|
||||
LogLevel::WARNING => LOG_WARNING,
|
||||
LogLevel::NOTICE => LOG_NOTICE,
|
||||
LogLevel::INFO => LOG_INFO,
|
||||
LogLevel::DEBUG => LOG_DEBUG,
|
||||
};
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->isOpen) {
|
||||
closelog();
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/Framework/Logging/Handlers/WebHandler.php
Normal file
66
src/Framework/Logging/Handlers/WebHandler.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
final class WebHandler implements LogHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LogLevel $minLevel = LogLevel::DEBUG,
|
||||
private readonly bool $debugOnly = true
|
||||
) {}
|
||||
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
// Nur bei Web-Requests (nicht CLI)
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug-Modus-Check
|
||||
if ($this->debugOnly && !filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $record->getLevel()->value >= $this->minLevel->value;
|
||||
}
|
||||
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$timestamp = $record->getFormattedTimestamp();
|
||||
$level = $record->getLevel()->getName();
|
||||
$message = $record->getMessage();
|
||||
$channel = $record->getChannel();
|
||||
|
||||
// Request-ID falls vorhanden
|
||||
$requestId = $record->hasExtra('request_id')
|
||||
? "[{$record->getExtra('request_id')}] "
|
||||
: '';
|
||||
|
||||
// Channel falls vorhanden
|
||||
$channelPrefix = $channel ? "[$channel] " : '';
|
||||
|
||||
$logLine = sprintf(
|
||||
'[%s] %s[%s] %s%s',
|
||||
$timestamp,
|
||||
$requestId,
|
||||
$level,
|
||||
$channelPrefix,
|
||||
$message
|
||||
);
|
||||
|
||||
// Context-Daten falls vorhanden
|
||||
$context = $record->getContext();
|
||||
if (!empty($context)) {
|
||||
$logLine .= ' | Context: ' . json_encode($context, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
// In error_log schreiben
|
||||
error_log($logLine);
|
||||
}
|
||||
|
||||
}
|
||||
20
src/Framework/Logging/LogHandler.php
Normal file
20
src/Framework/Logging/LogHandler.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
/**
|
||||
* Interface für Log-Handler, die Log-Einträge verarbeiten.
|
||||
*/
|
||||
interface LogHandler
|
||||
{
|
||||
/**
|
||||
* Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll
|
||||
*/
|
||||
public function isHandling(LogRecord $record): bool;
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Eintrag
|
||||
*/
|
||||
public function handle(LogRecord $record): void;
|
||||
}
|
||||
73
src/Framework/Logging/LogLevel.php
Normal file
73
src/Framework/Logging/LogLevel.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
|
||||
/**
|
||||
* Enum für Log-Level mit zugehörigen Werten und Namen.
|
||||
*/
|
||||
enum LogLevel: int
|
||||
{
|
||||
case DEBUG = 100;
|
||||
case INFO = 200;
|
||||
case NOTICE = 250;
|
||||
case WARNING = 300;
|
||||
case ERROR = 400;
|
||||
case CRITICAL = 500;
|
||||
case ALERT = 550;
|
||||
case EMERGENCY = 600;
|
||||
|
||||
/**
|
||||
* Gibt den Namen des Log-Levels zurück.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::DEBUG => 'DEBUG',
|
||||
self::INFO => 'INFO',
|
||||
self::NOTICE => 'NOTICE',
|
||||
self::WARNING => 'WARNING',
|
||||
self::ERROR => 'ERROR',
|
||||
self::CRITICAL => 'CRITICAL',
|
||||
self::ALERT => 'ALERT',
|
||||
self::EMERGENCY => 'EMERGENCY',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Farbe für die Konsole zurück.
|
||||
*/
|
||||
public function getConsoleColor(): ConsoleColor
|
||||
{
|
||||
return match($this) {
|
||||
self::DEBUG => ConsoleColor::GRAY,
|
||||
self::INFO => ConsoleColor::GREEN,
|
||||
self::NOTICE => ConsoleColor::CYAN,
|
||||
self::WARNING => ConsoleColor::YELLOW,
|
||||
self::ERROR => ConsoleColor::RED,
|
||||
self::CRITICAL => ConsoleColor::MAGENTA,
|
||||
self::ALERT => ConsoleColor::WHITE_ON_RED,
|
||||
self::EMERGENCY => ConsoleColor::BLACK_ON_YELLOW,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein LogLevel aus einem Integer-Wert.
|
||||
* Gibt das nächstniedrigere Level zurück, wenn der exakte Wert nicht existiert.
|
||||
*/
|
||||
public static function fromValue(int $value): self
|
||||
{
|
||||
return match(true) {
|
||||
$value >= self::EMERGENCY->value => self::EMERGENCY,
|
||||
$value >= self::ALERT->value => self::ALERT,
|
||||
$value >= self::CRITICAL->value => self::CRITICAL,
|
||||
$value >= self::ERROR->value => self::ERROR,
|
||||
$value >= self::WARNING->value => self::WARNING,
|
||||
$value >= self::NOTICE->value => self::NOTICE,
|
||||
$value >= self::INFO->value => self::INFO,
|
||||
default => self::DEBUG,
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/Framework/Logging/LogProcessor.php
Normal file
25
src/Framework/Logging/LogProcessor.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
/**
|
||||
* Interface für Log-Processors, die Log-Einträge vor dem Handling verarbeiten.
|
||||
*/
|
||||
interface LogProcessor
|
||||
{
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und gibt den modifizierten Record zurück
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord;
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück (höhere Werte = frühere Ausführung)
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string;
|
||||
}
|
||||
162
src/Framework/Logging/LogRecord.php
Normal file
162
src/Framework/Logging/LogRecord.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
/**
|
||||
* Repräsentiert einen einzelnen Log-Eintrag mit allen relevanten Informationen.
|
||||
*/
|
||||
final class LogRecord
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed> Zusätzliche Daten, die dynamisch hinzugefügt werden können
|
||||
*/
|
||||
private array $extra = [];
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Log-Eintrag
|
||||
*/
|
||||
public function __construct(
|
||||
private string $message,
|
||||
private array $context,
|
||||
private LogLevel $level,
|
||||
private \DateTimeImmutable $timestamp,
|
||||
private ?string $channel = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gibt die Log-Nachricht zurück
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Log-Nachricht
|
||||
*/
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Kontext zurück
|
||||
*/
|
||||
public function getContext(): array
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt oder erweitert den Kontext
|
||||
*/
|
||||
public function withContext(array $context): self
|
||||
{
|
||||
$this->context = array_merge($this->context, $context);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Log-Level zurück
|
||||
*/
|
||||
public function getLevel(): LogLevel
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Zeitstempel zurück
|
||||
*/
|
||||
public function getTimestamp(): \DateTimeImmutable
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den formattierten Zeitstempel zurück
|
||||
*/
|
||||
public function getFormattedTimestamp(string $format = 'Y-m-d H:i:s'): string
|
||||
{
|
||||
return $this->timestamp->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Kanal zurück, falls gesetzt
|
||||
*/
|
||||
public function getChannel(): ?string
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Kanal
|
||||
*/
|
||||
public function setChannel(string $channel): self
|
||||
{
|
||||
$this->channel = $channel;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Wert zu den Extra-Daten hinzu
|
||||
*/
|
||||
public function addExtra(string $key, mixed $value): self
|
||||
{
|
||||
$this->extra[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt mehrere Extra-Daten hinzu
|
||||
*/
|
||||
public function addExtras(array $extras): self
|
||||
{
|
||||
foreach ($extras as $key => $value) {
|
||||
$this->extra[$key] = $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein bestimmter Extra-Wert existiert
|
||||
*/
|
||||
public function hasExtra(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Extra-Wert zurück
|
||||
*/
|
||||
public function getExtra(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->extra[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Extra-Daten zurück
|
||||
*/
|
||||
public function getExtras(): array
|
||||
{
|
||||
return $this->extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Array mit allen Log-Daten
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'message' => $this->message,
|
||||
'context' => $this->context,
|
||||
'level' => $this->level->value,
|
||||
'level_name' => $this->level->getName(),
|
||||
'timestamp' => $this->getFormattedTimestamp(),
|
||||
'datetime' => $this->timestamp,
|
||||
'channel' => $this->channel,
|
||||
'extra' => $this->extra,
|
||||
];
|
||||
}
|
||||
}
|
||||
8
src/Framework/Logging/Logger.php
Normal file
8
src/Framework/Logging/Logger.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
interface Logger
|
||||
{
|
||||
|
||||
}
|
||||
46
src/Framework/Logging/LoggerFactory.php
Normal file
46
src/Framework/Logging/LoggerFactory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
/**
|
||||
* Factory für Logger-Instanzen.
|
||||
*/
|
||||
final class LoggerFactory
|
||||
{
|
||||
private static ?DefaultLogger $defaultLogger = null;
|
||||
|
||||
/**
|
||||
* Erzeugt einen neuen Logger mit optionalen Einstellungen.
|
||||
*/
|
||||
public static function create(
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
bool $enabled = true,
|
||||
array $handlers = []
|
||||
): DefaultLogger {
|
||||
return new DefaultLogger($minLevel, $enabled, $handlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Standard-Logger zurück oder erstellt ihn, falls er noch nicht existiert.
|
||||
*/
|
||||
public static function getDefaultLogger(): DefaultLogger
|
||||
{
|
||||
if (self::$defaultLogger === null) {
|
||||
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
|
||||
$minLevel = $debug ? LogLevel::DEBUG : LogLevel::INFO;
|
||||
|
||||
self::$defaultLogger = self::create($minLevel);
|
||||
}
|
||||
|
||||
return self::$defaultLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt einen benutzerdefinierten Logger als Standard-Logger.
|
||||
*/
|
||||
public static function setDefaultLogger(DefaultLogger $logger): void
|
||||
{
|
||||
self::$defaultLogger = $logger;
|
||||
}
|
||||
}
|
||||
44
src/Framework/Logging/LoggerInitializer.php
Normal file
44
src/Framework/Logging/LoggerInitializer.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||
use App\Framework\Logging\Handlers\FileHandler;
|
||||
use App\Framework\Logging\Handlers\QueuedLogHandler;
|
||||
use App\Framework\Logging\Handlers\WebHandler;
|
||||
use App\Framework\Queue\FileQueue;
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\RedisQueue;
|
||||
|
||||
;
|
||||
|
||||
final readonly class LoggerInitializer
|
||||
{
|
||||
public function __construct(
|
||||
#private DatabaseManager $db,
|
||||
#private PathProvider $pathProvider,
|
||||
){}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke():Logger
|
||||
{
|
||||
$processorManager = new ProcessorManager();
|
||||
|
||||
$queue = new RedisQueue('commands', 'redis');
|
||||
#$path = $this->pathProvider->resolvePath('/src/Framework/CommandBus/storage/queue');
|
||||
#$queue = new FileQueue($path);
|
||||
|
||||
return new DefaultLogger(
|
||||
minLevel: LogLevel::DEBUG,
|
||||
handlers: [
|
||||
new QueuedLogHandler($queue),
|
||||
new ConsoleHandler(),
|
||||
new WebHandler(),
|
||||
new FileHandler('logs/app.log')],
|
||||
processorManager: $processorManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/Framework/Logging/ProcessLogCommand.php
Normal file
13
src/Framework/Logging/ProcessLogCommand.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
|
||||
final readonly class ProcessLogCommand
|
||||
{
|
||||
public function __construct(
|
||||
public LogRecord $logData,
|
||||
) {}
|
||||
|
||||
}
|
||||
36
src/Framework/Logging/ProcessLogCommandHandler.php
Normal file
36
src/Framework/Logging/ProcessLogCommandHandler.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\CommandBus\CommandHandler;
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
|
||||
final readonly class ProcessLogCommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private DatabaseManager $db,
|
||||
){}
|
||||
|
||||
#[CommandHandler]
|
||||
public function __invoke(ProcessLogCommand $commandData): void
|
||||
{
|
||||
$data = $commandData->logData;
|
||||
$contextJson = json_encode($data->getContext());
|
||||
$contextHash = hash('sha256', $contextJson);
|
||||
|
||||
// context_hash zur Eindeutigkeit in den Primärschlüssel aufgenommen
|
||||
$this->db->getConnection()->execute("CREATE TABLE IF NOT EXISTS logs (timestamp TIMESTAMP, level VARCHAR(10), message VARCHAR(255), context JSON, context_hash VARCHAR(64), PRIMARY KEY (timestamp, level, message, context_hash))");
|
||||
|
||||
$this->db->getConnection()->execute(
|
||||
"INSERT INTO logs (timestamp, level, message, context, context_hash) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
$data->getFormattedTimestamp(),
|
||||
$data->getLevel()->getName(),
|
||||
$data->getMessage(),
|
||||
$contextJson,
|
||||
$contextHash,
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
79
src/Framework/Logging/ProcessorManager.php
Normal file
79
src/Framework/Logging/ProcessorManager.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
/**
|
||||
* Verwaltet die Log-Processors und wendet sie auf Log-Records an.
|
||||
*/
|
||||
final readonly class ProcessorManager
|
||||
{
|
||||
/** @var array<LogProcessor> Liste der Processors, sortiert nach Priorität */
|
||||
private array $processors;
|
||||
|
||||
public function __construct(LogProcessor ...$processors)
|
||||
{
|
||||
$this->processors = $processors;
|
||||
}
|
||||
|
||||
public function addProcessor(LogProcessor $processor): self
|
||||
{
|
||||
$processors = $this->sortProcessors($processor, ...$this->processors);
|
||||
|
||||
return new self(...$processors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet alle Processors auf einen Log-Record an
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
foreach ($this->processors as $processor) {
|
||||
$record = $processor->processRecord($record);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert die Processors nach Priorität (höhere Priorität = frühere Ausführung)
|
||||
*/
|
||||
private function sortProcessors(): array
|
||||
{
|
||||
$processors = $this->processors;
|
||||
|
||||
usort($processors, function (LogProcessor $a, LogProcessor $b) {
|
||||
return $b->getPriority() <=> $a->getPriority(); // Absteigend sortieren
|
||||
});
|
||||
|
||||
return $processors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle registrierten Processors zurück
|
||||
*
|
||||
* @return array<string, int> Array mit Processor-Namen als Schlüssel und Prioritäten als Werte
|
||||
*/
|
||||
public function getProcessorList(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->processors as $processor) {
|
||||
$result[$processor->getName()] = $processor->getPriority();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function hasProcessor(string $name): bool
|
||||
{
|
||||
return array_any($this->processors, fn($processor) => $processor->getName() === $name);
|
||||
|
||||
}
|
||||
|
||||
public function getProcessor(string $name): ?LogProcessor
|
||||
{
|
||||
return array_find($this->processors, fn($processor) => $processor->getName() === $name);
|
||||
|
||||
}
|
||||
}
|
||||
132
src/Framework/Logging/Processors/ExceptionProcessor.php
Normal file
132
src/Framework/Logging/Processors/ExceptionProcessor.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
use App\Framework\Logging\LogProcessor;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Verarbeitet Exceptions im Kontext und fügt detaillierte Informationen hinzu.
|
||||
*/
|
||||
final class ExceptionProcessor implements LogProcessor
|
||||
{
|
||||
/**
|
||||
* @var int Priorität des Processors (15 = früh, nach Interpolation)
|
||||
*/
|
||||
private const int PRIORITY = 15;
|
||||
|
||||
/**
|
||||
* @var bool Ob Stack-Traces hinzugefügt werden sollen
|
||||
*/
|
||||
private bool $includeStackTraces;
|
||||
|
||||
/**
|
||||
* @var int Maximale Tiefe für Stack-Traces
|
||||
*/
|
||||
private int $traceDepth;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen ExceptionProcessor
|
||||
*/
|
||||
public function __construct(
|
||||
bool $includeStackTraces = true,
|
||||
int $traceDepth = 10
|
||||
) {
|
||||
$this->includeStackTraces = $includeStackTraces;
|
||||
$this->traceDepth = $traceDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und extrahiert Exception-Informationen
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
$context = $record->getContext();
|
||||
|
||||
// Prüfen, ob eine Exception im Kontext vorhanden ist
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$exception = $context['exception'];
|
||||
$exceptionData = $this->formatException($exception);
|
||||
|
||||
// Exception-Daten zu Extra-Daten hinzufügen
|
||||
$record->addExtra('exception', $exceptionData);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert eine Exception für das Logging
|
||||
*/
|
||||
private function formatException(\Throwable $exception): array
|
||||
{
|
||||
$result = [
|
||||
'class' => get_class($exception),
|
||||
'message' => $exception->getMessage(),
|
||||
'code' => $exception->getCode(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
];
|
||||
|
||||
// Stack-Trace hinzufügen, wenn aktiviert
|
||||
if ($this->includeStackTraces) {
|
||||
$result['trace'] = $this->formatTrace($exception);
|
||||
}
|
||||
|
||||
// Bei verschachtelten Exceptions auch die vorherige Exception formatieren
|
||||
if ($exception->getPrevious()) {
|
||||
$result['previous'] = $this->formatException($exception->getPrevious());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert den Stack-Trace einer Exception
|
||||
*/
|
||||
private function formatTrace(\Throwable $exception): array
|
||||
{
|
||||
$trace = $exception->getTrace();
|
||||
$result = [];
|
||||
|
||||
// Tiefe begrenzen
|
||||
$trace = array_slice($trace, 0, $this->traceDepth);
|
||||
|
||||
foreach ($trace as $frame) {
|
||||
$entry = [
|
||||
'file' => $frame['file'] ?? 'unknown',
|
||||
'line' => $frame['line'] ?? 0,
|
||||
];
|
||||
|
||||
if (isset($frame['function'])) {
|
||||
$function = '';
|
||||
if (isset($frame['class'])) {
|
||||
$function .= $frame['class'] . $frame['type'];
|
||||
}
|
||||
$function .= $frame['function'];
|
||||
$entry['function'] = $function;
|
||||
}
|
||||
|
||||
$result[] = $entry;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'exception';
|
||||
}
|
||||
}
|
||||
77
src/Framework/Logging/Processors/InterpolationProcessor.php
Normal file
77
src/Framework/Logging/Processors/InterpolationProcessor.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
use App\Framework\Logging\LogProcessor;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Ersetzt Platzhalter in Log-Nachrichten durch Kontext-Werte.
|
||||
*/
|
||||
final class InterpolationProcessor implements LogProcessor
|
||||
{
|
||||
/**
|
||||
* @var int Priorität des Processors (höher = früher, 20 = sehr früh)
|
||||
*/
|
||||
private const int PRIORITY = 20;
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und ersetzt Platzhalter in der Nachricht
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
$message = $record->getMessage();
|
||||
$context = $record->getContext();
|
||||
|
||||
// Platzhalter ersetzen, falls vorhanden
|
||||
if (str_contains($message, '{')) {
|
||||
$message = $this->interpolate($message, $context);
|
||||
$record->setMessage($message);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter im Log-Text ersetzen
|
||||
*/
|
||||
private function interpolate(string $message, array $context): string
|
||||
{
|
||||
// Platzhalter ersetzen (Format: {key})
|
||||
$replace = [];
|
||||
|
||||
foreach ($context as $key => $val) {
|
||||
if ($val === null || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) {
|
||||
$replace['{' . $key . '}'] = $val;
|
||||
} elseif ($val instanceof \Throwable) {
|
||||
$replace['{' . $key . '}'] = $val->getMessage() . ' in ' . $val->getFile() . ':' . $val->getLine();
|
||||
} elseif (is_object($val)) {
|
||||
$replace['{' . $key . '}'] = '[object ' . get_class($val) . ']';
|
||||
} elseif (is_array($val)) {
|
||||
$replace['{' . $key . '}'] = 'array' . json_encode($val,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
|
||||
} else {
|
||||
$replace['{' . $key . '}'] = '[' . gettype($val) . ']';
|
||||
}
|
||||
}
|
||||
|
||||
return strtr($message, $replace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'interpolation';
|
||||
}
|
||||
}
|
||||
100
src/Framework/Logging/Processors/IntrospectionProcessor.php
Normal file
100
src/Framework/Logging/Processors/IntrospectionProcessor.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
use App\Framework\Logging\LogProcessor;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Fügt Code-Ortsinformationen (Datei, Zeile, Funktion) zu Log-Einträgen hinzu.
|
||||
*/
|
||||
final class IntrospectionProcessor implements LogProcessor
|
||||
{
|
||||
/**
|
||||
* @var int Priorität des Processors
|
||||
*/
|
||||
private const int PRIORITY = 7;
|
||||
|
||||
/**
|
||||
* @var string[] Liste der Klassen, die bei der Introspection ignoriert werden sollen
|
||||
*/
|
||||
private array $skipClassesPartials;
|
||||
|
||||
/**
|
||||
* @var int Anzahl der Frames, die übersprungen werden sollen (Logger usw.)
|
||||
*/
|
||||
private int $skipStackFrames;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen IntrospectionProcessor
|
||||
*
|
||||
* @param string[] $skipClassesPartials Liste der Klassen, die ignoriert werden sollen
|
||||
* @param int $skipStackFrames Anzahl der Stack-Frames, die übersprungen werden sollen
|
||||
*/
|
||||
public function __construct(
|
||||
array $skipClassesPartials = ['App\\Framework\\Logging\\'],
|
||||
int $skipStackFrames = 0
|
||||
) {
|
||||
$this->skipClassesPartials = $skipClassesPartials;
|
||||
$this->skipStackFrames = $skipStackFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und fügt Introspection-Daten hinzu
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
|
||||
// Überspringe Stack-Frames, die zur Logger-Infrastruktur gehören
|
||||
$i = $this->skipStackFrames;
|
||||
$framesCount = count($trace);
|
||||
|
||||
// Suche nach dem ersten Frame, der nicht zu ignorieren ist
|
||||
while ($i < $framesCount) {
|
||||
if (isset($trace[$i]['class'])) {
|
||||
foreach ($this->skipClassesPartials as $partial) {
|
||||
if (str_starts_with($trace[$i]['class'], $partial)) {
|
||||
$i++;
|
||||
continue 2; // Zum nächsten Frame wechseln
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Frame gefunden, der nicht ignoriert werden soll
|
||||
}
|
||||
|
||||
// Sicherstellen, dass wir einen gültigen Frame haben
|
||||
if ($i >= $framesCount) {
|
||||
return $record; // Kein passender Frame gefunden
|
||||
}
|
||||
|
||||
// Introspection-Daten extrahieren
|
||||
$frame = $trace[$i];
|
||||
$introspection = [
|
||||
'file' => $frame['file'] ?? 'unknown',
|
||||
'line' => $frame['line'] ?? 0,
|
||||
'class' => $frame['class'] ?? '',
|
||||
'function' => $frame['function'] ?? '',
|
||||
];
|
||||
|
||||
// Zu den Extra-Daten hinzufügen
|
||||
return $record->addExtra('introspection', $introspection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'introspection';
|
||||
}
|
||||
}
|
||||
54
src/Framework/Logging/Processors/RequestIdProcessor.php
Normal file
54
src/Framework/Logging/Processors/RequestIdProcessor.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
use App\Framework\Http\RequestIdGenerator;
|
||||
use App\Framework\Logging\LogProcessor;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Fügt die Request-ID zu Log-Einträgen hinzu.
|
||||
*/
|
||||
final readonly class RequestIdProcessor implements LogProcessor
|
||||
{
|
||||
/**
|
||||
* @var int Priorität des Processors (10 = recht früh)
|
||||
*/
|
||||
private const int PRIORITY = 10;
|
||||
|
||||
public function __construct(
|
||||
private RequestIdGenerator $requestIdGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und fügt die Request-ID hinzu
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
$currentId = $this->requestIdGenerator->getCurrentId();
|
||||
|
||||
if ($currentId !== null) {
|
||||
// Nur die ID ohne Signatur als Extra-Feld hinzufügen
|
||||
return $record->addExtra('request_id', $currentId->getId());
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'request_id';
|
||||
}
|
||||
}
|
||||
146
src/Framework/Logging/Processors/WebInfoProcessor.php
Normal file
146
src/Framework/Logging/Processors/WebInfoProcessor.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
use App\Framework\Logging\LogProcessor;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
/**
|
||||
* Fügt Web-Informationen wie IP, URL, Methode und User-Agent zu Log-Einträgen hinzu.
|
||||
*/
|
||||
final class WebInfoProcessor implements LogProcessor
|
||||
{
|
||||
/**
|
||||
* @var int Priorität des Processors (5 = früh, aber nach Request-ID)
|
||||
*/
|
||||
private const int PRIORITY = 5;
|
||||
|
||||
/**
|
||||
* @var array Konfiguration, welche Informationen gesammelt werden sollen
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen WebInfoProcessor
|
||||
*
|
||||
* @param array $config Konfiguration, welche Informationen gesammelt werden sollen
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = array_merge(
|
||||
[
|
||||
'url' => true,
|
||||
'ip' => true,
|
||||
'http_method' => true,
|
||||
'user_agent' => true,
|
||||
'referrer' => false,
|
||||
],
|
||||
$config
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und fügt Web-Informationen hinzu
|
||||
*/
|
||||
public function processRecord(LogRecord $record): LogRecord
|
||||
{
|
||||
// Nur im Web-Kontext ausführen
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return $record;
|
||||
}
|
||||
|
||||
// Webinfos als Extra-Felder hinzufügen
|
||||
$extras = $this->collectWebInfo();
|
||||
if (!empty($extras)) {
|
||||
$record->addExtras($extras);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt Informationen aus dem Web-Kontext
|
||||
*/
|
||||
private function collectWebInfo(): array
|
||||
{
|
||||
$info = [];
|
||||
|
||||
if ($this->config['url'] && isset($_SERVER['REQUEST_URI'])) {
|
||||
$info['url'] = $this->getFullUrl();
|
||||
}
|
||||
|
||||
if ($this->config['ip'] && isset($_SERVER['REMOTE_ADDR'])) {
|
||||
$info['ip'] = $this->getClientIp();
|
||||
}
|
||||
|
||||
if ($this->config['http_method'] && isset($_SERVER['REQUEST_METHOD'])) {
|
||||
$info['http_method'] = $_SERVER['REQUEST_METHOD'];
|
||||
}
|
||||
|
||||
if ($this->config['user_agent'] && isset($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$info['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
if ($this->config['referrer'] && isset($_SERVER['HTTP_REFERER'])) {
|
||||
$info['referrer'] = $_SERVER['HTTP_REFERER'];
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die vollständige URL des Requests
|
||||
*/
|
||||
private function getFullUrl(): string
|
||||
{
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
|
||||
return $protocol . '://' . $host . $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die IP-Adresse des Clients unter Berücksichtigung von Proxies
|
||||
*/
|
||||
private function getClientIp(): string
|
||||
{
|
||||
$keys = [
|
||||
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||
'HTTP_X_FORWARDED_FOR', // Standard Proxy/Load Balancer
|
||||
'HTTP_CLIENT_IP', // Manche Proxies
|
||||
'REMOTE_ADDR', // Fallback direkte Verbindung
|
||||
];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (isset($_SERVER[$key])) {
|
||||
// Bei X-Forwarded-For kann eine Komma-separierte Liste vorliegen
|
||||
if ($key === 'HTTP_X_FORWARDED_FOR') {
|
||||
$ips = explode(',', $_SERVER[$key]);
|
||||
return trim($ips[0]); // Erste IP = ursprünglicher Client
|
||||
}
|
||||
|
||||
return $_SERVER[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Priorität des Processors zurück
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen eindeutigen Namen für den Processor zurück
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'web_info';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user