Files
michaelschiemer/src/Framework/ErrorAggregation/ErrorEvent.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

302 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\ErrorAggregation;
use App\Framework\Exception\Core\ErrorSeverity;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Ulid\Ulid;
/**
* Represents a single error event for aggregation and analysis
*/
final readonly class ErrorEvent
{
public function __construct(
public Ulid $id,
public string $service,
public string $component,
public string $operation,
public ErrorCode $errorCode,
public string $errorMessage,
public ErrorSeverity $severity,
public \DateTimeImmutable $occurredAt,
public array $context = [],
public array $metadata = [],
public ?string $requestId = null,
public ?string $userId = null,
public ?string $clientIp = null,
public bool $isSecurityEvent = false,
public ?string $stackTrace = null,
public ?string $userAgent = null,
) {
}
/**
* Creates ErrorEvent from ErrorHandlerContext
*/
public static function fromErrorHandlerContext(ErrorHandlerContext $context, \App\Framework\DateTime\Clock $clock): self
{
return new self(
id: new Ulid($clock),
service: self::extractServiceName($context),
component: $context->exception->component ?? 'unknown',
operation: $context->exception->operation ?? 'unknown',
errorCode: self::extractErrorCode($context),
errorMessage: self::extractUserMessage($context),
severity: self::determineSeverity($context),
occurredAt: new \DateTimeImmutable(),
context: $context->exception->data,
metadata: $context->exception->metadata,
requestId: $context->request->requestId,
userId: $context->request->userId ?? null,
clientIp: $context->request->clientIp,
isSecurityEvent: $context->exception->metadata['security_event'] ?? false,
stackTrace: self::extractStackTrace($context),
userAgent: $context->request->userAgent,
);
}
/**
* Converts to array for storage/transmission
*/
public function toArray(): array
{
return [
'id' => (string) $this->id,
'service' => $this->service,
'component' => $this->component,
'operation' => $this->operation,
'error_code' => $this->errorCode->value,
'error_message' => $this->errorMessage,
'severity' => $this->severity->value,
'occurred_at' => $this->occurredAt->format('c'),
'context' => $this->context,
'metadata' => $this->metadata,
'request_id' => $this->requestId,
'user_id' => $this->userId,
'client_ip' => $this->clientIp,
'is_security_event' => $this->isSecurityEvent,
'stack_trace' => $this->stackTrace,
'user_agent' => $this->userAgent,
];
}
/**
* Creates from array (for deserialization)
*/
public static function fromArray(array $data): self
{
return new self(
id: Ulid::fromString($data['id']),
service: $data['service'],
component: $data['component'],
operation: $data['operation'],
errorCode: ErrorCode::from($data['error_code']),
errorMessage: $data['error_message'],
severity: ErrorSeverity::from($data['severity']),
occurredAt: new \DateTimeImmutable($data['occurred_at']),
context: $data['context'] ?? [],
metadata: $data['metadata'] ?? [],
requestId: $data['request_id'],
userId: $data['user_id'],
clientIp: $data['client_ip'],
isSecurityEvent: $data['is_security_event'] ?? false,
stackTrace: $data['stack_trace'],
userAgent: $data['user_agent'],
);
}
/**
* Gets fingerprint for grouping similar errors
*/
public function getFingerprint(): string
{
$components = [
$this->service,
$this->component,
$this->operation,
$this->errorCode->value,
// Normalize error message to group similar errors
$this->normalizeErrorMessage($this->errorMessage),
];
return hash('sha256', implode('|', $components));
}
/**
* Checks if this error should trigger an alert
*/
public function shouldTriggerAlert(): bool
{
// Critical and error severity always trigger alerts
if (in_array($this->severity, [ErrorSeverity::CRITICAL, ErrorSeverity::ERROR])) {
return true;
}
// Security events always trigger alerts
if ($this->isSecurityEvent) {
return true;
}
// Check metadata for explicit alert requirement
return $this->metadata['requires_alert'] ?? false;
}
/**
* Gets alert urgency level
*/
public function getAlertUrgency(): AlertUrgency
{
if ($this->severity === ErrorSeverity::CRITICAL || $this->isSecurityEvent) {
return AlertUrgency::URGENT;
}
if ($this->severity === ErrorSeverity::ERROR) {
return AlertUrgency::HIGH;
}
if ($this->severity === ErrorSeverity::WARNING) {
return AlertUrgency::MEDIUM;
}
return AlertUrgency::LOW;
}
private static function extractServiceName(ErrorHandlerContext $context): string
{
// Try to extract service from request URI
$uri = $context->request->requestUri;
if ($uri !== null) {
if (str_starts_with($uri, '/api/')) {
return 'api';
}
if (str_starts_with($uri, '/admin/')) {
return 'admin';
}
}
// Extract from component if available
if ($context->exception->component) {
return strtolower($context->exception->component);
}
return 'web';
}
private static function extractErrorCode(ErrorHandlerContext $context): ErrorCode
{
// Try to get ErrorCode from original exception if it's a FrameworkException
if (isset($context->exception->data['original_exception'])) {
$originalException = $context->exception->data['original_exception'];
if ($originalException instanceof \App\Framework\Exception\FrameworkException) {
$errorCode = $originalException->getErrorCode();
if ($errorCode !== null) {
return $errorCode;
}
}
}
// Fallback: Use SystemErrorCode::RESOURCE_EXHAUSTED as generic error
return \App\Framework\Exception\Core\SystemErrorCode::RESOURCE_EXHAUSTED;
}
private static function extractUserMessage(ErrorHandlerContext $context): string
{
// Try user_message first
if (isset($context->exception->data['user_message'])) {
return $context->exception->data['user_message'];
}
// Try exception_message (stored by ErrorHandler)
if (isset($context->exception->data['exception_message'])) {
return $context->exception->data['exception_message'];
}
// Try to get from original_exception if it's a FrameworkException
if (isset($context->exception->data['original_exception'])) {
$originalException = $context->exception->data['original_exception'];
if ($originalException instanceof \App\Framework\Exception\FrameworkException) {
return $originalException->getMessage();
}
if ($originalException instanceof \Throwable) {
return $originalException->getMessage();
}
}
// Fallback to operation and component
$operation = $context->exception->operation ?? 'unknown_operation';
$component = $context->exception->component ?? 'unknown_component';
return "Error in {$component} during {$operation}";
}
private static function determineSeverity(ErrorHandlerContext $context): ErrorSeverity
{
// Security events are always critical
if ($context->exception->metadata['security_event'] ?? false) {
return ErrorSeverity::CRITICAL;
}
// Check explicit severity in metadata
if (isset($context->exception->metadata['severity'])) {
return ErrorSeverity::tryFrom($context->exception->metadata['severity']) ?? ErrorSeverity::ERROR;
}
// Get severity from ErrorCode if available
$errorCode = self::extractErrorCode($context);
if ($errorCode !== null) {
return $errorCode->getSeverity();
}
// Determine from HTTP status
$httpStatus = $context->metadata['http_status'] ?? 500;
return match (true) {
$httpStatus >= 500 => ErrorSeverity::ERROR,
$httpStatus >= 400 => ErrorSeverity::WARNING,
default => ErrorSeverity::INFO,
};
}
private static function extractStackTrace(ErrorHandlerContext $context): ?string
{
// Don't include stack traces for security events in production
if (($context->exception->metadata['security_event'] ?? false) && ! ($_ENV['APP_DEBUG'] ?? false)) {
return null;
}
// Extract from exception debug data
if (isset($context->exception->debug['stack_trace'])) {
return $context->exception->debug['stack_trace'];
}
return null;
}
private function normalizeErrorMessage(string $message): string
{
// Remove specific details to group similar errors
$normalized = $message;
// Remove file paths
$normalized = preg_replace('#/[^\s]+#', '/path/to/file', $normalized);
// Remove specific IDs/numbers
$normalized = preg_replace('#\b\d+\b#', 'N', $normalized);
// Remove timestamps
$normalized = preg_replace('#\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}#', 'TIMESTAMP', $normalized);
// Remove ULIDs/UUIDs
$normalized = preg_replace('#[0-9A-HJ-NP-TV-Z]{26}#', 'ULID', $normalized);
$normalized = preg_replace('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#i', 'UUID', $normalized);
return $normalized;
}
}