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:
447
src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
Normal file
447
src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
Normal file
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorAggregation\Storage;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\ErrorAggregation\ErrorEvent;
|
||||
use App\Framework\ErrorAggregation\ErrorPattern;
|
||||
use App\Framework\ErrorAggregation\ErrorSeverity;
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Ulid\Ulid;
|
||||
|
||||
/**
|
||||
* Database-based error storage implementation
|
||||
*/
|
||||
final readonly class DatabaseErrorStorage implements ErrorStorageInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection
|
||||
) {
|
||||
}
|
||||
|
||||
public function storeEvent(ErrorEvent $event): void
|
||||
{
|
||||
$sql = "
|
||||
INSERT INTO error_events (
|
||||
id, service, component, operation, error_code, error_message,
|
||||
severity, occurred_at, context, metadata, request_id, user_id,
|
||||
client_ip, is_security_event, stack_trace, user_agent
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
";
|
||||
|
||||
$this->connection->execute($sql, [
|
||||
$event->id->toString(),
|
||||
$event->service,
|
||||
$event->component,
|
||||
$event->operation,
|
||||
$event->errorCode->value,
|
||||
$event->errorMessage,
|
||||
$event->severity->value,
|
||||
$event->occurredAt->format('Y-m-d H:i:s'),
|
||||
json_encode($event->context),
|
||||
json_encode($event->metadata),
|
||||
$event->requestId,
|
||||
$event->userId,
|
||||
$event->clientIp,
|
||||
$event->isSecurityEvent ? 1 : 0,
|
||||
$event->stackTrace,
|
||||
$event->userAgent,
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeEventsBatch(array $events): void
|
||||
{
|
||||
if (empty($events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
INSERT INTO error_events (
|
||||
id, service, component, operation, error_code, error_message,
|
||||
severity, occurred_at, context, metadata, request_id, user_id,
|
||||
client_ip, is_security_event, stack_trace, user_agent
|
||||
) VALUES " . str_repeat('(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?),', count($events));
|
||||
|
||||
$sql = rtrim($sql, ',');
|
||||
|
||||
$params = [];
|
||||
foreach ($events as $event) {
|
||||
$params = array_merge($params, [
|
||||
$event->id->toString(),
|
||||
$event->service,
|
||||
$event->component,
|
||||
$event->operation,
|
||||
$event->errorCode->value,
|
||||
$event->errorMessage,
|
||||
$event->severity->value,
|
||||
$event->occurredAt->format('Y-m-d H:i:s'),
|
||||
json_encode($event->context),
|
||||
json_encode($event->metadata),
|
||||
$event->requestId,
|
||||
$event->userId,
|
||||
$event->clientIp,
|
||||
$event->isSecurityEvent ? 1 : 0,
|
||||
$event->stackTrace,
|
||||
$event->userAgent,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->connection->execute($sql, $params);
|
||||
}
|
||||
|
||||
public function storePattern(ErrorPattern $pattern): void
|
||||
{
|
||||
$sql = "
|
||||
INSERT INTO error_patterns (
|
||||
id, fingerprint, service, component, operation, error_code,
|
||||
normalized_message, severity, occurrence_count, first_occurrence,
|
||||
last_occurrence, affected_users, affected_ips, is_active,
|
||||
is_acknowledged, acknowledged_by, acknowledged_at, resolution, metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
occurrence_count = VALUES(occurrence_count),
|
||||
last_occurrence = VALUES(last_occurrence),
|
||||
affected_users = VALUES(affected_users),
|
||||
affected_ips = VALUES(affected_ips),
|
||||
is_active = VALUES(is_active),
|
||||
is_acknowledged = VALUES(is_acknowledged),
|
||||
acknowledged_by = VALUES(acknowledged_by),
|
||||
acknowledged_at = VALUES(acknowledged_at),
|
||||
resolution = VALUES(resolution),
|
||||
metadata = VALUES(metadata)
|
||||
";
|
||||
|
||||
$this->connection->execute($sql, [
|
||||
$pattern->id->toString(),
|
||||
$pattern->fingerprint,
|
||||
$pattern->service,
|
||||
$pattern->component,
|
||||
$pattern->operation,
|
||||
$pattern->errorCode,
|
||||
$pattern->normalizedMessage,
|
||||
$pattern->severity->value,
|
||||
$pattern->occurrenceCount,
|
||||
$pattern->firstOccurrence->format('Y-m-d H:i:s'),
|
||||
$pattern->lastOccurrence->format('Y-m-d H:i:s'),
|
||||
json_encode($pattern->affectedUsers),
|
||||
json_encode($pattern->affectedIps),
|
||||
$pattern->isActive ? 1 : 0,
|
||||
$pattern->isAcknowledged ? 1 : 0,
|
||||
$pattern->acknowledgedBy,
|
||||
$pattern->acknowledgedAt?->format('Y-m-d H:i:s'),
|
||||
$pattern->resolution,
|
||||
json_encode($pattern->metadata),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPatternById(string $patternId): ?ErrorPattern
|
||||
{
|
||||
$sql = "SELECT * FROM error_patterns WHERE id = ?";
|
||||
$result = $this->connection->query($sql, [$patternId]);
|
||||
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydratePattern($result[0]);
|
||||
}
|
||||
|
||||
public function getPatternByFingerprint(string $fingerprint): ?ErrorPattern
|
||||
{
|
||||
$sql = "SELECT * FROM error_patterns WHERE fingerprint = ?";
|
||||
$result = $this->connection->query($sql, [$fingerprint]);
|
||||
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydratePattern($result[0]);
|
||||
}
|
||||
|
||||
public function getActivePatterns(int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT * FROM error_patterns
|
||||
WHERE is_active = 1
|
||||
ORDER BY last_occurrence DESC, occurrence_count DESC
|
||||
LIMIT ? OFFSET ?
|
||||
";
|
||||
|
||||
$results = $this->connection->query($sql, [$limit, $offset]);
|
||||
|
||||
return array_map([$this, 'hydratePattern'], $results);
|
||||
}
|
||||
|
||||
public function getPatternsByService(string $service, int $limit = 50): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT * FROM error_patterns
|
||||
WHERE service = ? AND is_active = 1
|
||||
ORDER BY last_occurrence DESC, occurrence_count DESC
|
||||
LIMIT ?
|
||||
";
|
||||
|
||||
$results = $this->connection->query($sql, [$service, $limit]);
|
||||
|
||||
return array_map([$this, 'hydratePattern'], $results);
|
||||
}
|
||||
|
||||
public function getRecentEvents(int $limit = 100, ?ErrorSeverity $severity = null): array
|
||||
{
|
||||
$sql = "SELECT * FROM error_events";
|
||||
$params = [];
|
||||
|
||||
if ($severity) {
|
||||
$sql .= " WHERE severity = ?";
|
||||
$params[] = $severity->value;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY occurred_at DESC LIMIT ?";
|
||||
$params[] = $limit;
|
||||
|
||||
$results = $this->connection->query($sql, $params);
|
||||
|
||||
return array_map([$this, 'hydrateEvent'], $results);
|
||||
}
|
||||
|
||||
public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT
|
||||
COUNT(*) as total_events,
|
||||
COUNT(DISTINCT service) as services_affected,
|
||||
COUNT(DISTINCT user_id) as users_affected,
|
||||
COUNT(DISTINCT client_ip) as ips_affected,
|
||||
severity,
|
||||
COUNT(*) as severity_count
|
||||
FROM error_events
|
||||
WHERE occurred_at BETWEEN ? AND ?
|
||||
GROUP BY severity
|
||||
";
|
||||
|
||||
$results = $this->connection->query($sql, [
|
||||
$from->format('Y-m-d H:i:s'),
|
||||
$to->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total_events' => 0,
|
||||
'services_affected' => 0,
|
||||
'users_affected' => 0,
|
||||
'ips_affected' => 0,
|
||||
'by_severity' => [],
|
||||
];
|
||||
|
||||
foreach ($results as $row) {
|
||||
$stats['total_events'] += $row['severity_count'];
|
||||
$stats['services_affected'] = max($stats['services_affected'], $row['services_affected']);
|
||||
$stats['users_affected'] = max($stats['users_affected'], $row['users_affected']);
|
||||
$stats['ips_affected'] = max($stats['ips_affected'], $row['ips_affected']);
|
||||
$stats['by_severity'][$row['severity']] = $row['severity_count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function getErrorTrends(
|
||||
\DateTimeImmutable $from,
|
||||
\DateTimeImmutable $to,
|
||||
string $groupBy = 'hour'
|
||||
): array {
|
||||
$dateFormat = match ($groupBy) {
|
||||
'hour' => '%Y-%m-%d %H:00:00',
|
||||
'day' => '%Y-%m-%d',
|
||||
'week' => '%Y-%u',
|
||||
'month' => '%Y-%m',
|
||||
default => '%Y-%m-%d %H:00:00',
|
||||
};
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
DATE_FORMAT(occurred_at, ?) as time_bucket,
|
||||
severity,
|
||||
COUNT(*) as count
|
||||
FROM error_events
|
||||
WHERE occurred_at BETWEEN ? AND ?
|
||||
GROUP BY time_bucket, severity
|
||||
ORDER BY time_bucket ASC
|
||||
";
|
||||
|
||||
$results = $this->connection->query($sql, [
|
||||
$dateFormat,
|
||||
$from->format('Y-m-d H:i:s'),
|
||||
$to->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$trends = [];
|
||||
foreach ($results as $row) {
|
||||
$trends[] = [
|
||||
'time' => $row['time_bucket'],
|
||||
'severity' => $row['severity'],
|
||||
'count' => (int) $row['count'],
|
||||
];
|
||||
}
|
||||
|
||||
return $trends;
|
||||
}
|
||||
|
||||
public function getTopPatterns(int $limit = 10, ?string $service = null): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT * FROM error_patterns
|
||||
WHERE is_active = 1
|
||||
";
|
||||
$params = [];
|
||||
|
||||
if ($service) {
|
||||
$sql .= " AND service = ?";
|
||||
$params[] = $service;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY occurrence_count DESC LIMIT ?";
|
||||
$params[] = $limit;
|
||||
|
||||
$results = $this->connection->query($sql, $params);
|
||||
|
||||
return array_map([$this, 'hydratePattern'], $results);
|
||||
}
|
||||
|
||||
public function deleteOldEvents(\DateTimeImmutable $cutoffDate, ErrorSeverity $severity): int
|
||||
{
|
||||
$sql = "DELETE FROM error_events WHERE occurred_at < ? AND severity = ?";
|
||||
|
||||
$affectedRows = $this->connection->execute($sql, [
|
||||
$cutoffDate->format('Y-m-d H:i:s'),
|
||||
$severity->value,
|
||||
]);
|
||||
|
||||
return $affectedRows;
|
||||
}
|
||||
|
||||
public function deleteOldPatterns(\DateTimeImmutable $cutoffDate): int
|
||||
{
|
||||
$sql = "DELETE FROM error_patterns WHERE is_active = 0 AND last_occurrence < ?";
|
||||
|
||||
$affectedRows = $this->connection->execute($sql, [
|
||||
$cutoffDate->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $affectedRows;
|
||||
}
|
||||
|
||||
public function exportEvents(
|
||||
\DateTimeImmutable $from,
|
||||
\DateTimeImmutable $to,
|
||||
array $filters = []
|
||||
): \Generator {
|
||||
$sql = "SELECT * FROM error_events WHERE occurred_at BETWEEN ? AND ?";
|
||||
$params = [$from->format('Y-m-d H:i:s'), $to->format('Y-m-d H:i:s')];
|
||||
|
||||
// Add filters
|
||||
if (isset($filters['service'])) {
|
||||
$sql .= " AND service = ?";
|
||||
$params[] = $filters['service'];
|
||||
}
|
||||
|
||||
if (isset($filters['severity'])) {
|
||||
$sql .= " AND severity = ?";
|
||||
$params[] = $filters['severity'];
|
||||
}
|
||||
|
||||
if (isset($filters['security_events_only']) && $filters['security_events_only']) {
|
||||
$sql .= " AND is_security_event = 1";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY occurred_at ASC";
|
||||
|
||||
// Use cursor-based pagination for large datasets
|
||||
$batchSize = 1000;
|
||||
$offset = 0;
|
||||
|
||||
do {
|
||||
$batchSql = $sql . " LIMIT ? OFFSET ?";
|
||||
$batchParams = array_merge($params, [$batchSize, $offset]);
|
||||
|
||||
$results = $this->connection->query($batchSql, $batchParams);
|
||||
|
||||
foreach ($results as $row) {
|
||||
yield $this->hydrateEvent($row);
|
||||
}
|
||||
|
||||
$offset += $batchSize;
|
||||
} while (count($results) === $batchSize);
|
||||
}
|
||||
|
||||
public function getHealthStatus(): array
|
||||
{
|
||||
try {
|
||||
// Test connection
|
||||
$this->connection->query('SELECT 1');
|
||||
|
||||
// Get table stats
|
||||
$eventCount = $this->connection->query('SELECT COUNT(*) as count FROM error_events')[0]['count'] ?? 0;
|
||||
$patternCount = $this->connection->query('SELECT COUNT(*) as count FROM error_patterns')[0]['count'] ?? 0;
|
||||
|
||||
return [
|
||||
'status' => 'healthy',
|
||||
'event_count' => (int) $eventCount,
|
||||
'pattern_count' => (int) $patternCount,
|
||||
'connection' => 'ok',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'status' => 'unhealthy',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function hydratePattern(array $row): ErrorPattern
|
||||
{
|
||||
return new ErrorPattern(
|
||||
id: Ulid::fromString($row['id']),
|
||||
fingerprint: $row['fingerprint'],
|
||||
service: $row['service'],
|
||||
component: $row['component'],
|
||||
operation: $row['operation'],
|
||||
errorCode: $row['error_code'],
|
||||
normalizedMessage: $row['normalized_message'],
|
||||
severity: ErrorSeverity::from($row['severity']),
|
||||
occurrenceCount: (int) $row['occurrence_count'],
|
||||
firstOccurrence: new \DateTimeImmutable($row['first_occurrence']),
|
||||
lastOccurrence: new \DateTimeImmutable($row['last_occurrence']),
|
||||
affectedUsers: json_decode($row['affected_users'], true) ?? [],
|
||||
affectedIps: json_decode($row['affected_ips'], true) ?? [],
|
||||
isActive: (bool) $row['is_active'],
|
||||
isAcknowledged: (bool) $row['is_acknowledged'],
|
||||
acknowledgedBy: $row['acknowledged_by'],
|
||||
acknowledgedAt: $row['acknowledged_at'] ? new \DateTimeImmutable($row['acknowledged_at']) : null,
|
||||
resolution: $row['resolution'],
|
||||
metadata: json_decode($row['metadata'], true) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
private function hydrateEvent(array $row): ErrorEvent
|
||||
{
|
||||
return new ErrorEvent(
|
||||
id: Ulid::fromString($row['id']),
|
||||
service: $row['service'],
|
||||
component: $row['component'],
|
||||
operation: $row['operation'],
|
||||
errorCode: ErrorCode::from($row['error_code']),
|
||||
errorMessage: $row['error_message'],
|
||||
severity: ErrorSeverity::from($row['severity']),
|
||||
occurredAt: new \DateTimeImmutable($row['occurred_at']),
|
||||
context: json_decode($row['context'], true) ?? [],
|
||||
metadata: json_decode($row['metadata'], true) ?? [],
|
||||
requestId: $row['request_id'],
|
||||
userId: $row['user_id'],
|
||||
clientIp: $row['client_ip'],
|
||||
isSecurityEvent: (bool) $row['is_security_event'],
|
||||
stackTrace: $row['stack_trace'],
|
||||
userAgent: $row['user_agent'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorAggregation\Storage;
|
||||
|
||||
use App\Framework\ErrorAggregation\ErrorEvent;
|
||||
use App\Framework\ErrorAggregation\ErrorPattern;
|
||||
use App\Framework\ErrorAggregation\ErrorSeverity;
|
||||
|
||||
/**
|
||||
* Interface for error storage backends
|
||||
*/
|
||||
interface ErrorStorageInterface
|
||||
{
|
||||
/**
|
||||
* Stores a single error event
|
||||
*/
|
||||
public function storeEvent(ErrorEvent $event): void;
|
||||
|
||||
/**
|
||||
* Stores multiple error events in batch
|
||||
*/
|
||||
public function storeEventsBatch(array $events): void;
|
||||
|
||||
/**
|
||||
* Stores or updates an error pattern
|
||||
*/
|
||||
public function storePattern(ErrorPattern $pattern): void;
|
||||
|
||||
/**
|
||||
* Gets error pattern by ID
|
||||
*/
|
||||
public function getPatternById(string $patternId): ?ErrorPattern;
|
||||
|
||||
/**
|
||||
* Gets error pattern by fingerprint
|
||||
*/
|
||||
public function getPatternByFingerprint(string $fingerprint): ?ErrorPattern;
|
||||
|
||||
/**
|
||||
* Gets active error patterns
|
||||
*/
|
||||
public function getActivePatterns(int $limit = 50, int $offset = 0): array;
|
||||
|
||||
/**
|
||||
* Gets error patterns by service
|
||||
*/
|
||||
public function getPatternsByService(string $service, int $limit = 50): array;
|
||||
|
||||
/**
|
||||
* Gets recent error events
|
||||
*/
|
||||
public function getRecentEvents(int $limit = 100, ?ErrorSeverity $severity = null): array;
|
||||
|
||||
/**
|
||||
* Gets error statistics for a time period
|
||||
*/
|
||||
public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): array;
|
||||
|
||||
/**
|
||||
* Gets error trends grouped by time period
|
||||
*/
|
||||
public function getErrorTrends(
|
||||
\DateTimeImmutable $from,
|
||||
\DateTimeImmutable $to,
|
||||
string $groupBy = 'hour'
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Gets top error patterns by occurrence count
|
||||
*/
|
||||
public function getTopPatterns(int $limit = 10, ?string $service = null): array;
|
||||
|
||||
/**
|
||||
* Deletes old events based on cutoff date and severity
|
||||
*/
|
||||
public function deleteOldEvents(\DateTimeImmutable $cutoffDate, ErrorSeverity $severity): int;
|
||||
|
||||
/**
|
||||
* Deletes old inactive patterns
|
||||
*/
|
||||
public function deleteOldPatterns(\DateTimeImmutable $cutoffDate): int;
|
||||
|
||||
/**
|
||||
* Exports events for external analysis
|
||||
*/
|
||||
public function exportEvents(
|
||||
\DateTimeImmutable $from,
|
||||
\DateTimeImmutable $to,
|
||||
array $filters = []
|
||||
): \Generator;
|
||||
|
||||
/**
|
||||
* Gets health status of storage backend
|
||||
*/
|
||||
public function getHealthStatus(): array;
|
||||
}
|
||||
Reference in New Issue
Block a user