Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
77
src/Framework/Logging/Commands/RotateLogsCommand.php
Normal file
77
src/Framework/Logging/Commands/RotateLogsCommand.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Commands;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Logging\LogRotator;
|
||||
|
||||
final readonly class RotateLogsCommand
|
||||
{
|
||||
#[ConsoleCommand(name: "logs:rotate", description: "Rotiert Log-Dateien basierend auf Größe und Anzahl")]
|
||||
public function execute(ConsoleInput $input, ConsoleOutput $output): int
|
||||
{
|
||||
$output->writeLine('<info>Starting log rotation...</info>');
|
||||
|
||||
// Standard Log-Pfade
|
||||
$logPaths = $this->getLogPaths();
|
||||
$rotator = LogRotator::production();
|
||||
|
||||
$rotatedCount = 0;
|
||||
$totalSize = Byte::zero();
|
||||
|
||||
foreach ($logPaths as $name => $path) {
|
||||
if (! file_exists($path)) {
|
||||
$output->writeLine("<comment>Skipping {$name}: File not found ({$path})</comment>");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$beforeSize = Byte::fromBytes(filesize($path));
|
||||
$totalSize = $totalSize->add($beforeSize);
|
||||
|
||||
if ($rotator->shouldRotate($path)) {
|
||||
$output->writeLine("<info>Rotating {$name} ({$path})</info>");
|
||||
|
||||
if ($rotator->rotateLog($path)) {
|
||||
$rotatedCount++;
|
||||
$output->writeLine("<success> ✓ Successfully rotated {$name}</success>");
|
||||
|
||||
// Zeige Rotation-Info
|
||||
$info = $rotator->getRotationInfo($path);
|
||||
$output->writeLine(" Current size: {$info['current_size']}");
|
||||
$output->writeLine(" Rotated files: " . count($info['rotated_files']));
|
||||
} else {
|
||||
$output->writeLine("<error> ✗ Failed to rotate {$name}</error>");
|
||||
}
|
||||
} else {
|
||||
$output->writeLine("<comment> → {$name} doesn't need rotation</comment>");
|
||||
$info = $rotator->getRotationInfo($path);
|
||||
$output->writeLine(" Current size: {$info['current_size']} / {$info['max_size']}");
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeLine('');
|
||||
$output->writeLine("<info>Rotation completed:</info>");
|
||||
$output->writeLine(" Files rotated: {$rotatedCount}");
|
||||
$output->writeLine(" Total log size: " . $totalSize->toHumanReadable());
|
||||
|
||||
return ExitCode::SUCCESS->value;
|
||||
}
|
||||
|
||||
private function getLogPaths(): array
|
||||
{
|
||||
$basePath = $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html';
|
||||
|
||||
return [
|
||||
'app' => $basePath . '/public/logs/app.log',
|
||||
'mcp_debug' => $basePath . '/mcp_debug.log',
|
||||
'framework' => '/tmp/framework.log',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,8 @@ final readonly class DefaultLogger implements Logger
|
||||
/** @var LogHandler[] */
|
||||
private array $handlers = [],
|
||||
private ProcessorManager $processorManager = new ProcessorManager(),
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function debug(string $message, array $context = []): void
|
||||
{
|
||||
@@ -100,7 +101,7 @@ final readonly class DefaultLogger implements Logger
|
||||
{
|
||||
return [
|
||||
'minLevel' => $this->minLevel->value,
|
||||
'handlers' => array_map(fn(LogHandler $h) => get_class($h), $this->handlers),
|
||||
'handlers' => array_map(fn (LogHandler $h) => get_class($h), $this->handlers),
|
||||
'processors' => $this->processorManager->getProcessorList(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
@@ -57,7 +58,7 @@ class ConsoleHandler implements LogHandler
|
||||
}
|
||||
|
||||
// Optional: Debug-Modus-Check nur in CLI
|
||||
if ($this->debugOnly && !filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
if ($this->debugOnly && ! filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ class ConsoleHandler implements LogHandler
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -118,6 +120,7 @@ class ConsoleHandler implements LogHandler
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
use App\Framework\Logging\LogRotator;
|
||||
|
||||
/**
|
||||
* Handler für die Ausgabe von Log-Einträgen in Dateien.
|
||||
@@ -32,6 +35,16 @@ class FileHandler implements LogHandler
|
||||
*/
|
||||
private int $fileMode;
|
||||
|
||||
/**
|
||||
* @var LogRotator|null Log-Rotator für automatische Rotation
|
||||
*/
|
||||
private ?LogRotator $rotator = null;
|
||||
|
||||
/**
|
||||
* @var PathProvider|null PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
private ?PathProvider $pathProvider = null;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen FileHandler
|
||||
*
|
||||
@@ -39,17 +52,29 @@ class FileHandler implements LogHandler
|
||||
* @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
|
||||
* @param LogRotator|null $rotator Optional: Log-Rotator für automatische Rotation
|
||||
* @param PathProvider|null $pathProvider Optional: PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
public function __construct(
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
string $outputFormat = '[{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
int $fileMode = 0644
|
||||
int $fileMode = 0644,
|
||||
?LogRotator $rotator = null,
|
||||
?PathProvider $pathProvider = null
|
||||
) {
|
||||
$this->pathProvider = $pathProvider;
|
||||
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
$logFile = $this->pathProvider->resolvePath($logFile);
|
||||
}
|
||||
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
$this->outputFormat = $outputFormat;
|
||||
$this->fileMode = $fileMode;
|
||||
$this->rotator = $rotator;
|
||||
|
||||
// Stelle sicher, dass das Verzeichnis existiert
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
@@ -90,6 +115,11 @@ class FileHandler implements LogHandler
|
||||
// Formatierte Ausgabe erstellen
|
||||
$output = strtr($this->outputFormat, $values) . PHP_EOL;
|
||||
|
||||
// Prüfe Rotation vor dem Schreiben
|
||||
if ($this->rotator !== null) {
|
||||
$this->rotator->rotateIfNeeded($this->logFile);
|
||||
}
|
||||
|
||||
// In die Datei schreiben
|
||||
$this->write($output);
|
||||
|
||||
@@ -122,7 +152,7 @@ class FileHandler implements LogHandler
|
||||
*/
|
||||
private function ensureDirectoryExists(string $dir): void
|
||||
{
|
||||
if (!file_exists($dir)) {
|
||||
if (! file_exists($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
@@ -133,6 +163,7 @@ class FileHandler implements LogHandler
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -142,6 +173,7 @@ class FileHandler implements LogHandler
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -150,8 +182,14 @@ class FileHandler implements LogHandler
|
||||
*/
|
||||
public function setLogFile(string $logFile): self
|
||||
{
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
$logFile = $this->pathProvider->resolvePath($logFile);
|
||||
}
|
||||
|
||||
$this->logFile = $logFile;
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
@@ -28,18 +30,32 @@ class JsonFileHandler implements LogHandler
|
||||
*/
|
||||
private array $includedFields;
|
||||
|
||||
/**
|
||||
* @var PathProvider|null PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
private ?PathProvider $pathProvider = null;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @param PathProvider|null $pathProvider Optional: PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
public function __construct(
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::INFO,
|
||||
?array $includedFields = null
|
||||
?array $includedFields = null,
|
||||
?PathProvider $pathProvider = null
|
||||
) {
|
||||
$this->pathProvider = $pathProvider;
|
||||
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
$logFile = $this->pathProvider->resolvePath($logFile);
|
||||
}
|
||||
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
|
||||
@@ -74,7 +90,7 @@ class JsonFileHandler implements LogHandler
|
||||
$data = $record->toArray();
|
||||
|
||||
// Nur die gewünschten Felder behalten
|
||||
if (!empty($this->includedFields)) {
|
||||
if (! empty($this->includedFields)) {
|
||||
$data = array_intersect_key($data, array_flip($this->includedFields));
|
||||
}
|
||||
|
||||
@@ -102,7 +118,7 @@ class JsonFileHandler implements LogHandler
|
||||
*/
|
||||
private function ensureDirectoryExists(string $dir): void
|
||||
{
|
||||
if (!file_exists($dir)) {
|
||||
if (! file_exists($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
@@ -113,6 +129,7 @@ class JsonFileHandler implements LogHandler
|
||||
public function setMinLevel(LogLevel|int $level): self
|
||||
{
|
||||
$this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -122,6 +139,7 @@ class JsonFileHandler implements LogHandler
|
||||
public function setIncludedFields(array $fields): self
|
||||
{
|
||||
$this->includedFields = $fields;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -130,8 +148,14 @@ class JsonFileHandler implements LogHandler
|
||||
*/
|
||||
public function setLogFile(string $logFile): self
|
||||
{
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
$logFile = $this->pathProvider->resolvePath($logFile);
|
||||
}
|
||||
|
||||
$this->logFile = $logFile;
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
@@ -14,7 +15,8 @@ final readonly class QueuedLogHandler implements LogHandler
|
||||
public function __construct(
|
||||
private Queue $queue,
|
||||
#private LogLevel $minLevel = LogLevel::INFO,
|
||||
){}
|
||||
) {
|
||||
}
|
||||
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
@@ -18,7 +19,8 @@ final class SyslogHandler implements LogHandler
|
||||
private readonly string $ident = 'php-app',
|
||||
private readonly int $facility = LOG_USER,
|
||||
private readonly LogLevel $minLevel = LogLevel::DEBUG
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function isHandling(LogRecord $record): bool
|
||||
{
|
||||
@@ -37,7 +39,7 @@ final class SyslogHandler implements LogHandler
|
||||
|
||||
private function openSyslog(): void
|
||||
{
|
||||
if (!$this->isOpen) {
|
||||
if (! $this->isOpen) {
|
||||
openlog($this->ident, LOG_PID | LOG_PERROR, $this->facility);
|
||||
$this->isOpen = true;
|
||||
}
|
||||
@@ -62,7 +64,7 @@ final class SyslogHandler implements LogHandler
|
||||
|
||||
// Context-Daten falls vorhanden
|
||||
$context = $record->getContext();
|
||||
if (!empty($context)) {
|
||||
if (! empty($context)) {
|
||||
$parts[] = 'Context: ' . json_encode($context, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
@@ -12,7 +13,8 @@ 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
|
||||
{
|
||||
@@ -22,7 +24,7 @@ final class WebHandler implements LogHandler
|
||||
}
|
||||
|
||||
// Debug-Modus-Check
|
||||
if ($this->debugOnly && !filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
if ($this->debugOnly && ! filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,12 +57,11 @@ final class WebHandler implements LogHandler
|
||||
|
||||
// Context-Daten falls vorhanden
|
||||
$context = $record->getContext();
|
||||
if (!empty($context)) {
|
||||
if (! empty($context)) {
|
||||
$logLine .= ' | Context: ' . json_encode($context, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
// In error_log schreiben
|
||||
error_log($logLine);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
109
src/Framework/Logging/LogConfig.php
Normal file
109
src/Framework/Logging/LogConfig.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
|
||||
/**
|
||||
* Zentrale Konfiguration für Logpfade
|
||||
*/
|
||||
final class LogConfig
|
||||
{
|
||||
/**
|
||||
* @var array<string, string> Konfigurierte Logpfade
|
||||
*/
|
||||
private array $logPaths;
|
||||
|
||||
/**
|
||||
* @var PathProvider PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
private PathProvider $pathProvider;
|
||||
|
||||
/**
|
||||
* Erstellt eine neue LogConfig-Instanz
|
||||
*/
|
||||
public function __construct(PathProvider $pathProvider)
|
||||
{
|
||||
$this->pathProvider = $pathProvider;
|
||||
|
||||
// Basis-Logverzeichnis
|
||||
$logBasePath = $_ENV['LOG_BASE_PATH'] ?? 'logs';
|
||||
|
||||
// Standard-Logpfade definieren
|
||||
$this->logPaths = [
|
||||
// Anwendungslogs
|
||||
'app' => $this->resolvePath($logBasePath . '/app/app.log'),
|
||||
'error' => $this->resolvePath($logBasePath . '/app/error.log'),
|
||||
|
||||
// Systemlogs
|
||||
'nginx_access' => $_ENV['NGINX_ACCESS_LOG'] ?? '/var/log/nginx/access.log',
|
||||
'nginx_error' => $_ENV['NGINX_ERROR_LOG'] ?? '/var/log/nginx/error.log',
|
||||
|
||||
// Sicherheitslogs
|
||||
'security' => $this->resolvePath($logBasePath . '/security/security.log'),
|
||||
|
||||
// Debug-Logs
|
||||
'framework' => $this->resolvePath($logBasePath . '/debug/framework.log'),
|
||||
'cache' => $this->resolvePath($logBasePath . '/debug/cache.log'),
|
||||
'database' => $this->resolvePath($logBasePath . '/debug/database.log'),
|
||||
];
|
||||
|
||||
// Umgebungsvariablen für Logpfade berücksichtigen
|
||||
if (isset($_ENV['LOG_PATH'])) {
|
||||
$this->logPaths['app'] = $_ENV['LOG_PATH'];
|
||||
}
|
||||
|
||||
if (isset($_ENV['PHP_ERROR_LOG'])) {
|
||||
$this->logPaths['error'] = $_ENV['PHP_ERROR_LOG'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Pfad für einen bestimmten Log-Typ zurück
|
||||
*/
|
||||
public function getLogPath(string $type): string
|
||||
{
|
||||
return $this->logPaths[$type] ?? $this->logPaths['app'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle konfigurierten Logpfade zurück
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAllLogPaths(): array
|
||||
{
|
||||
return $this->logPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst einen relativen Pfad auf
|
||||
*/
|
||||
private function resolvePath(string $path): string
|
||||
{
|
||||
return $this->pathProvider->resolvePath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Basis-Logpfad zurück
|
||||
*/
|
||||
public function getBaseLogPath(): string
|
||||
{
|
||||
return dirname($this->logPaths['app']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass alle Logverzeichnisse existieren
|
||||
*/
|
||||
public function ensureLogDirectoriesExist(): void
|
||||
{
|
||||
foreach ($this->logPaths as $path) {
|
||||
$dir = dirname($path);
|
||||
if (! file_exists($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
@@ -22,7 +23,8 @@ final class LogRecord
|
||||
private LogLevel $level,
|
||||
private \DateTimeImmutable $timestamp,
|
||||
private ?string $channel = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Log-Nachricht zurück
|
||||
@@ -38,6 +40,7 @@ final class LogRecord
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -55,6 +58,7 @@ final class LogRecord
|
||||
public function withContext(array $context): self
|
||||
{
|
||||
$this->context = array_merge($this->context, $context);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -96,6 +100,7 @@ final class LogRecord
|
||||
public function setChannel(string $channel): self
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -105,6 +110,7 @@ final class LogRecord
|
||||
public function addExtra(string $key, mixed $value): self
|
||||
{
|
||||
$this->extra[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -116,6 +122,7 @@ final class LogRecord
|
||||
foreach ($extras as $key => $value) {
|
||||
$this->extra[$key] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
254
src/Framework/Logging/LogRotator.php
Normal file
254
src/Framework/Logging/LogRotator.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
final class LogRotator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Byte $maxFileSize = new Byte(10 * 1024 * 1024), // 10MB default
|
||||
private readonly int $maxFiles = 5,
|
||||
private readonly bool $compress = true
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotiert eine Log-Datei wenn sie zu groß ist
|
||||
*/
|
||||
public function rotateIfNeeded(string $logFile): bool
|
||||
{
|
||||
if (! file_exists($logFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentSize = Byte::fromBytes(filesize($logFile));
|
||||
|
||||
if ($currentSize->lessThan($this->maxFileSize)) {
|
||||
return false; // Rotation nicht nötig
|
||||
}
|
||||
|
||||
return $this->rotateLog($logFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die Log-Rotation durch
|
||||
*/
|
||||
public function rotateLog(string $logFile): bool
|
||||
{
|
||||
if (! file_exists($logFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Lösche älteste Log-Datei wenn nötig
|
||||
$this->cleanupOldLogs($logFile);
|
||||
|
||||
// Rotiere bestehende Log-Dateien
|
||||
$this->rotateLogs($logFile);
|
||||
|
||||
// Erstelle neue leere Log-Datei
|
||||
file_put_contents($logFile, '');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Log rotation failed for {$logFile}: " . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotiert bestehende Log-Dateien (.1 -> .2, .2 -> .3, etc.)
|
||||
*/
|
||||
private function rotateLogs(string $logFile): void
|
||||
{
|
||||
// Rotiere von der höchsten zur niedrigsten Nummer
|
||||
for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
|
||||
$currentFile = $logFile . '.' . $i;
|
||||
$nextFile = $logFile . '.' . ($i + 1);
|
||||
|
||||
if (file_exists($currentFile)) {
|
||||
if ($i + 1 <= $this->maxFiles) {
|
||||
// Komprimiere wenn aktiviert
|
||||
if ($this->compress && $i + 1 == $this->maxFiles) {
|
||||
$this->compressFile($currentFile, $nextFile . '.gz');
|
||||
} else {
|
||||
rename($currentFile, $nextFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bewege aktuelle Log-Datei zu .1
|
||||
if (file_exists($logFile)) {
|
||||
rename($logFile, $logFile . '.1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alte Log-Dateien die das Limit überschreiten
|
||||
*/
|
||||
private function cleanupOldLogs(string $logFile): void
|
||||
{
|
||||
$baseDir = dirname($logFile);
|
||||
$baseName = basename($logFile);
|
||||
|
||||
// Finde alle rotierten Log-Dateien
|
||||
$pattern = $baseDir . '/' . $baseName . '.*';
|
||||
$files = glob($pattern);
|
||||
|
||||
if (empty($files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sortiere nach Nummer (höchste zuerst)
|
||||
usort($files, function ($a, $b) use ($baseName) {
|
||||
$aNum = $this->extractRotationNumber($a, $baseName);
|
||||
$bNum = $this->extractRotationNumber($b, $baseName);
|
||||
|
||||
return $bNum <=> $aNum;
|
||||
});
|
||||
|
||||
// Lösche Files die das Limit überschreiten
|
||||
$filesToDelete = array_slice($files, $this->maxFiles - 1);
|
||||
foreach ($filesToDelete as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die Rotations-Nummer aus einem Dateinamen
|
||||
*/
|
||||
private function extractRotationNumber(string $fileName, string $baseName): int
|
||||
{
|
||||
$suffix = str_replace(dirname($fileName) . '/' . $baseName . '.', '', $fileName);
|
||||
$suffix = str_replace('.gz', '', $suffix);
|
||||
|
||||
return is_numeric($suffix) ? (int) $suffix : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Komprimiert eine Datei mit gzip
|
||||
*/
|
||||
private function compressFile(string $source, string $destination): bool
|
||||
{
|
||||
if (! function_exists('gzopen')) {
|
||||
// Fallback: normale Datei verschieben
|
||||
return rename($source, str_replace('.gz', '', $destination));
|
||||
}
|
||||
|
||||
$sourceHandle = fopen($source, 'rb');
|
||||
$destHandle = gzopen($destination, 'wb9');
|
||||
|
||||
if (! $sourceHandle || ! $destHandle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (! feof($sourceHandle)) {
|
||||
$data = fread($sourceHandle, 8192);
|
||||
gzwrite($destHandle, $data);
|
||||
}
|
||||
|
||||
fclose($sourceHandle);
|
||||
gzclose($destHandle);
|
||||
|
||||
// Lösche Original nach erfolgreicher Komprimierung
|
||||
unlink($source);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Information über rotierte Log-Dateien zurück
|
||||
*/
|
||||
public function getRotationInfo(string $logFile): array
|
||||
{
|
||||
$baseDir = dirname($logFile);
|
||||
$baseName = basename($logFile);
|
||||
$pattern = $baseDir . '/' . $baseName . '.*';
|
||||
|
||||
$files = glob($pattern);
|
||||
$rotatedFiles = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (file_exists($file)) {
|
||||
$size = Byte::fromBytes(filesize($file));
|
||||
$rotatedFiles[] = [
|
||||
'file' => $file,
|
||||
'size' => $size->toHumanReadable(),
|
||||
'size_bytes' => $size->toBytes(),
|
||||
'modified' => date('Y-m-d H:i:s', filemtime($file)),
|
||||
'compressed' => str_ends_with($file, '.gz'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sortiere nach Rotation-Nummer
|
||||
usort($rotatedFiles, function ($a, $b) use ($baseName) {
|
||||
$aNum = $this->extractRotationNumber($a['file'], $baseName);
|
||||
$bNum = $this->extractRotationNumber($b['file'], $baseName);
|
||||
|
||||
return $aNum <=> $bNum;
|
||||
});
|
||||
|
||||
return [
|
||||
'current_file' => $logFile,
|
||||
'current_size' => file_exists($logFile) ? Byte::fromBytes(filesize($logFile))->toHumanReadable() : '0 B',
|
||||
'max_size' => $this->maxFileSize->toHumanReadable(),
|
||||
'max_files' => $this->maxFiles,
|
||||
'compress_enabled' => $this->compress,
|
||||
'rotated_files' => $rotatedFiles,
|
||||
'total_files' => count($rotatedFiles) + (file_exists($logFile) ? 1 : 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Rotation empfohlen wird
|
||||
*/
|
||||
public function shouldRotate(string $logFile): bool
|
||||
{
|
||||
if (! file_exists($logFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentSize = Byte::fromBytes(filesize($logFile));
|
||||
|
||||
return $currentSize->greaterThan($this->maxFileSize) || $currentSize->equals($this->maxFileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methoden für gängige Konfigurationen
|
||||
*/
|
||||
public static function daily(): self
|
||||
{
|
||||
return new self(
|
||||
maxFileSize: Byte::fromMegabytes(50), // 50MB
|
||||
maxFiles: 7, // Eine Woche
|
||||
compress: true
|
||||
);
|
||||
}
|
||||
|
||||
public static function weekly(): self
|
||||
{
|
||||
return new self(
|
||||
maxFileSize: Byte::fromMegabytes(100), // 100MB
|
||||
maxFiles: 4, // Ein Monat
|
||||
compress: true
|
||||
);
|
||||
}
|
||||
|
||||
public static function production(): self
|
||||
{
|
||||
return new self(
|
||||
maxFileSize: Byte::fromMegabytes(25), // 25MB
|
||||
maxFiles: 10,
|
||||
compress: true
|
||||
);
|
||||
}
|
||||
}
|
||||
422
src/Framework/Logging/LogViewer.php
Normal file
422
src/Framework/Logging/LogViewer.php
Normal file
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
|
||||
final class LogViewer
|
||||
{
|
||||
private array $logPaths = [];
|
||||
|
||||
private int $defaultLimit = 100;
|
||||
|
||||
private array $logLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
|
||||
public function __construct(
|
||||
array $logPaths = []
|
||||
) {
|
||||
$this->logPaths = $logPaths ?: $this->getDefaultLogPaths();
|
||||
}
|
||||
|
||||
public function getAvailableLogs(): array
|
||||
{
|
||||
$logs = [];
|
||||
|
||||
foreach ($this->logPaths as $name => $path) {
|
||||
if (file_exists($path)) {
|
||||
$fileSize = filesize($path);
|
||||
$byteSize = Byte::fromBytes($fileSize);
|
||||
|
||||
$logs[$name] = [
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
'size' => $fileSize,
|
||||
'size_human' => $byteSize->toHumanReadable(),
|
||||
'modified' => filemtime($path),
|
||||
'modified_human' => date('Y-m-d H:i:s', filemtime($path)),
|
||||
'readable' => is_readable($path),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
public function readLog(string $logName, ?int $limit = null, ?string $level = null, ?string $search = null): array
|
||||
{
|
||||
$limit = $limit ?? $this->defaultLimit;
|
||||
|
||||
if (! isset($this->logPaths[$logName])) {
|
||||
throw new \InvalidArgumentException("Log '{$logName}' not found");
|
||||
}
|
||||
|
||||
$path = $this->logPaths[$logName];
|
||||
|
||||
if (! file_exists($path) || ! is_readable($path)) {
|
||||
throw new \InvalidArgumentException("Log file '{$path}' is not readable");
|
||||
}
|
||||
|
||||
// Use generator for memory efficiency
|
||||
$entries = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->readLogEntriesGenerator($path, $level, $search) as $entry) {
|
||||
$entries[] = $entry;
|
||||
$count++;
|
||||
|
||||
if ($count >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'log_name' => $logName,
|
||||
'path' => $path,
|
||||
'total_entries' => count($entries),
|
||||
'filters' => [
|
||||
'level' => $level,
|
||||
'search' => $search,
|
||||
'limit' => $limit,
|
||||
],
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
public function tailLog(string $logName, int $lines = 50): array
|
||||
{
|
||||
if (! isset($this->logPaths[$logName])) {
|
||||
throw new \InvalidArgumentException("Log '{$logName}' not found");
|
||||
}
|
||||
|
||||
$path = $this->logPaths[$logName];
|
||||
|
||||
if (! file_exists($path) || ! is_readable($path)) {
|
||||
throw new \InvalidArgumentException("Log file '{$path}' is not readable");
|
||||
}
|
||||
|
||||
// Use generator for memory efficiency
|
||||
$entries = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->readLogLinesGenerator($path, $lines) as $line) {
|
||||
$entry = $this->parseLogLine($line);
|
||||
if ($entry) {
|
||||
$entries[] = $entry;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse to show most recent first
|
||||
$entries = array_reverse($entries);
|
||||
|
||||
return [
|
||||
'log_name' => $logName,
|
||||
'path' => $path,
|
||||
'lines_requested' => $lines,
|
||||
'entries_found' => count($entries),
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
public function searchLogs(string $query, ?array $logNames = null, ?string $level = null): array
|
||||
{
|
||||
$logNames = $logNames ?? array_keys($this->logPaths);
|
||||
$results = [];
|
||||
|
||||
foreach ($logNames as $logName) {
|
||||
if (! isset($this->logPaths[$logName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$logData = $this->readLog($logName, 500, $level, $query);
|
||||
if (! empty($logData['entries'])) {
|
||||
$results[$logName] = $logData;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$results[$logName] = [
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'level_filter' => $level,
|
||||
'searched_logs' => $logNames,
|
||||
'results' => $results,
|
||||
'total_matches' => array_sum(array_column($results, 'total_entries')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator für Streaming-API: liefert Log-Einträge einzeln
|
||||
* Ideal für Server-Sent Events oder Ajax-Streaming
|
||||
*/
|
||||
public function streamLog(string $logName, ?string $level = null, ?string $search = null, int $batchSize = 10): \Generator
|
||||
{
|
||||
if (! isset($this->logPaths[$logName])) {
|
||||
throw new \InvalidArgumentException("Log '{$logName}' not found");
|
||||
}
|
||||
|
||||
$path = $this->logPaths[$logName];
|
||||
|
||||
if (! file_exists($path) || ! is_readable($path)) {
|
||||
throw new \InvalidArgumentException("Log file '{$path}' is not readable");
|
||||
}
|
||||
|
||||
$batch = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->readLogEntriesGenerator($path, $level, $search) as $entry) {
|
||||
$batch[] = $entry;
|
||||
$count++;
|
||||
|
||||
// Sende Batch wenn voll
|
||||
if (count($batch) >= $batchSize) {
|
||||
yield [
|
||||
'batch' => $batch,
|
||||
'batch_number' => intval($count / $batchSize),
|
||||
'entries_in_batch' => count($batch),
|
||||
'total_processed' => $count,
|
||||
];
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Sende letzten Batch falls nicht leer
|
||||
if (! empty($batch)) {
|
||||
yield [
|
||||
'batch' => $batch,
|
||||
'batch_number' => intval($count / $batchSize) + 1,
|
||||
'entries_in_batch' => count($batch),
|
||||
'total_processed' => $count,
|
||||
'is_final' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaultLogPaths(): array
|
||||
{
|
||||
$basePath = $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html';
|
||||
|
||||
return [
|
||||
'app' => $basePath . '/public/logs/app.log',
|
||||
'app_dist' => $basePath . '/dist/logs/app.log',
|
||||
'mcp_debug' => $basePath . '/mcp_debug.log',
|
||||
'php_error' => ini_get('error_log') ?: '/var/log/php_errors.log',
|
||||
'framework' => '/tmp/framework.log',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator für effizientes Lesen der letzten N Zeilen einer Log-Datei
|
||||
* Verwendet Memory-Mapping für bessere Performance bei großen Dateien
|
||||
*/
|
||||
private function readLogLinesGenerator(string $path, int $limit): \Generator
|
||||
{
|
||||
$file = fopen($path, 'r');
|
||||
|
||||
if (! $file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileSize = filesize($path);
|
||||
if ($fileSize === 0) {
|
||||
fclose($file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Für kleine Dateien: einfach von hinten lesen
|
||||
if ($fileSize < 1024 * 1024) { // < 1MB
|
||||
yield from $this->readSmallFileLines($file, $limit);
|
||||
fclose($file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Für große Dateien: chunk-weise von hinten lesen
|
||||
yield from $this->readLargeFileLines($file, $fileSize, $limit);
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest Zeilen von kleinen Dateien (< 1MB)
|
||||
*/
|
||||
private function readSmallFileLines($file, int $limit): \Generator
|
||||
{
|
||||
$lines = [];
|
||||
$content = stream_get_contents($file);
|
||||
$allLines = array_filter(explode("\n", $content));
|
||||
|
||||
// Nimm die letzten $limit Zeilen
|
||||
$lastLines = array_slice($allLines, -$limit);
|
||||
|
||||
foreach ($lastLines as $line) {
|
||||
yield trim($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest Zeilen von großen Dateien chunk-weise von hinten
|
||||
*/
|
||||
private function readLargeFileLines($file, int $fileSize, int $limit): \Generator
|
||||
{
|
||||
$chunkSize = 8192; // 8KB chunks
|
||||
$lines = [];
|
||||
$buffer = '';
|
||||
$position = $fileSize;
|
||||
|
||||
while ($position > 0 && count($lines) < $limit) {
|
||||
// Berechne Chunk-Größe
|
||||
$readSize = min($chunkSize, $position);
|
||||
$position -= $readSize;
|
||||
|
||||
// Lese Chunk
|
||||
fseek($file, $position);
|
||||
$chunk = fread($file, $readSize);
|
||||
|
||||
// Füge zu Buffer hinzu
|
||||
$buffer = $chunk . $buffer;
|
||||
|
||||
// Extrahiere vollständige Zeilen
|
||||
$newLines = explode("\n", $buffer);
|
||||
|
||||
// Die erste "Zeile" ist unvollständig (außer am Dateianfang)
|
||||
if ($position > 0) {
|
||||
$buffer = array_shift($newLines);
|
||||
} else {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
// Füge neue Zeilen hinzu (rückwärts)
|
||||
foreach (array_reverse($newLines) as $line) {
|
||||
if (trim($line) !== '') {
|
||||
array_unshift($lines, trim($line));
|
||||
if (count($lines) >= $limit) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gib Zeilen aus
|
||||
foreach ($lines as $line) {
|
||||
yield $line;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator für gefilterte Log-Einträge mit Memory-Effizienz
|
||||
*/
|
||||
private function readLogEntriesGenerator(string $path, ?string $level = null, ?string $search = null): \Generator
|
||||
{
|
||||
foreach ($this->readLogLinesGenerator($path, PHP_INT_MAX) as $line) {
|
||||
$entry = $this->parseLogLine($line);
|
||||
|
||||
if (! $entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Level-Filter anwenden
|
||||
if ($level && strtoupper($entry['level']) !== strtoupper($level)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Such-Filter anwenden
|
||||
if ($search &&
|
||||
stripos($entry['message'], $search) === false &&
|
||||
stripos($entry['context'], $search) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy-Wrapper für Rückwärtskompatibilität
|
||||
*/
|
||||
private function readLogLines(string $path, int $limit): array
|
||||
{
|
||||
return iterator_to_array($this->readLogLinesGenerator($path, $limit));
|
||||
}
|
||||
|
||||
private function parseLogEntries(array $lines): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$entry = $this->parseLogLine($line);
|
||||
if ($entry) {
|
||||
$entries[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return array_reverse($entries); // Most recent first
|
||||
}
|
||||
|
||||
private function parseLogLine(string $line): ?array
|
||||
{
|
||||
// Try to parse common log formats
|
||||
|
||||
// Standard PHP/Laravel format: [2023-12-25 10:30:45] local.ERROR: Message {"context":"data"}
|
||||
if (preg_match('/\[([^\]]+)\]\s+\w+\.(\w+):\s+(.+)/', $line, $matches)) {
|
||||
return [
|
||||
'timestamp' => $matches[1],
|
||||
'level' => strtoupper($matches[2]),
|
||||
'message' => $this->extractMessage($matches[3]),
|
||||
'context' => $this->extractContext($matches[3]),
|
||||
'raw' => $line,
|
||||
'parsed' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Simple format: 2023-12-25 10:30:45 ERROR: Message
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+(\w+):\s+(.+)/', $line, $matches)) {
|
||||
return [
|
||||
'timestamp' => $matches[1],
|
||||
'level' => strtoupper($matches[2]),
|
||||
'message' => $matches[3],
|
||||
'context' => '',
|
||||
'raw' => $line,
|
||||
'parsed' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback - treat as unstructured log
|
||||
return [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'level' => 'INFO',
|
||||
'message' => $line,
|
||||
'context' => '',
|
||||
'raw' => $line,
|
||||
'parsed' => false,
|
||||
];
|
||||
}
|
||||
|
||||
private function extractMessage(string $text): string
|
||||
{
|
||||
// Extract message before JSON context
|
||||
$pos = strpos($text, '{');
|
||||
if ($pos !== false) {
|
||||
return trim(substr($text, 0, $pos));
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function extractContext(string $text): string
|
||||
{
|
||||
// Extract JSON context
|
||||
$pos = strpos($text, '{');
|
||||
if ($pos !== false) {
|
||||
return substr($text, $pos);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
26
src/Framework/Logging/LogViewerInitializer.php
Normal file
26
src/Framework/Logging/LogViewerInitializer.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\DI\Initializer;
|
||||
|
||||
final readonly class LogViewerInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function __invoke(PathProvider $pathProvider): LogViewer
|
||||
{
|
||||
// Erstelle LogConfig für zentrale Pfadverwaltung
|
||||
$logConfig = new LogConfig($pathProvider);
|
||||
|
||||
// Stelle sicher, dass alle Logverzeichnisse existieren
|
||||
$logConfig->ensureLogDirectoriesExist();
|
||||
|
||||
// Verwende die konfigurierten Logpfade aus LogConfig
|
||||
$logPaths = $logConfig->getAllLogPaths();
|
||||
|
||||
return new LogViewer($logPaths);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
interface Logger
|
||||
{
|
||||
public function debug(string $message, array $context = []): void;
|
||||
|
||||
public function info(string $message, array $context = []): void;
|
||||
|
||||
public function notice(string $message, array $context = []): void;
|
||||
|
||||
public function warning(string $message, array $context = []): void;
|
||||
|
||||
public function error(string $message, array $context = []): void;
|
||||
|
||||
public function critical(string $message, array $context = []): void;
|
||||
|
||||
public function alert(string $message, array $context = []): void;
|
||||
|
||||
public function emergency(string $message, array $context = []): void;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
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;
|
||||
@@ -12,33 +14,57 @@ use App\Framework\Logging\Handlers\WebHandler;
|
||||
use App\Framework\Queue\FileQueue;
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\RedisQueue;
|
||||
|
||||
;
|
||||
use App\Framework\Redis\RedisConfig;
|
||||
use App\Framework\Redis\RedisConnection;
|
||||
|
||||
final readonly class LoggerInitializer
|
||||
{
|
||||
public function __construct(
|
||||
#private DatabaseManager $db,
|
||||
#private PathProvider $pathProvider,
|
||||
){}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke():Logger
|
||||
public function __invoke(TypedConfiguration $config, PathProvider $pathProvider): Logger
|
||||
{
|
||||
$processorManager = new ProcessorManager();
|
||||
|
||||
$queue = new RedisQueue('commands', 'redis');
|
||||
#$path = $this->pathProvider->resolvePath('/src/Framework/CommandBus/storage/queue');
|
||||
#$queue = new FileQueue($path);
|
||||
// Set log level based on environment
|
||||
$minLevel = $config->app->isDebugEnabled()
|
||||
? LogLevel::DEBUG
|
||||
: LogLevel::INFO;
|
||||
|
||||
// Erstelle LogConfig für zentrale Pfadverwaltung
|
||||
$logConfig = new LogConfig($pathProvider);
|
||||
|
||||
// Stelle sicher, dass alle Logverzeichnisse existieren
|
||||
$logConfig->ensureLogDirectoriesExist();
|
||||
|
||||
$redisConfig = new RedisConfig(host: 'redis', database: 2);
|
||||
$redisConnection = new RedisConnection($redisConfig, 'queue');
|
||||
$queue = new RedisQueue($redisConnection, 'commands');
|
||||
|
||||
// Alternativ: FileQueue mit aufgelöstem Pfad
|
||||
// $queuePath = $pathProvider->resolvePath('storage/queue');
|
||||
// $queue = new FileQueue($queuePath);
|
||||
|
||||
// In production, we might want to exclude console handler
|
||||
$handlers = [];
|
||||
|
||||
if (! $config->app->isProduction()) {
|
||||
$handlers[] = new ConsoleHandler();
|
||||
}
|
||||
|
||||
$handlers[] = new QueuedLogHandler($queue);
|
||||
$handlers[] = new WebHandler();
|
||||
$handlers[] = new FileHandler(
|
||||
$logConfig->getLogPath('app'),
|
||||
$minLevel,
|
||||
'[{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
0644,
|
||||
null,
|
||||
$pathProvider
|
||||
);
|
||||
|
||||
return new DefaultLogger(
|
||||
minLevel: LogLevel::DEBUG,
|
||||
handlers: [
|
||||
new QueuedLogHandler($queue),
|
||||
new ConsoleHandler(),
|
||||
new WebHandler(),
|
||||
new FileHandler('logs/app.log')],
|
||||
processorManager: $processorManager,
|
||||
minLevel: $minLevel,
|
||||
handlers: $handlers,
|
||||
processorManager: $processorManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
final readonly class ProcessLogCommand
|
||||
{
|
||||
public function __construct(
|
||||
public LogRecord $logData,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\CommandBus\CommandHandler;
|
||||
@@ -9,7 +11,8 @@ final readonly class ProcessLogCommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private DatabaseManager $db,
|
||||
){}
|
||||
) {
|
||||
}
|
||||
|
||||
#[CommandHandler]
|
||||
public function __invoke(ProcessLogCommand $commandData): void
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
@@ -8,7 +9,7 @@ namespace App\Framework\Logging;
|
||||
*/
|
||||
final readonly class ProcessorManager
|
||||
{
|
||||
/** @var array<LogProcessor> Liste der Processors, sortiert nach Priorität */
|
||||
/** @var array<LogProcessor> Liste der Processors, sortiert nach Priorität */
|
||||
private array $processors;
|
||||
|
||||
public function __construct(LogProcessor ...$processors)
|
||||
@@ -67,13 +68,13 @@ final readonly class ProcessorManager
|
||||
|
||||
public function hasProcessor(string $name): bool
|
||||
{
|
||||
return array_any($this->processors, fn($processor) => $processor->getName() === $name);
|
||||
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);
|
||||
return array_find($this->processors, fn ($processor) => $processor->getName() === $name);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
@@ -49,8 +50,10 @@ final class InterpolationProcessor implements LogProcessor
|
||||
} 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);
|
||||
$replace['{' . $key . '}'] = 'array' . json_encode(
|
||||
$val,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
|
||||
);
|
||||
} else {
|
||||
$replace['{' . $key . '}'] = '[' . gettype($val) . ']';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
@@ -57,10 +58,12 @@ final class IntrospectionProcessor implements LogProcessor
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
@@ -19,7 +20,8 @@ final readonly class RequestIdProcessor implements LogProcessor
|
||||
|
||||
public function __construct(
|
||||
private RequestIdGenerator $requestIdGenerator
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen Log-Record und fügt die Request-ID hinzu
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Processors;
|
||||
@@ -52,7 +53,7 @@ final class WebInfoProcessor implements LogProcessor
|
||||
|
||||
// Webinfos als Extra-Felder hinzufügen
|
||||
$extras = $this->collectWebInfo();
|
||||
if (!empty($extras)) {
|
||||
if (! empty($extras)) {
|
||||
$record->addExtras($extras);
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ final class WebInfoProcessor implements LogProcessor
|
||||
*/
|
||||
private function getFullUrl(): string
|
||||
{
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$protocol = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
|
||||
@@ -118,6 +119,7 @@ final class WebInfoProcessor implements LogProcessor
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user