minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel); $this->debugOnly = $debugOnly; $this->outputFormat = $outputFormat; } /** * Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll */ public function isHandling(LogRecord $record): bool { // Nur im CLI-Modus aktiv - NIE bei Web-Requests! if (PHP_SAPI !== 'cli') { return false; } // Optional: Debug-Modus-Check nur in CLI if ($this->debugOnly && ! filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) { return false; } return $record->getLevel()->value >= $this->minLevel->value; } /** * Verarbeitet einen Log-Eintrag */ public function handle(LogRecord $record): void { $logLevel = $record->getLevel(); $color = $logLevel->getConsoleColor()->value; $reset = ConsoleColor::RESET->value; // Request-ID-Teil erstellen, falls vorhanden $requestId = $record->hasExtra('request_id') ? "[{$record->getExtra('request_id')}] " : ''; // 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; // Fehler und Warnungen auf stderr, alles andere auf stdout if ($record->getLevel()->value >= $this->stderrLevel->value) { // WARNING, ERROR, CRITICAL, ALERT, EMERGENCY -> stderr file_put_contents('php://stderr', $output); } else { // DEBUG, INFO, NOTICE -> stdout echo $output; } } /** * Minimales Log-Level setzen */ public function setMinLevel(LogLevel|int $level): self { $this->minLevel = $level instanceof LogLevel ? $level : LogLevel::fromValue($level); return $this; } /** * Ausgabeformat setzen */ public function setOutputFormat(string $format): self { $this->outputFormat = $format; return $this; } /** * 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); } }