BREAKING CHANGE: Requires PHP 8.5.0RC3 Changes: - Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm - Enable ext-uri for native WHATWG URL parsing support - Update composer.json PHP requirement from ^8.4 to ^8.5 - Add ext-uri as required extension in composer.json - Move URL classes from Url.php85/ to Url/ directory (now compatible) - Remove temporary PHP 8.4 compatibility workarounds Benefits: - Native URL parsing with Uri\WhatWg\Url class - Better performance for URL operations - Future-proof with latest PHP features - Eliminates PHP version compatibility issues
354 lines
11 KiB
PHP
354 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\ErrorAggregation;
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\ErrorAggregation\Storage\ErrorStorageInterface;
|
|
use App\Framework\Exception\Core\ErrorSeverity;
|
|
use App\Framework\Exception\ErrorHandlerContext;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Queue\Queue;
|
|
|
|
/**
|
|
* Central error aggregation engine
|
|
* Collects, analyzes, and patterns errors for alerting and monitoring
|
|
*/
|
|
final readonly class ErrorAggregator implements ErrorAggregatorInterface
|
|
{
|
|
private const string PATTERN_CACHE_PREFIX = 'error_pattern:';
|
|
private const int PATTERN_CACHE_TTL = 3600; // 1 hour
|
|
|
|
public function __construct(
|
|
private ErrorStorageInterface $storage,
|
|
private Cache $cache,
|
|
private Clock $clock,
|
|
private Queue $alertQueue,
|
|
private ?Logger $logger = null,
|
|
private int $batchSize = 100,
|
|
private int $maxRetentionDays = 90,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Processes a new error from ErrorHandlerContext
|
|
*/
|
|
public function processError(ErrorHandlerContext $context): void
|
|
{
|
|
try {
|
|
$errorEvent = ErrorEvent::fromErrorHandlerContext($context, $this->clock);
|
|
$this->processErrorEvent($errorEvent);
|
|
} catch (\Throwable $e) {
|
|
// Don't let error aggregation break the application
|
|
$this->logError("Failed to process error: " . $e->getMessage(), [
|
|
'exception' => $e,
|
|
'context' => $context->toArray(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes an ErrorEvent
|
|
*/
|
|
public function processErrorEvent(ErrorEvent $event): void
|
|
{
|
|
// Store the individual error event
|
|
$this->storage->storeEvent($event);
|
|
|
|
// Update or create error pattern
|
|
$pattern = $this->updateErrorPattern($event);
|
|
|
|
// Check if pattern should trigger alert
|
|
if ($pattern->shouldAlert()) {
|
|
$this->queueAlert($pattern, $event);
|
|
}
|
|
|
|
// Log for debugging
|
|
$this->logError("Error processed", [
|
|
'event_id' => (string) $event->id,
|
|
'fingerprint' => $event->getFingerprint(),
|
|
'pattern_id' => (string) $pattern->id,
|
|
'occurrence_count' => $pattern->occurrenceCount,
|
|
'should_alert' => $pattern->shouldAlert(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Updates or creates error pattern for the event
|
|
*/
|
|
private function updateErrorPattern(ErrorEvent $event): ErrorPattern
|
|
{
|
|
$fingerprint = $event->getFingerprint();
|
|
$cacheKey = CacheKey::fromString(self::PATTERN_CACHE_PREFIX . $fingerprint);
|
|
|
|
// Try to get existing pattern from cache
|
|
$cacheResult = $this->cache->get($cacheKey);
|
|
if ($cacheResult->isHit) {
|
|
// Cache returns the pattern array, we need to deserialize it
|
|
$pattern = ErrorPattern::fromArray($cacheResult->value);
|
|
} else {
|
|
// Try to get from storage
|
|
$pattern = $this->storage->getPatternByFingerprint($fingerprint);
|
|
}
|
|
|
|
if ($pattern === null) {
|
|
// Create new pattern
|
|
$pattern = ErrorPattern::fromErrorEvent($event, $this->clock);
|
|
} else {
|
|
// Update existing pattern
|
|
$pattern = $pattern->withNewOccurrence($event);
|
|
}
|
|
|
|
// Store updated pattern
|
|
$this->storage->storePattern($pattern);
|
|
|
|
// Cache the pattern
|
|
$cacheItem = \App\Framework\Cache\CacheItem::forSet(
|
|
$cacheKey,
|
|
$pattern->toArray(),
|
|
Duration::fromSeconds(self::PATTERN_CACHE_TTL)
|
|
);
|
|
$this->cache->set($cacheItem);
|
|
|
|
return $pattern;
|
|
}
|
|
|
|
/**
|
|
* Queues alert for pattern
|
|
*/
|
|
private function queueAlert(ErrorPattern $pattern, ErrorEvent $triggeringEvent): void
|
|
{
|
|
// Create alert job
|
|
$alertJob = new \App\Framework\ErrorAggregation\Jobs\ErrorPatternAlertJob(
|
|
patternId: (string) $pattern->id,
|
|
eventId: (string) $triggeringEvent->id,
|
|
urgency: $pattern->getAlertUrgency(),
|
|
patternData: $pattern->toArray(),
|
|
triggeringEventData: $triggeringEvent->toArray()
|
|
);
|
|
|
|
// Map urgency to queue priority
|
|
$queuePriority = match ($pattern->getAlertUrgency()) {
|
|
AlertUrgency::URGENT => \App\Framework\Queue\ValueObjects\QueuePriority::critical(),
|
|
AlertUrgency::HIGH => \App\Framework\Queue\ValueObjects\QueuePriority::high(),
|
|
AlertUrgency::MEDIUM => \App\Framework\Queue\ValueObjects\QueuePriority::normal(),
|
|
AlertUrgency::LOW => \App\Framework\Queue\ValueObjects\QueuePriority::low(),
|
|
};
|
|
|
|
// Create job payload and push to queue
|
|
$payload = \App\Framework\Queue\ValueObjects\JobPayload::create(
|
|
job: $alertJob,
|
|
priority: $queuePriority
|
|
);
|
|
|
|
$this->alertQueue->push($payload);
|
|
|
|
$this->logError("Alert queued", [
|
|
'pattern_id' => (string) $pattern->id,
|
|
'urgency' => $pattern->getAlertUrgency()->value,
|
|
'occurrence_count' => $pattern->occurrenceCount,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Gets error statistics for a time period
|
|
*/
|
|
public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
|
{
|
|
return $this->storage->getStatistics($from, $to);
|
|
}
|
|
|
|
/**
|
|
* Gets active error patterns
|
|
*/
|
|
public function getActivePatterns(int $limit = 50, int $offset = 0): array
|
|
{
|
|
return $this->storage->getActivePatterns($limit, $offset);
|
|
}
|
|
|
|
/**
|
|
* Gets error patterns by service
|
|
*/
|
|
public function getPatternsByService(string $service, int $limit = 50): array
|
|
{
|
|
return $this->storage->getPatternsByService($service, $limit);
|
|
}
|
|
|
|
/**
|
|
* Gets recent error events
|
|
*/
|
|
public function getRecentEvents(int $limit = 100, ?ErrorSeverity $severity = null): array
|
|
{
|
|
return $this->storage->getRecentEvents($limit, $severity);
|
|
}
|
|
|
|
/**
|
|
* Acknowledges an error pattern
|
|
*/
|
|
public function acknowledgePattern(string $patternId, string $acknowledgedBy, ?string $resolution = null): bool
|
|
{
|
|
$pattern = $this->storage->getPatternById($patternId);
|
|
if ($pattern === null) {
|
|
return false;
|
|
}
|
|
|
|
$acknowledgedPattern = $pattern->acknowledge($acknowledgedBy, $resolution);
|
|
$this->storage->storePattern($acknowledgedPattern);
|
|
|
|
// Clear from cache
|
|
$cacheKey = self::PATTERN_CACHE_PREFIX . $pattern->fingerprint;
|
|
$this->cache->delete(CacheKey::fromString($cacheKey));
|
|
|
|
$this->logError("Pattern acknowledged", [
|
|
'pattern_id' => $patternId,
|
|
'acknowledged_by' => $acknowledgedBy,
|
|
'resolution' => $resolution,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resolves an error pattern
|
|
*/
|
|
public function resolvePattern(string $patternId, string $resolution): bool
|
|
{
|
|
$pattern = $this->storage->getPatternById($patternId);
|
|
if ($pattern === null) {
|
|
return false;
|
|
}
|
|
|
|
$resolvedPattern = $pattern->resolve($resolution);
|
|
$this->storage->storePattern($resolvedPattern);
|
|
|
|
// Clear from cache
|
|
$cacheKey = self::PATTERN_CACHE_PREFIX . $pattern->fingerprint;
|
|
$this->cache->delete(CacheKey::fromString($cacheKey));
|
|
|
|
$this->logError("Pattern resolved", [
|
|
'pattern_id' => $patternId,
|
|
'resolution' => $resolution,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Processes errors in batch for performance
|
|
*/
|
|
public function processBatch(array $errorEvents): void
|
|
{
|
|
$batches = array_chunk($errorEvents, $this->batchSize);
|
|
|
|
foreach ($batches as $batch) {
|
|
$this->storage->storeEventsBatch($batch);
|
|
|
|
foreach ($batch as $event) {
|
|
$this->updateErrorPattern($event);
|
|
}
|
|
}
|
|
|
|
$this->logError("Batch processed", [
|
|
'total_events' => count($errorEvents),
|
|
'batches' => count($batches),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Cleans up old data based on retention policy
|
|
*/
|
|
public function cleanup(): int
|
|
{
|
|
$deletedEvents = 0;
|
|
$deletedPatterns = 0;
|
|
|
|
foreach (ErrorSeverity::cases() as $severity) {
|
|
$retentionDays = $severity->getRetentionDays();
|
|
$cutoffDate = $this->clock->now()->sub(new \DateInterval("P{$retentionDays}D"));
|
|
|
|
$deletedEvents += $this->storage->deleteOldEvents($cutoffDate, $severity);
|
|
}
|
|
|
|
// Delete inactive patterns older than max retention
|
|
$maxCutoff = $this->clock->now()->sub(new \DateInterval("P{$this->maxRetentionDays}D"));
|
|
$deletedPatterns = $this->storage->deleteOldPatterns($maxCutoff);
|
|
|
|
$this->logError("Cleanup completed", [
|
|
'deleted_events' => $deletedEvents,
|
|
'deleted_patterns' => $deletedPatterns,
|
|
]);
|
|
|
|
return $deletedEvents + $deletedPatterns;
|
|
}
|
|
|
|
/**
|
|
* Gets error trends for analysis
|
|
*/
|
|
public function getErrorTrends(
|
|
\DateTimeImmutable $from,
|
|
\DateTimeImmutable $to,
|
|
string $groupBy = 'hour'
|
|
): array {
|
|
return $this->storage->getErrorTrends($from, $to, $groupBy);
|
|
}
|
|
|
|
/**
|
|
* Gets top error patterns by occurrence count
|
|
*/
|
|
public function getTopPatterns(int $limit = 10, ?string $service = null): array
|
|
{
|
|
return $this->storage->getTopPatterns($limit, $service);
|
|
}
|
|
|
|
/**
|
|
* Exports error data for external analysis
|
|
*/
|
|
public function exportData(
|
|
\DateTimeImmutable $from,
|
|
\DateTimeImmutable $to,
|
|
array $filters = []
|
|
): \Generator {
|
|
return $this->storage->exportEvents($from, $to, $filters);
|
|
}
|
|
|
|
/**
|
|
* Gets health status of error aggregation system
|
|
*/
|
|
public function getHealthStatus(): array
|
|
{
|
|
try {
|
|
$stats = $this->getStatistics(
|
|
$this->clock->now()->sub(new \DateInterval('PT1H')),
|
|
$this->clock->now()
|
|
);
|
|
|
|
return [
|
|
'status' => 'healthy',
|
|
'last_hour_events' => $stats['total_events'] ?? 0,
|
|
'active_patterns' => count($this->getActivePatterns(100)),
|
|
'storage_health' => $this->storage->getHealthStatus(),
|
|
'cache_status' => $this->cache->get(CacheKey::fromString('health_check')) !== null ? 'healthy' : 'degraded',
|
|
];
|
|
} catch (\Throwable $e) {
|
|
return [
|
|
'status' => 'unhealthy',
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
private function logError(string $message, array $context = []): void
|
|
{
|
|
if ($this->logger) {
|
|
$logContext = !empty($context)
|
|
? \App\Framework\Logging\ValueObjects\LogContext::withData($context)
|
|
: null;
|
|
$this->logger->info("[ErrorAggregator] {$message}", $logContext);
|
|
}
|
|
}
|
|
}
|