Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
289
src/Framework/ErrorAggregation/ErrorEvent.php
Normal file
289
src/Framework/ErrorAggregation/ErrorEvent.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorAggregation;
|
||||
|
||||
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): self
|
||||
{
|
||||
return new self(
|
||||
id: Ulid::generate(),
|
||||
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' => $this->id->toString(),
|
||||
'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 (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 from exception metadata
|
||||
if (isset($context->exception->metadata['error_code'])) {
|
||||
return ErrorCode::from($context->exception->metadata['error_code']);
|
||||
}
|
||||
|
||||
// Try to get from exception data
|
||||
if (isset($context->exception->data['error_code'])) {
|
||||
return ErrorCode::from($context->exception->data['error_code']);
|
||||
}
|
||||
|
||||
// Fallback based on HTTP status
|
||||
$httpStatus = $context->metadata['http_status'] ?? 500;
|
||||
|
||||
return match (true) {
|
||||
$httpStatus >= 500 => ErrorCode::SYSTEM_RESOURCE_EXHAUSTED,
|
||||
$httpStatus === 404 => ErrorCode::HTTP_NOT_FOUND,
|
||||
$httpStatus === 401 => ErrorCode::AUTH_CREDENTIALS_INVALID,
|
||||
$httpStatus === 403 => ErrorCode::AUTH_INSUFFICIENT_PRIVILEGES,
|
||||
$httpStatus === 429 => ErrorCode::HTTP_RATE_LIMIT_EXCEEDED,
|
||||
default => ErrorCode::SYSTEM_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
|
||||
if (isset($context->exception->data['exception_message'])) {
|
||||
return $context->exception->data['exception_message'];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user