- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
340 lines
9.5 KiB
PHP
340 lines
9.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Logging\Handlers;
|
|
|
|
use App\Framework\Core\PathProvider;
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\LogRotator;
|
|
|
|
/**
|
|
* Handler für Log-Rotation mit Size- und Time-based Strategien.
|
|
*
|
|
* Erweitert FileHandler mit automatischer Log-Rotation basierend auf:
|
|
* - Dateigröße (via LogRotator)
|
|
* - Zeit (täglich, wöchentlich, monatlich)
|
|
*
|
|
* Verwendung:
|
|
* ```php
|
|
* // Size-based rotation (10MB, 5 files)
|
|
* $handler = RotatingFileHandler::withSizeRotation(
|
|
* 'storage/logs/app.log',
|
|
* maxFileSize: Byte::fromMegabytes(10),
|
|
* maxFiles: 5
|
|
* );
|
|
*
|
|
* // Time-based rotation (täglich)
|
|
* $handler = RotatingFileHandler::daily('storage/logs/app.log');
|
|
*
|
|
* // Kombiniert (Size + Time)
|
|
* $handler = RotatingFileHandler::daily('storage/logs/app.log')
|
|
* ->withMaxSize(Byte::fromMegabytes(50));
|
|
* ```
|
|
*/
|
|
final class RotatingFileHandler extends FileHandler
|
|
{
|
|
private ?string $rotationFrequency = null;
|
|
private ?int $lastRotationCheck = null;
|
|
|
|
/**
|
|
* Factory: Size-based Rotation
|
|
*/
|
|
public static function withSizeRotation(
|
|
string $logFile,
|
|
Byte $maxFileSize = new Byte(10 * 1024 * 1024), // 10MB default
|
|
int $maxFiles = 5,
|
|
bool $compress = true,
|
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
?PathProvider $pathProvider = null
|
|
): self {
|
|
$rotator = new LogRotator($maxFileSize, $maxFiles, $compress);
|
|
|
|
return new self(
|
|
logFile: $logFile,
|
|
minLevel: $minLevel,
|
|
rotator: $rotator,
|
|
pathProvider: $pathProvider
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Factory: Daily Rotation (täglich um Mitternacht)
|
|
*/
|
|
public static function daily(
|
|
string $logFile,
|
|
int $maxFiles = 7,
|
|
bool $compress = true,
|
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
?PathProvider $pathProvider = null
|
|
): self {
|
|
$rotator = new LogRotator(
|
|
maxFileSize: Byte::fromMegabytes(50),
|
|
maxFiles: $maxFiles,
|
|
compress: $compress
|
|
);
|
|
|
|
$handler = new self(
|
|
logFile: $logFile,
|
|
minLevel: $minLevel,
|
|
rotator: $rotator,
|
|
pathProvider: $pathProvider
|
|
);
|
|
|
|
$handler->rotationFrequency = 'daily';
|
|
|
|
return $handler;
|
|
}
|
|
|
|
/**
|
|
* Factory: Weekly Rotation (wöchentlich am Montag)
|
|
*/
|
|
public static function weekly(
|
|
string $logFile,
|
|
int $maxFiles = 4,
|
|
bool $compress = true,
|
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
?PathProvider $pathProvider = null
|
|
): self {
|
|
$rotator = new LogRotator(
|
|
maxFileSize: Byte::fromMegabytes(100),
|
|
maxFiles: $maxFiles,
|
|
compress: $compress
|
|
);
|
|
|
|
$handler = new self(
|
|
logFile: $logFile,
|
|
minLevel: $minLevel,
|
|
rotator: $rotator,
|
|
pathProvider: $pathProvider
|
|
);
|
|
|
|
$handler->rotationFrequency = 'weekly';
|
|
|
|
return $handler;
|
|
}
|
|
|
|
/**
|
|
* Factory: Monthly Rotation (monatlich am 1. des Monats)
|
|
*/
|
|
public static function monthly(
|
|
string $logFile,
|
|
int $maxFiles = 12,
|
|
bool $compress = true,
|
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
?PathProvider $pathProvider = null
|
|
): self {
|
|
$rotator = new LogRotator(
|
|
maxFileSize: Byte::fromMegabytes(200),
|
|
maxFiles: $maxFiles,
|
|
compress: $compress
|
|
);
|
|
|
|
$handler = new self(
|
|
logFile: $logFile,
|
|
minLevel: $minLevel,
|
|
rotator: $rotator,
|
|
pathProvider: $pathProvider
|
|
);
|
|
|
|
$handler->rotationFrequency = 'monthly';
|
|
|
|
return $handler;
|
|
}
|
|
|
|
/**
|
|
* Factory: Production-optimized (25MB, 10 files, täglich)
|
|
*/
|
|
public static function production(
|
|
string $logFile,
|
|
LogLevel|int $minLevel = LogLevel::INFO,
|
|
?PathProvider $pathProvider = null
|
|
): self {
|
|
$rotator = LogRotator::production();
|
|
|
|
$handler = new self(
|
|
logFile: $logFile,
|
|
minLevel: $minLevel,
|
|
rotator: $rotator,
|
|
pathProvider: $pathProvider
|
|
);
|
|
|
|
$handler->rotationFrequency = 'daily';
|
|
|
|
return $handler;
|
|
}
|
|
|
|
/**
|
|
* Verarbeitet einen Log-Eintrag mit Time-based Rotation
|
|
*/
|
|
public function handle(LogRecord $record): void
|
|
{
|
|
// Time-based Rotation prüfen (vor Parent-Handle)
|
|
if ($this->rotationFrequency !== null) {
|
|
$this->checkTimeBasedRotation();
|
|
}
|
|
|
|
// Parent FileHandler handle() führt Size-based Rotation aus
|
|
parent::handle($record);
|
|
}
|
|
|
|
/**
|
|
* Prüft ob Time-based Rotation notwendig ist
|
|
*
|
|
* Diese Methode wird direkt vor handle() aufgerufen und rotiert die Log-Datei,
|
|
* falls die Zeit-basierte Rotation-Bedingung erfüllt ist.
|
|
* Der Parent FileHandler hat Zugriff auf logFile und rotator via reflection.
|
|
*/
|
|
private function checkTimeBasedRotation(): void
|
|
{
|
|
$currentTime = time();
|
|
|
|
// Cache Rotation-Check für Performance (max. 1x pro Minute)
|
|
if ($this->lastRotationCheck !== null && $currentTime - $this->lastRotationCheck < 60) {
|
|
return;
|
|
}
|
|
|
|
$this->lastRotationCheck = $currentTime;
|
|
|
|
$shouldRotate = match ($this->rotationFrequency) {
|
|
'daily' => $this->shouldRotateDaily(),
|
|
'weekly' => $this->shouldRotateWeekly(),
|
|
'monthly' => $this->shouldRotateMonthly(),
|
|
default => false
|
|
};
|
|
|
|
// Nur rotieren wenn Rotation-Bedingung erfüllt ist
|
|
// Parent FileHandler hat den LogRotator, wir müssen ihn nicht direkt aufrufen
|
|
if ($shouldRotate) {
|
|
// Force rotation durch setzen einer sehr kleinen max file size temporär
|
|
// Der Parent FileHandler wird dann beim nächsten handle() rotieren
|
|
$this->triggerRotation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggert eine Rotation durch Manipulation des Rotation-Status
|
|
*/
|
|
private function triggerRotation(): void
|
|
{
|
|
// Nutze Reflection um auf private logFile Property zuzugreifen
|
|
$reflection = new \ReflectionClass(parent::class);
|
|
$logFileProperty = $reflection->getProperty('logFile');
|
|
$logFile = $logFileProperty->getValue($this);
|
|
|
|
$rotatorProperty = $reflection->getProperty('rotator');
|
|
$rotator = $rotatorProperty->getValue($this);
|
|
|
|
// Rotiere direkt wenn Rotator vorhanden
|
|
if ($rotator instanceof LogRotator && file_exists($logFile)) {
|
|
$rotator->rotateLog($logFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prüft ob Daily Rotation notwendig ist
|
|
*/
|
|
private function shouldRotateDaily(): bool
|
|
{
|
|
$logFile = $this->getLogFilePath();
|
|
|
|
if (!file_exists($logFile)) {
|
|
return false;
|
|
}
|
|
|
|
$fileDate = date('Y-m-d', filemtime($logFile));
|
|
$currentDate = date('Y-m-d');
|
|
|
|
// Rotiere wenn Log-Datei von gestern oder älter
|
|
return $fileDate < $currentDate;
|
|
}
|
|
|
|
/**
|
|
* Prüft ob Weekly Rotation notwendig ist
|
|
*/
|
|
private function shouldRotateWeekly(): bool
|
|
{
|
|
$logFile = $this->getLogFilePath();
|
|
|
|
if (!file_exists($logFile)) {
|
|
return false;
|
|
}
|
|
|
|
$fileWeek = date('Y-W', filemtime($logFile));
|
|
$currentWeek = date('Y-W');
|
|
|
|
// Rotiere wenn Log-Datei von letzter Woche oder älter
|
|
return $fileWeek < $currentWeek;
|
|
}
|
|
|
|
/**
|
|
* Prüft ob Monthly Rotation notwendig ist
|
|
*/
|
|
private function shouldRotateMonthly(): bool
|
|
{
|
|
$logFile = $this->getLogFilePath();
|
|
|
|
if (!file_exists($logFile)) {
|
|
return false;
|
|
}
|
|
|
|
$fileMonth = date('Y-m', filemtime($logFile));
|
|
$currentMonth = date('Y-m');
|
|
|
|
// Rotiere wenn Log-Datei von letztem Monat oder älter
|
|
return $fileMonth < $currentMonth;
|
|
}
|
|
|
|
/**
|
|
* Holt Log-File-Pfad via Reflection
|
|
*/
|
|
private function getLogFilePath(): string
|
|
{
|
|
$reflection = new \ReflectionClass(parent::class);
|
|
$logFileProperty = $reflection->getProperty('logFile');
|
|
|
|
return $logFileProperty->getValue($this);
|
|
}
|
|
|
|
/**
|
|
* Setzt maximale Dateigröße (für kombinierte Size + Time Rotation)
|
|
*/
|
|
public function withMaxSize(Byte $maxSize, int $maxFiles = 5, bool $compress = true): self
|
|
{
|
|
// Setze Rotator via Reflection um dynamic property warning zu vermeiden
|
|
$reflection = new \ReflectionClass(parent::class);
|
|
$rotatorProperty = $reflection->getProperty('rotator');
|
|
|
|
$newRotator = new LogRotator(
|
|
maxFileSize: $maxSize,
|
|
maxFiles: $maxFiles,
|
|
compress: $compress
|
|
);
|
|
|
|
$rotatorProperty->setValue($this, $newRotator);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Gibt Rotation-Strategie-Info zurück
|
|
*/
|
|
public function getRotationStrategy(): array
|
|
{
|
|
// Prüfe via Reflection ob Rotator im Parent gesetzt ist
|
|
$reflection = new \ReflectionClass(parent::class);
|
|
$rotatorProperty = $reflection->getProperty('rotator');
|
|
$rotator = $rotatorProperty->getValue($this);
|
|
|
|
return [
|
|
'time_based' => $this->rotationFrequency ?? 'none',
|
|
'size_based' => $rotator !== null,
|
|
'last_check' => $this->lastRotationCheck
|
|
? date('Y-m-d H:i:s', $this->lastRotationCheck)
|
|
: null
|
|
];
|
|
}
|
|
}
|