- 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.
150 lines
4.8 KiB
PHP
150 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Logging\Formatter;
|
|
|
|
use App\Framework\Config\Environment;
|
|
use App\Framework\Filesystem\Serializers\JsonSerializer;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\Security\SensitiveDataRedactor;
|
|
|
|
/**
|
|
* JSON Formatter für strukturierte Logs
|
|
*
|
|
* Nutzt JsonSerializer für einheitliche JSON-Ausgabe.
|
|
* Produziert konsistente Struktur für Log-Aggregatoren (Elasticsearch, Datadog, etc.)
|
|
*
|
|
* Standard-Felder für Log-Aggregatoren:
|
|
* - @timestamp: Elasticsearch-konformes Zeitstempelfeld
|
|
* - severity: RFC 5424 Severity Level (0-7)
|
|
* - environment: Deployment-Umgebung (production, staging, development)
|
|
* - host: Server-Hostname
|
|
* - service: Service-/Anwendungsname
|
|
*/
|
|
final readonly class JsonFormatter implements LogFormatter
|
|
{
|
|
private JsonSerializer $serializer;
|
|
private string $environment;
|
|
private string $host;
|
|
private string $serviceName;
|
|
private ?SensitiveDataRedactor $redactor;
|
|
|
|
public function __construct(
|
|
private bool $prettyPrint = false,
|
|
private bool $includeExtras = true,
|
|
private bool $flattenContext = true,
|
|
?Environment $env = null,
|
|
?string $serviceName = null,
|
|
private bool $redactSensitiveData = false,
|
|
?SensitiveDataRedactor $redactor = null
|
|
) {
|
|
$this->serializer = $prettyPrint
|
|
? JsonSerializer::pretty()
|
|
: 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';
|
|
|
|
// Redactor Setup: Auto-select based on environment if not provided
|
|
if ($redactSensitiveData && $redactor === null) {
|
|
$this->redactor = match ($this->environment) {
|
|
'production' => SensitiveDataRedactor::production(),
|
|
'testing' => SensitiveDataRedactor::testing(),
|
|
default => SensitiveDataRedactor::development(),
|
|
};
|
|
} else {
|
|
$this->redactor = $redactor;
|
|
}
|
|
}
|
|
|
|
public function __invoke(LogRecord $record): string
|
|
{
|
|
$data = $this->formatRecord($record);
|
|
|
|
return $this->serializer->serialize($data);
|
|
}
|
|
|
|
/**
|
|
* Formatiert LogRecord zu einheitlichem Array für JSON-Serialisierung
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function formatRecord(LogRecord $record): array
|
|
{
|
|
$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' => $this->redactMessage($record->message),
|
|
|
|
// Infrastructure Fields (for log aggregators)
|
|
'environment' => $this->environment,
|
|
'host' => $this->host,
|
|
'service' => $this->serviceName,
|
|
];
|
|
|
|
// Correlation ID (top-level für bessere Filterbarkeit)
|
|
if ($record->context->hasCorrelationId()) {
|
|
$data['correlation.id'] = $record->context->getCorrelationId()->toString();
|
|
}
|
|
|
|
// 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'] = $this->redactData($context['structured']);
|
|
} else {
|
|
// Raw: Gesamtes LogContext-Array
|
|
$data['context'] = $this->redactData($context);
|
|
}
|
|
|
|
// Extras hinzufügen (wenn aktiviert und vorhanden)
|
|
if ($this->includeExtras && !empty($record->extra)) {
|
|
$data['extra'] = $this->redactData($record->extra);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Redacted Message falls Redactor aktiviert
|
|
*/
|
|
private function redactMessage(string $message): string
|
|
{
|
|
if ($this->redactor === null) {
|
|
return $message;
|
|
}
|
|
|
|
return $this->redactor->redactString($message);
|
|
}
|
|
|
|
/**
|
|
* Redacted Array-Daten falls Redactor aktiviert
|
|
*
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function redactData(array $data): array
|
|
{
|
|
if ($this->redactor === null) {
|
|
return $data;
|
|
}
|
|
|
|
return $this->redactor->redact($data);
|
|
}
|
|
}
|