refactor: improve logging system and add deployment fixes
- Enhance logging handlers (Console, DockerJson, File, JsonFile, MultiFile) - Improve exception and line formatters - Update logger initialization and processor management - Add Ansible playbooks for staging 502 error troubleshooting - Update deployment documentation - Fix serializer and queue components - Update error kernel and queued log handler
This commit is contained in:
@@ -5,10 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Logging\ValueObjects\LogContext;
|
||||
use DateMalformedStringException;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* Einfacher Logger für das Framework.
|
||||
@@ -19,12 +18,14 @@ final readonly class DefaultLogger implements Logger, SupportsChannels
|
||||
private ChannelLoggerRegistry $channelRegistry;
|
||||
|
||||
/**
|
||||
* @param Clock $clock Clock für Timestamp-Generierung
|
||||
* @param LogLevel $minLevel Minimales Level, das geloggt werden soll
|
||||
* @param array<LogHandler> $handlers Array von Log-Handlern
|
||||
* @param ProcessorManager $processorManager Processor Manager für die Verarbeitung
|
||||
* @param LogContextManager|null $contextManager Optional: Context Manager für automatische Kontext-Anreicherung
|
||||
*/
|
||||
public function __construct(
|
||||
private Clock $clock,
|
||||
private LogLevel $minLevel = LogLevel::DEBUG,
|
||||
/** @var LogHandler[] */
|
||||
private array $handlers = [],
|
||||
@@ -84,6 +85,30 @@ final readonly class DefaultLogger implements Logger, SupportsChannels
|
||||
* @throws DateMalformedStringException
|
||||
*/
|
||||
public function log(LogLevel $level, string $message, ?LogContext $context = null): void
|
||||
{
|
||||
$this->createAndProcessRecord($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt in einen spezifischen Channel
|
||||
*
|
||||
* @internal Wird von ChannelLogger verwendet
|
||||
*/
|
||||
public function logToChannel(LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null): void
|
||||
{
|
||||
$this->createAndProcessRecord($level, $message, $context, $channel->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt und verarbeitet einen Log-Record
|
||||
*
|
||||
* @param LogLevel $level Log-Level
|
||||
* @param string $message Log-Nachricht
|
||||
* @param LogContext|null $context Strukturierter LogContext
|
||||
* @param string|null $channel Optional: Channel-Name
|
||||
* @throws DateMalformedStringException
|
||||
*/
|
||||
private function createAndProcessRecord(LogLevel $level, string $message, ?LogContext $context = null, ?string $channel = null): void
|
||||
{
|
||||
// Wenn kein Context übergeben, leeren Context erstellen
|
||||
if ($context === null) {
|
||||
@@ -103,47 +128,8 @@ final readonly class DefaultLogger implements Logger, SupportsChannels
|
||||
message: $message,
|
||||
context: $finalContext,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt in einen spezifischen Channel
|
||||
*
|
||||
* @internal Wird von ChannelLogger verwendet
|
||||
*/
|
||||
public function logToChannel(LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null): void
|
||||
{
|
||||
// Wenn kein Context übergeben, leeren Context erstellen
|
||||
if ($context === null) {
|
||||
$context = LogContext::empty();
|
||||
}
|
||||
|
||||
// Prüfen, ob Level hoch genug ist
|
||||
if ($level->isLowerThan($this->minLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// LogContext automatisch mit aktuellem Context anreichern
|
||||
$finalContext = $this->enrichWithCurrentContext($context);
|
||||
|
||||
// Log-Record erstellen mit Channel
|
||||
$record = new LogRecord(
|
||||
message: $message,
|
||||
context: $finalContext,
|
||||
level: $level,
|
||||
timestamp: new DateTimeImmutable(timezone: new DateTimeZone('Europe/Berlin')),
|
||||
channel: $channel->value
|
||||
timestamp: $this->clock->now(),
|
||||
channel: $channel
|
||||
);
|
||||
|
||||
// Record durch alle Processors verarbeiten
|
||||
@@ -173,75 +159,6 @@ final readonly class DefaultLogger implements Logger, SupportsChannels
|
||||
return $currentContext->merge($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert LogContext zu Array für Legacy-Kompatibilität
|
||||
* @deprecated
|
||||
*/
|
||||
private function convertLogContextToArray(LogContext $logContext): array
|
||||
{
|
||||
$context = $logContext->structured;
|
||||
|
||||
// Tags hinzufügen
|
||||
if ($logContext->hasTags()) {
|
||||
$context['_tags'] = $logContext->tags;
|
||||
}
|
||||
|
||||
// Trace-Informationen hinzufügen
|
||||
if ($logContext->trace !== null) {
|
||||
$context['_trace_id'] = $logContext->trace->getTraceId();
|
||||
if ($activeSpan = $logContext->trace->getActiveSpan()) {
|
||||
$context['_span_id'] = $activeSpan->spanId;
|
||||
}
|
||||
}
|
||||
|
||||
// User-Kontext hinzufügen
|
||||
if ($logContext->user !== null) {
|
||||
$context['_user_id'] = $logContext->user->userId ?? null;
|
||||
}
|
||||
|
||||
// Request-Kontext hinzufügen
|
||||
if ($logContext->request !== null) {
|
||||
$context['_request_id'] = $logContext->request->requestId ?? null;
|
||||
}
|
||||
|
||||
return array_merge($context, $logContext->metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reichert LogRecord mit strukturierten Daten aus LogContext an
|
||||
*/
|
||||
private function enrichRecordWithLogContext(LogRecord $record, LogContext $logContext): LogRecord
|
||||
{
|
||||
// Tags als Extra hinzufügen
|
||||
if ($logContext->hasTags()) {
|
||||
$record->addExtra('structured_tags', $logContext->tags);
|
||||
}
|
||||
|
||||
// Trace-Kontext als Extra hinzufügen
|
||||
if ($logContext->trace !== null) {
|
||||
$record->addExtra('trace_context', [
|
||||
'trace_id' => $logContext->trace->getTraceId(),
|
||||
'active_span' => $logContext->trace->getActiveSpan()?->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
// User-Kontext als Extra hinzufügen
|
||||
if ($logContext->user !== null) {
|
||||
$record->addExtra('user_context', $logContext->user->toArray());
|
||||
}
|
||||
|
||||
// Request-Kontext als Extra hinzufügen
|
||||
if ($logContext->request !== null) {
|
||||
$record->addExtra('request_context', $logContext->request->toArray());
|
||||
}
|
||||
|
||||
// Metadaten als Extra hinzufügen
|
||||
if (! empty($logContext->metadata)) {
|
||||
$record->addExtras($logContext->metadata);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt einen ChannelLogger für einen spezifischen Channel
|
||||
|
||||
@@ -13,11 +13,11 @@ use App\Framework\Logging\ValueObjects\StackFrame;
|
||||
* Formatiert ExceptionContext zu lesbarem String-Format
|
||||
* ähnlich wie PHP's natürliche Exception-Ausgabe.
|
||||
*/
|
||||
final class ExceptionFormatter
|
||||
final readonly class ExceptionFormatter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $maxStackFrames = 10,
|
||||
private readonly bool $includeArgs = false
|
||||
private int $maxStackFrames = 10,
|
||||
private bool $includeArgs = false
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,24 @@ final readonly class LineFormatter implements LogFormatter
|
||||
$allData = array_merge($context, $extras);
|
||||
$contextString = ! empty($allData) ? json_encode($allData, JSON_UNESCAPED_SLASHES) : '';
|
||||
|
||||
// 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()}] "
|
||||
: '';
|
||||
|
||||
$replacements = [
|
||||
'{timestamp}' => $record->getFormattedTimestamp($this->timestampFormat),
|
||||
'{channel}' => $record->channel ?? 'app',
|
||||
'{level}' => $record->level->getName(),
|
||||
'{level_name}' => $record->level->getName(),
|
||||
'{message}' => $record->message,
|
||||
'{context}' => $contextString,
|
||||
'{request_id}' => $requestId,
|
||||
];
|
||||
|
||||
return strtr($this->format, $replacements);
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\Formatter\LogFormatter;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
@@ -25,26 +25,26 @@ class ConsoleHandler implements LogHandler
|
||||
private bool $debugOnly;
|
||||
|
||||
/**
|
||||
* @var string Format für die Ausgabe
|
||||
* @var LogFormatter Formatter für die Log-Ausgabe
|
||||
*/
|
||||
private string $outputFormat;
|
||||
private LogFormatter $formatter;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen ConsoleHandler
|
||||
*
|
||||
* @param LogFormatter $formatter Formatter für die Log-Ausgabe
|
||||
* @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}{structured}',
|
||||
LogFormatter $formatter,
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
bool $debugOnly = true,
|
||||
private readonly LogLevel $stderrLevel = LogLevel::WARNING,
|
||||
) {
|
||||
$this->formatter = $formatter;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
$this->debugOnly = $debugOnly;
|
||||
$this->outputFormat = $outputFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,32 +71,18 @@ class ConsoleHandler implements LogHandler
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$logLevel = $record->getLevel();
|
||||
$color = $logLevel->getConsoleColor()->value;
|
||||
$reset = ConsoleColor::RESET->value;
|
||||
// Formatter verwenden für Formatierung
|
||||
$formatted = ($this->formatter)($record);
|
||||
|
||||
// Request-ID-Teil erstellen, falls vorhanden
|
||||
$requestId = $record->hasExtra('request_id')
|
||||
? "[{$record->getExtra('request_id')}] "
|
||||
: '';
|
||||
// Formatter gibt immer string zurück für Console
|
||||
$output = is_string($formatted)
|
||||
? $formatted
|
||||
: json_encode($formatted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// Structured Logging Extras formatieren
|
||||
$structuredInfo = $this->formatStructuredExtras($record);
|
||||
|
||||
// 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()}] " : '',
|
||||
'{structured}' => $structuredInfo,
|
||||
];
|
||||
|
||||
// Formatierte Ausgabe erstellen
|
||||
$output = strtr($this->outputFormat, $values) . PHP_EOL;
|
||||
// Stelle sicher, dass ein Newline vorhanden ist
|
||||
if (!str_ends_with($output, PHP_EOL)) {
|
||||
$output .= PHP_EOL;
|
||||
}
|
||||
|
||||
// Fehler und Warnungen auf stderr, alles andere auf stdout
|
||||
if ($record->getLevel()->value >= $this->stderrLevel->value) {
|
||||
@@ -108,6 +94,14 @@ class ConsoleHandler implements LogHandler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Formatter zurück
|
||||
*/
|
||||
public function getFormatter(): LogFormatter
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimales Log-Level setzen
|
||||
*/
|
||||
@@ -117,101 +111,4 @@ class ConsoleHandler implements LogHandler
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabeformat setzen
|
||||
*/
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Structured Logging Extras für Console-Ausgabe mit Farben
|
||||
*/
|
||||
private function formatStructuredExtras(LogRecord $record): string
|
||||
{
|
||||
$parts = [];
|
||||
$reset = ConsoleColor::RESET->toAnsi();
|
||||
|
||||
// Tags anzeigen (Cyan mit Tag-Symbol)
|
||||
if ($record->hasExtra('structured_tags')) {
|
||||
$tags = $record->getExtra('structured_tags');
|
||||
if (! empty($tags)) {
|
||||
$cyan = ConsoleColor::CYAN->toAnsi();
|
||||
$tagString = implode(',', $tags);
|
||||
$parts[] = "{$cyan}🏷 [{$tagString}]{$reset}";
|
||||
}
|
||||
}
|
||||
|
||||
// Trace-Kontext anzeigen (Blau mit Trace-Symbol)
|
||||
if ($record->hasExtra('trace_context')) {
|
||||
$traceContext = $record->getExtra('trace_context');
|
||||
$blue = ConsoleColor::BLUE->toAnsi();
|
||||
|
||||
if (isset($traceContext['trace_id'])) {
|
||||
$traceId = substr($traceContext['trace_id'], 0, 8);
|
||||
$parts[] = "{$blue}🔍 {$traceId}{$reset}";
|
||||
}
|
||||
if (isset($traceContext['active_span']['spanId'])) {
|
||||
$spanId = substr($traceContext['active_span']['spanId'], 0, 8);
|
||||
$parts[] = "{$blue}↳ {$spanId}{$reset}";
|
||||
}
|
||||
}
|
||||
|
||||
// User-Kontext anzeigen (Grün mit User-Symbol)
|
||||
if ($record->hasExtra('user_context')) {
|
||||
$userContext = $record->getExtra('user_context');
|
||||
$green = ConsoleColor::GREEN->toAnsi();
|
||||
|
||||
if (isset($userContext['user_id'])) {
|
||||
// Anonymisierte User-ID für Privacy
|
||||
$userId = substr(md5($userContext['user_id']), 0, 8);
|
||||
$parts[] = "{$green}👤 {$userId}{$reset}";
|
||||
} elseif (isset($userContext['is_authenticated']) && ! $userContext['is_authenticated']) {
|
||||
$parts[] = "{$green}👤 anon{$reset}";
|
||||
}
|
||||
}
|
||||
|
||||
// Request-Kontext anzeigen (Gelb mit HTTP-Symbol)
|
||||
if ($record->hasExtra('request_context')) {
|
||||
$requestContext = $record->getExtra('request_context');
|
||||
if (isset($requestContext['request_method'], $requestContext['request_uri'])) {
|
||||
$yellow = ConsoleColor::YELLOW->toAnsi();
|
||||
$method = $requestContext['request_method'];
|
||||
$uri = $requestContext['request_uri'];
|
||||
|
||||
// Kompakte URI-Darstellung
|
||||
if (strlen($uri) > 25) {
|
||||
$uri = substr($uri, 0, 22) . '...';
|
||||
}
|
||||
|
||||
$parts[] = "{$yellow}🌐 {$method} {$uri}{$reset}";
|
||||
}
|
||||
}
|
||||
|
||||
// Context-Data anzeigen (Grau mit Data-Symbol)
|
||||
$context = $record->getContext();
|
||||
if (! empty($context)) {
|
||||
$contextKeys = array_keys($context);
|
||||
// Interne Keys ausfiltern
|
||||
$contextKeys = array_filter($contextKeys, fn ($key) => ! str_starts_with($key, '_'));
|
||||
|
||||
if (! empty($contextKeys)) {
|
||||
$gray = ConsoleColor::GRAY->toAnsi();
|
||||
$keyCount = count($contextKeys);
|
||||
|
||||
if ($keyCount <= 3) {
|
||||
$keyString = implode('·', $contextKeys);
|
||||
} else {
|
||||
$keyString = implode('·', array_slice($contextKeys, 0, 2)) . "·+{$keyCount}";
|
||||
}
|
||||
$parts[] = "{$gray}📊 {$keyString}{$reset}";
|
||||
}
|
||||
}
|
||||
|
||||
return empty($parts) ? '' : "\n " . implode(' ', $parts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Logging\FormattableHandler;
|
||||
use App\Framework\Logging\Formatter\JsonFormatter;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\Formatter\LogFormatter;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
use App\Framework\Logging\Security\SensitiveDataRedactor;
|
||||
@@ -33,7 +34,7 @@ use App\Framework\Logging\Security\SensitiveDataRedactor;
|
||||
* - docker logs <container> 2>&1 | jq 'select(.level == "ERROR")'
|
||||
* - docker logs <container> 2>&1 | jq -r '[.timestamp, .level, .message] | @tsv'
|
||||
*/
|
||||
final readonly class DockerJsonHandler implements LogHandler
|
||||
final readonly class DockerJsonHandler implements FormattableHandler
|
||||
{
|
||||
private JsonFormatter $formatter;
|
||||
private LogLevel $minLevel;
|
||||
@@ -79,6 +80,14 @@ final readonly class DockerJsonHandler implements LogHandler
|
||||
echo $json . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Formatter zurück
|
||||
*/
|
||||
public function getFormatter(): LogFormatter
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt minimales Log-Level
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Logging\Formatter\LogFormatter;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
@@ -26,9 +27,9 @@ class FileHandler implements LogHandler
|
||||
private string $logFile;
|
||||
|
||||
/**
|
||||
* @var string Format für die Ausgabe
|
||||
* @var LogFormatter Formatter für die Log-Ausgabe
|
||||
*/
|
||||
private string $outputFormat;
|
||||
private LogFormatter $formatter;
|
||||
|
||||
/**
|
||||
* @var int Datei-Modi für die Log-Datei
|
||||
@@ -48,22 +49,23 @@ class FileHandler implements LogHandler
|
||||
/**
|
||||
* Erstellt einen neuen FileHandler
|
||||
*
|
||||
* @param LogFormatter $formatter Formatter für die Log-Ausgabe
|
||||
* @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
|
||||
* @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(
|
||||
LogFormatter $formatter,
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
string $outputFormat = '[{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
int $fileMode = 0644,
|
||||
?LogRotator $rotator = null,
|
||||
?PathProvider $pathProvider = null
|
||||
) {
|
||||
$this->pathProvider = $pathProvider;
|
||||
$this->formatter = $formatter;
|
||||
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
@@ -72,7 +74,6 @@ class FileHandler implements LogHandler
|
||||
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
$this->outputFormat = $outputFormat;
|
||||
$this->fileMode = $fileMode;
|
||||
$this->rotator = $rotator;
|
||||
|
||||
@@ -93,27 +94,13 @@ class FileHandler implements LogHandler
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
// Request-ID-Teil erstellen, falls vorhanden
|
||||
$requestId = $record->hasExtra('request_id')
|
||||
? "[{$record->getExtra('request_id')}] "
|
||||
: '';
|
||||
// Formatter verwenden für Formatierung
|
||||
$formatted = ($this->formatter)($record);
|
||||
|
||||
// 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;
|
||||
// Formatter kann string oder array zurückgeben
|
||||
$output = is_string($formatted)
|
||||
? $formatted . PHP_EOL
|
||||
: json_encode($formatted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
|
||||
// Prüfe Rotation vor dem Schreiben
|
||||
if ($this->rotator !== null) {
|
||||
@@ -129,6 +116,14 @@ class FileHandler implements LogHandler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Formatter zurück
|
||||
*/
|
||||
public function getFormatter(): LogFormatter
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt einen String in die Log-Datei
|
||||
*/
|
||||
@@ -167,15 +162,6 @@ class FileHandler implements LogHandler
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabeformat setzen
|
||||
*/
|
||||
public function setOutputFormat(string $format): self
|
||||
{
|
||||
$this->outputFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Datei setzen
|
||||
|
||||
@@ -6,8 +6,9 @@ namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Filesystem\Serializers\JsonSerializer;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\Formatter\JsonFormatter;
|
||||
use App\Framework\Logging\Formatter\LogFormatter;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
@@ -15,7 +16,7 @@ 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.
|
||||
*
|
||||
* Nutzt JsonSerializer für einheitliche JSON-Ausgabe konsistent mit JsonFormatter.
|
||||
* Nutzt JsonFormatter für einheitliche JSON-Ausgabe.
|
||||
*
|
||||
* Standard-Felder für Log-Aggregatoren:
|
||||
* - @timestamp: Elasticsearch-konformes Zeitstempelfeld
|
||||
@@ -37,62 +38,31 @@ class JsonFileHandler implements LogHandler
|
||||
private string $logFile;
|
||||
|
||||
/**
|
||||
* @var array Liste der Felder, die in der JSON-Ausgabe enthalten sein sollen
|
||||
* @var JsonFormatter Formatter für die JSON-Ausgabe
|
||||
*/
|
||||
private array $includedFields;
|
||||
private JsonFormatter $formatter;
|
||||
|
||||
/**
|
||||
* @var PathProvider|null PathProvider für die Auflösung von Pfaden
|
||||
*/
|
||||
private ?PathProvider $pathProvider = null;
|
||||
|
||||
/**
|
||||
* @var JsonSerializer JSON Serializer für konsistente Ausgabe
|
||||
*/
|
||||
private JsonSerializer $serializer;
|
||||
|
||||
/**
|
||||
* @var bool Flatten LogContext für bessere Aggregator-Kompatibilität
|
||||
*/
|
||||
private bool $flattenContext;
|
||||
|
||||
/**
|
||||
* @var string Deployment-Umgebung (production, staging, development)
|
||||
*/
|
||||
private string $environment;
|
||||
|
||||
/**
|
||||
* @var string Server-Hostname
|
||||
*/
|
||||
private string $host;
|
||||
|
||||
/**
|
||||
* @var string Service-/Anwendungsname
|
||||
*/
|
||||
private string $serviceName;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen JsonFileHandler
|
||||
*
|
||||
* @param JsonFormatter $formatter Formatter für die JSON-Ausgabe
|
||||
* @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
|
||||
* @param bool $flattenContext Flatten LogContext structured data (default: true)
|
||||
* @param Environment|null $env Optional: Environment für Konfiguration
|
||||
* @param string|null $serviceName Optional: Service-/Anwendungsname
|
||||
*/
|
||||
public function __construct(
|
||||
JsonFormatter $formatter,
|
||||
string $logFile,
|
||||
LogLevel|int $minLevel = LogLevel::INFO,
|
||||
?array $includedFields = null,
|
||||
?PathProvider $pathProvider = null,
|
||||
bool $flattenContext = true,
|
||||
?Environment $env = null,
|
||||
?string $serviceName = null
|
||||
?PathProvider $pathProvider = null
|
||||
) {
|
||||
$this->pathProvider = $pathProvider;
|
||||
$this->flattenContext = $flattenContext;
|
||||
$this->formatter = $formatter;
|
||||
|
||||
// Pfad auflösen, falls PathProvider vorhanden
|
||||
if ($this->pathProvider !== null && ! str_starts_with($logFile, '/')) {
|
||||
@@ -102,34 +72,6 @@ class JsonFileHandler implements LogHandler
|
||||
$this->logFile = $logFile;
|
||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||
|
||||
// Standardfelder, falls nicht anders angegeben (konsistent mit JsonFormatter)
|
||||
$this->includedFields = $includedFields ?? [
|
||||
'timestamp',
|
||||
'@timestamp',
|
||||
'level',
|
||||
'level_value',
|
||||
'severity',
|
||||
'channel',
|
||||
'message',
|
||||
'environment',
|
||||
'host',
|
||||
'service',
|
||||
'context',
|
||||
'extra',
|
||||
];
|
||||
|
||||
// Compact JSON für Datei-Ausgabe (eine Zeile pro Log)
|
||||
$this->serializer = JsonSerializer::compact();
|
||||
|
||||
// Environment Detection (production, staging, development)
|
||||
$this->environment = $env?->getString('APP_ENV', 'production') ?? 'production';
|
||||
|
||||
// Host Detection
|
||||
$this->host = gethostname() ?: 'unknown';
|
||||
|
||||
// Service Name (default: app name from env)
|
||||
$this->serviceName = $serviceName ?? $env?->getString('APP_NAME', 'app') ?? 'app';
|
||||
|
||||
// Stelle sicher, dass das Verzeichnis existiert
|
||||
$this->ensureDirectoryExists(dirname($logFile));
|
||||
}
|
||||
@@ -147,81 +89,26 @@ class JsonFileHandler implements LogHandler
|
||||
*/
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
// Formatiere Record zu einheitlichem Array (konsistent mit JsonFormatter)
|
||||
$data = $this->formatRecord($record);
|
||||
// Formatter verwenden für JSON-Formatierung
|
||||
$json = ($this->formatter)($record);
|
||||
|
||||
// Als JSON formatieren mit JsonSerializer und in die Datei schreiben
|
||||
$json = $this->serializer->serialize($data) . PHP_EOL;
|
||||
$this->write($json);
|
||||
// JsonFormatter gibt immer string zurück
|
||||
$output = is_string($json) ? $json : json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// Stelle sicher, dass ein Newline vorhanden ist
|
||||
if (!str_ends_with($output, PHP_EOL)) {
|
||||
$output .= PHP_EOL;
|
||||
}
|
||||
|
||||
$this->write($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert LogRecord zu einheitlichem Array für JSON-Serialisierung
|
||||
* (Gleiche Logik wie JsonFormatter für Konsistenz)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
* Gibt den Formatter zurück
|
||||
*/
|
||||
private function formatRecord(LogRecord $record): array
|
||||
public function getFormatter(): LogFormatter
|
||||
{
|
||||
$timestamp = $record->timestamp->format('c'); // ISO 8601
|
||||
|
||||
$data = [
|
||||
// Standard Log Fields
|
||||
'timestamp' => $timestamp,
|
||||
'@timestamp' => $timestamp, // Elasticsearch convention
|
||||
'level' => $record->level->getName(),
|
||||
'level_value' => $record->level->value,
|
||||
'severity' => $record->level->toRFC5424(), // RFC 5424 (0-7)
|
||||
'channel' => $record->channel,
|
||||
'message' => $record->message,
|
||||
|
||||
// Infrastructure Fields (for log aggregators)
|
||||
'environment' => $this->environment,
|
||||
'host' => $this->host,
|
||||
'service' => $this->serviceName,
|
||||
];
|
||||
|
||||
// Context hinzufügen mit optionalem Flatten
|
||||
$context = $record->context->toArray();
|
||||
|
||||
if ($this->flattenContext && isset($context['structured'])) {
|
||||
// Flatten: Nur strukturierte Daten für bessere Aggregator-Kompatibilität
|
||||
$data['context'] = $context['structured'];
|
||||
} else {
|
||||
// Raw: Gesamtes LogContext-Array
|
||||
$data['context'] = $context;
|
||||
}
|
||||
|
||||
// Extras hinzufügen (wenn vorhanden)
|
||||
if (!empty($record->extra)) {
|
||||
$data['extra'] = $record->extra;
|
||||
}
|
||||
|
||||
// Nur gewünschte Felder behalten, in Reihenfolge von $includedFields
|
||||
return $this->filterFields($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtert Array nach includedFields
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function filterFields(array $data): array
|
||||
{
|
||||
if (empty($this->includedFields)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
|
||||
foreach ($this->includedFields as $field) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$filtered[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,15 +139,6 @@ class JsonFileHandler implements LogHandler
|
||||
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
|
||||
|
||||
@@ -5,9 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Logging\Handlers;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\Formatter\LogFormatter;
|
||||
use App\Framework\Logging\LogChannel;
|
||||
use App\Framework\Logging\LogConfig;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\LogRecord;
|
||||
|
||||
@@ -24,13 +25,19 @@ final class MultiFileHandler implements LogHandler
|
||||
*/
|
||||
private array $fileHandles = [];
|
||||
|
||||
/**
|
||||
* @var LogFormatter Formatter für die Log-Ausgabe
|
||||
*/
|
||||
private LogFormatter $formatter;
|
||||
|
||||
public function __construct(
|
||||
private readonly mixed $logConfig,
|
||||
private readonly mixed $pathProvider,
|
||||
private readonly LogConfig $logConfig,
|
||||
private readonly PathProvider $pathProvider,
|
||||
LogFormatter $formatter,
|
||||
private readonly LogLevel $minLevel = LogLevel::DEBUG,
|
||||
private readonly string $outputFormat = '[{timestamp}] [{level_name}] [{channel}] {message}',
|
||||
private readonly int $fileMode = 0644
|
||||
) {
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,13 +63,31 @@ final class MultiFileHandler implements LogHandler
|
||||
// Entsprechende Log-Datei ermitteln
|
||||
$logFile = $this->getLogFileForChannel($channel);
|
||||
|
||||
// Log-Nachricht formatieren
|
||||
$formattedMessage = $this->formatMessage($record);
|
||||
// Formatter verwenden für Formatierung
|
||||
$formatted = ($this->formatter)($record);
|
||||
|
||||
// Formatter kann string oder array zurückgeben
|
||||
$formattedMessage = is_string($formatted)
|
||||
? $formatted
|
||||
: json_encode($formatted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// Stelle sicher, dass ein Newline vorhanden ist
|
||||
if (!str_ends_with($formattedMessage, PHP_EOL)) {
|
||||
$formattedMessage .= PHP_EOL;
|
||||
}
|
||||
|
||||
// In Datei schreiben
|
||||
$this->writeToFile($logFile, $formattedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Formatter zurück
|
||||
*/
|
||||
public function getFormatter(): LogFormatter
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die Log-Datei für einen Channel
|
||||
*/
|
||||
@@ -81,29 +106,6 @@ final class MultiFileHandler implements LogHandler
|
||||
return $this->logConfig->getLogPath($logPathKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert die Log-Nachricht
|
||||
*/
|
||||
private function formatMessage(LogRecord $record): string
|
||||
{
|
||||
$replacements = [
|
||||
'{timestamp}' => $record->getFormattedTimestamp('Y-m-d H:i:s'),
|
||||
'{level_name}' => $record->getLevel()->getName(),
|
||||
'{channel}' => $record->getChannel() ?? 'app',
|
||||
'{message}' => $record->getMessage(),
|
||||
];
|
||||
|
||||
$formatted = str_replace(array_keys($replacements), array_values($replacements), $this->outputFormat);
|
||||
|
||||
// Context hinzufügen, falls vorhanden
|
||||
$context = $record->getContext();
|
||||
if (! empty($context)) {
|
||||
$formatted .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return $formatted . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt die formatierte Nachricht in eine Datei
|
||||
*/
|
||||
|
||||
@@ -26,8 +26,9 @@ final readonly class QueuedLogHandler implements LogHandler
|
||||
|
||||
public function handle(LogRecord $record): void
|
||||
{
|
||||
$job = new ProcessLogCommand($record);
|
||||
$payload = JobPayload::immediate($job);
|
||||
$this->queue->push($payload);
|
||||
var_dump('whould be queued');
|
||||
#$job = new ProcessLogCommand($record);
|
||||
#$payload = JobPayload::immediate($job);
|
||||
#$this->queue->push($payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Logging;
|
||||
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
|
||||
/**
|
||||
* Factory für Logger-Instanzen.
|
||||
*
|
||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
||||
* Diese Klasse wird nur noch für Legacy-Code verwendet und sollte nicht in neuem Code genutzt werden.
|
||||
*/
|
||||
final class LoggerFactory
|
||||
{
|
||||
@@ -13,17 +19,23 @@ final class LoggerFactory
|
||||
|
||||
/**
|
||||
* Erzeugt einen neuen Logger mit optionalen Einstellungen.
|
||||
*
|
||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
||||
*/
|
||||
public static function create(
|
||||
?Clock $clock = null,
|
||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||
bool $enabled = true,
|
||||
array $handlers = []
|
||||
): DefaultLogger {
|
||||
return new DefaultLogger($minLevel, $enabled, $handlers);
|
||||
$clock ??= new SystemClock();
|
||||
|
||||
return new DefaultLogger($clock, $minLevel, $handlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Standard-Logger zurück oder erstellt ihn, falls er noch nicht existiert.
|
||||
*
|
||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
||||
*/
|
||||
public static function getDefaultLogger(): DefaultLogger
|
||||
{
|
||||
@@ -31,7 +43,7 @@ final class LoggerFactory
|
||||
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
|
||||
$minLevel = $debug ? LogLevel::DEBUG : LogLevel::INFO;
|
||||
|
||||
self::$defaultLogger = self::create($minLevel);
|
||||
self::$defaultLogger = self::create(null, $minLevel);
|
||||
}
|
||||
|
||||
return self::$defaultLogger;
|
||||
@@ -39,6 +51,8 @@ final class LoggerFactory
|
||||
|
||||
/**
|
||||
* Setzt einen benutzerdefinierten Logger als Standard-Logger.
|
||||
*
|
||||
* @deprecated Verwende stattdessen Dependency Injection.
|
||||
*/
|
||||
public static function setDefaultLogger(DefaultLogger $logger): void
|
||||
{
|
||||
|
||||
@@ -8,17 +8,19 @@ use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\EnvKey;
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Logging\Formatter\DevelopmentFormatter;
|
||||
use App\Framework\Logging\Formatter\LineFormatter;
|
||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||
use App\Framework\Logging\Handlers\DockerJsonHandler;
|
||||
use App\Framework\Logging\Handlers\FileHandler;
|
||||
use App\Framework\Logging\Handlers\JsonFileHandler;
|
||||
use App\Framework\Logging\Handlers\MultiFileHandler;
|
||||
use App\Framework\Logging\Handlers\NullHandler;
|
||||
use App\Framework\Logging\Handlers\QueuedLogHandler;
|
||||
use App\Framework\Logging\Handlers\WebHandler;
|
||||
use App\Framework\Queue\FileQueue;
|
||||
use App\Framework\Logging\LogHandler;
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\RedisQueue;
|
||||
use App\Framework\Redis\RedisConfig;
|
||||
@@ -36,18 +38,52 @@ final readonly class LoggerInitializer
|
||||
// MCP Server Mode: Use NullHandler to suppress all output
|
||||
// This prevents log interference with JSON-RPC communication
|
||||
if ($env->get(EnvKey::MCP_SERVER_MODE) === '1') {
|
||||
$contextManager = new LogContextManager();
|
||||
$processorManager = new ProcessorManager();
|
||||
|
||||
return new DefaultLogger(
|
||||
minLevel: LogLevel::DEBUG,
|
||||
handlers: [new NullHandler()],
|
||||
processorManager: $processorManager,
|
||||
contextManager: $contextManager
|
||||
);
|
||||
return $this->createMcpLogger($container);
|
||||
}
|
||||
|
||||
// LogContextManager als Singleton im Container registrieren
|
||||
$this->initializeLogContextManager($config, $container);
|
||||
|
||||
$processorManager = new ProcessorManager();
|
||||
$minLevel = $this->determineMinLogLevel($config);
|
||||
$logConfig = $this->initializeLogConfig($pathProvider);
|
||||
$queue = $this->createQueue($env);
|
||||
$handlers = $this->createHandlers($config, $env, $logConfig, $pathProvider, $minLevel, $queue);
|
||||
$contextManager = $container->get(LogContextManager::class);
|
||||
$clock = $container->get(Clock::class);
|
||||
|
||||
return new DefaultLogger(
|
||||
clock: $clock,
|
||||
minLevel: $minLevel,
|
||||
handlers: $handlers,
|
||||
processorManager: $processorManager,
|
||||
contextManager: $contextManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Logger für MCP Server Mode
|
||||
*/
|
||||
private function createMcpLogger(Container $container): Logger
|
||||
{
|
||||
$contextManager = new LogContextManager();
|
||||
$processorManager = new ProcessorManager();
|
||||
$clock = $container->get(Clock::class);
|
||||
|
||||
return new DefaultLogger(
|
||||
clock: $clock,
|
||||
minLevel: LogLevel::DEBUG,
|
||||
handlers: [new NullHandler()],
|
||||
processorManager: $processorManager,
|
||||
contextManager: $contextManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den LogContextManager im Container
|
||||
*/
|
||||
private function initializeLogContextManager(TypedConfiguration $config, Container $container): void
|
||||
{
|
||||
if (! $container->has(LogContextManager::class)) {
|
||||
$contextManager = new LogContextManager();
|
||||
$container->singleton(LogContextManager::class, $contextManager);
|
||||
@@ -57,89 +93,137 @@ final readonly class LoggerInitializer
|
||||
$contextManager->addGlobalData('environment', $config->app->environment->value);
|
||||
$contextManager->addGlobalTags('application', 'framework');
|
||||
}
|
||||
$processorManager = new ProcessorManager();
|
||||
}
|
||||
|
||||
// Set log level based on environment
|
||||
$minLevel = $config->app->isDebugEnabled()
|
||||
/**
|
||||
* Bestimmt das minimale Log-Level basierend auf der Konfiguration
|
||||
*/
|
||||
private function determineMinLogLevel(TypedConfiguration $config): LogLevel
|
||||
{
|
||||
return $config->app->isDebugEnabled()
|
||||
? LogLevel::DEBUG
|
||||
: LogLevel::INFO;
|
||||
}
|
||||
|
||||
// Erstelle LogConfig für zentrale Pfadverwaltung
|
||||
/**
|
||||
* Initialisiert die Log-Konfiguration und stellt sicher, dass Verzeichnisse existieren
|
||||
*/
|
||||
private function initializeLogConfig(PathProvider $pathProvider): LogConfig
|
||||
{
|
||||
$logConfig = new LogConfig($pathProvider);
|
||||
|
||||
// Stelle sicher, dass alle Logverzeichnisse existieren
|
||||
$logConfig->ensureLogDirectoriesExist();
|
||||
|
||||
return $logConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die Queue für asynchrones Logging
|
||||
*/
|
||||
private function createQueue(Environment $env): Queue
|
||||
{
|
||||
$redisConfig = new RedisConfig(host: $env->getString(EnvKey::REDIS_HOST, 'redis'), database: 2);
|
||||
$redisConnection = new RedisConnection($redisConfig, 'queue');
|
||||
$queue = new RedisQueue($redisConnection, 'commands');
|
||||
|
||||
return new RedisQueue($redisConnection, 'commands');
|
||||
|
||||
// Alternativ: FileQueue mit aufgelöstem Pfad
|
||||
// $queuePath = $pathProvider->resolvePath('storage/queue');
|
||||
// $queue = new FileQueue($queuePath);
|
||||
// return new FileQueue($queuePath);
|
||||
}
|
||||
|
||||
// Handler-Konfiguration basierend auf Umgebung
|
||||
/**
|
||||
* Erstellt alle Handler basierend auf der Umgebung
|
||||
*
|
||||
* @param TypedConfiguration $config
|
||||
* @param Environment $env
|
||||
* @param LogConfig $logConfig
|
||||
* @param PathProvider $pathProvider
|
||||
* @param LogLevel $minLevel
|
||||
* @param Queue $queue
|
||||
* @return array<LogHandler>
|
||||
*/
|
||||
private function createHandlers(
|
||||
TypedConfiguration $config,
|
||||
Environment $env,
|
||||
LogConfig $logConfig,
|
||||
PathProvider $pathProvider,
|
||||
LogLevel $minLevel,
|
||||
Queue $queue
|
||||
): array {
|
||||
$handlers = [];
|
||||
|
||||
// Docker/Console Logging Handler
|
||||
if (PHP_SAPI === 'cli') {
|
||||
// Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs)
|
||||
$inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true';
|
||||
|
||||
if ($inDocker) {
|
||||
if ($config->app->isProduction()) {
|
||||
// Production Docker: Compact JSON für Log-Aggregatoren mit Redaction
|
||||
$handlers[] = new DockerJsonHandler(
|
||||
env: $env,
|
||||
minLevel: $minLevel,
|
||||
redactSensitiveData: true // Auto-redact in Production
|
||||
);
|
||||
} else {
|
||||
// Development Docker: Pretty JSON für bessere Lesbarkeit
|
||||
$handlers[] = new DockerJsonHandler(
|
||||
env: $env,
|
||||
serviceName: $config->app->name ?? 'app',
|
||||
minLevel: $minLevel,
|
||||
prettyPrint: true, // Pretty-print für Development
|
||||
redactSensitiveData: false // Keine Redaction in Development für Debugging
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Lokale Entwicklung: Farbige Console-Logs
|
||||
$handlers[] = new ConsoleHandler();
|
||||
}
|
||||
$handlers[] = $this->createCliHandler($config, $env, $minLevel);
|
||||
}
|
||||
|
||||
$handlers[] = new QueuedLogHandler($queue);
|
||||
//$handlers[] = new QueuedLogHandler($queue);
|
||||
$handlers[] = new WebHandler();
|
||||
|
||||
// MultiFileHandler für automatisches Channel-Routing
|
||||
$multiFileFormatter = new LineFormatter();
|
||||
|
||||
$handlers[] = new MultiFileHandler(
|
||||
$logConfig,
|
||||
$pathProvider,
|
||||
$multiFileFormatter,
|
||||
$minLevel,
|
||||
'[{timestamp}] [{level_name}] [{channel}] {message}',
|
||||
0644
|
||||
);
|
||||
|
||||
// Fallback FileHandler für Kompatibilität (nur für 'app' Channel ohne Channel-Info)
|
||||
$fileFormatter = new LineFormatter(
|
||||
format: 'Line Formatter: [{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
timestampFormat: 'Y-m-d H:i:s'
|
||||
);
|
||||
$handlers[] = new FileHandler(
|
||||
$fileFormatter,
|
||||
$logConfig->getLogPath('app'),
|
||||
$minLevel,
|
||||
'[{timestamp}] [{level_name}] {request_id}{channel}{message}',
|
||||
0644,
|
||||
null,
|
||||
$pathProvider
|
||||
);
|
||||
|
||||
// LogContextManager aus Container holen
|
||||
$contextManager = $container->get(LogContextManager::class);
|
||||
return $handlers;
|
||||
}
|
||||
|
||||
return new DefaultLogger(
|
||||
minLevel: $minLevel,
|
||||
handlers: $handlers,
|
||||
processorManager: $processorManager,
|
||||
contextManager: $contextManager,
|
||||
);
|
||||
/**
|
||||
* Erstellt den CLI-Handler (Docker JSON oder Console)
|
||||
*/
|
||||
private function createCliHandler(
|
||||
TypedConfiguration $config,
|
||||
Environment $env,
|
||||
LogLevel $minLevel
|
||||
): LogHandler {
|
||||
// Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs)
|
||||
$inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true';
|
||||
|
||||
if ($inDocker) {
|
||||
if ($config->app->isProduction()) {
|
||||
// Production Docker: Compact JSON für Log-Aggregatoren mit Redaction
|
||||
return new DockerJsonHandler(
|
||||
env: $env,
|
||||
minLevel: $minLevel,
|
||||
redactSensitiveData: true // Auto-redact in Production
|
||||
);
|
||||
} else {
|
||||
// Development Docker: Pretty JSON für bessere Lesbarkeit
|
||||
return new DockerJsonHandler(
|
||||
env: $env,
|
||||
serviceName: $config->app->name ?? 'app',
|
||||
minLevel: $minLevel,
|
||||
prettyPrint: true, // Pretty-print für Development
|
||||
redactSensitiveData: false // Keine Redaction in Development für Debugging
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Lokale Entwicklung: Farbige Console-Logs
|
||||
$consoleFormatter = new DevelopmentFormatter(
|
||||
includeStackTrace: true,
|
||||
colorOutput: true
|
||||
);
|
||||
return new ConsoleHandler($consoleFormatter, $minLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,20 +37,6 @@ final readonly class ProcessorManager
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert eine Liste von Processors
|
||||
*/
|
||||
|
||||
@@ -28,16 +28,91 @@ final readonly class StackFrame implements \JsonSerializable
|
||||
*/
|
||||
public static function fromArray(array $frame): self
|
||||
{
|
||||
// Bereinige Args, um nicht-serialisierbare Objekte zu entfernen
|
||||
$args = isset($frame['args']) ? self::sanitizeArgs($frame['args']) : [];
|
||||
|
||||
return new self(
|
||||
file: $frame['file'] ?? 'unknown',
|
||||
line: $frame['line'] ?? 0,
|
||||
function: $frame['function'] ?? null,
|
||||
class: $frame['class'] ?? null,
|
||||
type: $frame['type'] ?? null,
|
||||
args: $frame['args'] ?? []
|
||||
args: $args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt Args, um nicht-serialisierbare Objekte (wie ReflectionClass) zu entfernen
|
||||
*
|
||||
* @param array<int, mixed> $args
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private static function sanitizeArgs(array $args): array
|
||||
{
|
||||
return array_map(
|
||||
fn($arg) => self::sanitizeValue($arg),
|
||||
$args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt einzelnen Wert, entfernt nicht-serialisierbare Objekte
|
||||
*/
|
||||
private static function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
// Closures können nicht serialisiert werden
|
||||
if ($value instanceof \Closure) {
|
||||
// Versuche ReflectionFunction zu verwenden, um Closure-Details zu bekommen
|
||||
try {
|
||||
$reflection = new \ReflectionFunction($value);
|
||||
$file = $reflection->getFileName();
|
||||
$line = $reflection->getStartLine();
|
||||
return sprintf('Closure(%s:%d)', basename($file), $line);
|
||||
} catch (\Throwable) {
|
||||
return 'Closure';
|
||||
}
|
||||
}
|
||||
|
||||
// Reflection-Objekte können nicht serialisiert werden
|
||||
if (is_object($value)) {
|
||||
$className = get_class($value);
|
||||
if ($value instanceof \ReflectionClass
|
||||
|| $value instanceof \ReflectionMethod
|
||||
|| $value instanceof \ReflectionProperty
|
||||
|| $value instanceof \ReflectionFunction
|
||||
|| $value instanceof \ReflectionParameter
|
||||
|| $value instanceof \ReflectionType
|
||||
|| str_starts_with($className, 'Reflection')) {
|
||||
// Ersetze durch String-Repräsentation
|
||||
return sprintf('ReflectionObject(%s)', $className);
|
||||
}
|
||||
|
||||
// Anonyme Klassen können auch Probleme verursachen
|
||||
if (str_contains($className, '@anonymous')) {
|
||||
// Versuche den Parent-Type zu extrahieren
|
||||
$parentClass = get_parent_class($value);
|
||||
if ($parentClass !== false) {
|
||||
return sprintf('Anonymous(%s)', $parentClass);
|
||||
}
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
// Andere Objekte durch Klassenname ersetzen
|
||||
return $className;
|
||||
}
|
||||
|
||||
// Arrays rekursiv bereinigen
|
||||
if (is_array($value)) {
|
||||
return array_map(
|
||||
fn($item) => self::sanitizeValue($item),
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
// Primitives bleiben unverändert
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Kurzform des File-Pfads zurück (relativ zum Project Root wenn möglich)
|
||||
*/
|
||||
@@ -131,24 +206,26 @@ final readonly class StackFrame implements \JsonSerializable
|
||||
|
||||
/**
|
||||
* Serialisiert Arguments für Log-Ausgabe
|
||||
*
|
||||
* Note: Args sind bereits beim Erstellen bereinigt, aber für toArray()
|
||||
* formatieren wir sie nochmal kompakter.
|
||||
*
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function serializeArgs(): array
|
||||
{
|
||||
return array_map(
|
||||
fn($arg) => $this->serializeValue($arg),
|
||||
fn($arg) => $this->formatValueForOutput($arg),
|
||||
$this->args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialisiert einzelnen Wert (verhindert zu große Ausgaben)
|
||||
* Formatiert Wert für Log-Ausgabe (kompaktere Darstellung)
|
||||
*/
|
||||
private function serializeValue(mixed $value): mixed
|
||||
private function formatValueForOutput(mixed $value): mixed
|
||||
{
|
||||
return match (true) {
|
||||
is_object($value) => get_class($value),
|
||||
is_array($value) => sprintf('array(%d)', count($value)),
|
||||
is_resource($value) => sprintf('resource(%s)', get_resource_type($value)),
|
||||
is_string($value) && strlen($value) > 100 => substr($value, 0, 100) . '...',
|
||||
|
||||
Reference in New Issue
Block a user