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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View 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'],
);
}
}

View File

@@ -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;
}