- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
218 lines
7.3 KiB
PHP
218 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Logging\Handlers;
|
|
|
|
use App\Framework\Console\ConsoleColor;
|
|
use App\Framework\Logging\LogHandler;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
|
|
/**
|
|
* Handler für die Ausgabe von Log-Einträgen in der Konsole.
|
|
*/
|
|
class ConsoleHandler implements LogHandler
|
|
{
|
|
/**
|
|
* @var LogLevel Minimales Level, ab dem dieser Handler aktiv wird
|
|
*/
|
|
private LogLevel $minLevel;
|
|
|
|
/**
|
|
* @var bool Ob der Handler nur im Debug-Modus aktiv ist
|
|
*/
|
|
private bool $debugOnly;
|
|
|
|
/**
|
|
* @var string Format für die Ausgabe
|
|
*/
|
|
private string $outputFormat;
|
|
|
|
/**
|
|
* Erstellt einen neuen ConsoleHandler
|
|
*
|
|
* @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
|
|
* @param bool $debugOnly Ob der Handler nur im Debug-Modus aktiv ist
|
|
* @param string $outputFormat Format für die Ausgabe
|
|
*/
|
|
public function __construct(
|
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
bool $debugOnly = true,
|
|
string $outputFormat = '{color}[{level_name}]{reset} {timestamp} {request_id}{message}{structured}',
|
|
private readonly LogLevel $stderrLevel = LogLevel::WARNING,
|
|
) {
|
|
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
|
$this->debugOnly = $debugOnly;
|
|
$this->outputFormat = $outputFormat;
|
|
}
|
|
|
|
/**
|
|
* Überprüft, ob dieser Handler den Log-Eintrag verarbeiten soll
|
|
*/
|
|
public function isHandling(LogRecord $record): bool
|
|
{
|
|
// Nur im CLI-Modus aktiv - NIE bei Web-Requests!
|
|
if (PHP_SAPI !== 'cli') {
|
|
return false;
|
|
}
|
|
|
|
// Optional: Debug-Modus-Check nur in CLI
|
|
if ($this->debugOnly && ! filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN)) {
|
|
return false;
|
|
}
|
|
|
|
|
|
return $record->getLevel()->value >= $this->minLevel->value;
|
|
}
|
|
|
|
/**
|
|
* Verarbeitet einen Log-Eintrag
|
|
*/
|
|
public function handle(LogRecord $record): void
|
|
{
|
|
$logLevel = $record->getLevel();
|
|
$color = $logLevel->getConsoleColor()->value;
|
|
$reset = ConsoleColor::RESET->value;
|
|
|
|
// Request-ID-Teil erstellen, falls vorhanden
|
|
$requestId = $record->hasExtra('request_id')
|
|
? "[{$record->getExtra('request_id')}] "
|
|
: '';
|
|
|
|
// 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);
|
|
}
|
|
}
|