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:
2025-11-02 01:37:49 +01:00
parent 2defdf2baf
commit cf0ad6e905
23 changed files with 612 additions and 556 deletions

View File

@@ -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);
}
}

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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);
}
}