- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
279 lines
7.5 KiB
PHP
279 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\ErrorReporting;
|
|
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\ErrorReporting\Storage\ErrorReportStorageInterface;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Queue\Queue;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Central error reporting service
|
|
*/
|
|
final readonly class ErrorReporter
|
|
{
|
|
public function __construct(
|
|
private ErrorReportStorageInterface $storage,
|
|
private Clock $clock,
|
|
private ?Logger $logger = null,
|
|
private ?Queue $queue = null,
|
|
private bool $asyncProcessing = true,
|
|
private array $processors = [],
|
|
private array $filters = []
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Report an error from Throwable
|
|
*/
|
|
public function reportThrowable(
|
|
Throwable $throwable,
|
|
string $level = 'error',
|
|
array $context = []
|
|
): string {
|
|
$report = ErrorReport::fromThrowable($throwable, $level, $context);
|
|
|
|
return $this->report($report);
|
|
}
|
|
|
|
/**
|
|
* Report a manual error
|
|
*/
|
|
public function reportError(
|
|
string $level,
|
|
string $message,
|
|
array $context = [],
|
|
?Throwable $exception = null
|
|
): string {
|
|
$report = ErrorReport::create($level, $message, $context, $exception);
|
|
|
|
return $this->report($report);
|
|
}
|
|
|
|
/**
|
|
* Report an error with full context
|
|
*/
|
|
public function report(ErrorReport $report): string
|
|
{
|
|
try {
|
|
// Apply filters - skip reporting if any filter returns false
|
|
foreach ($this->filters as $filter) {
|
|
if (! $filter($report)) {
|
|
$this->logDebug("Error report filtered out", ['report_id' => $report->id]);
|
|
|
|
return $report->id;
|
|
}
|
|
}
|
|
|
|
// Apply processors to enrich the report
|
|
$enrichedReport = $this->applyProcessors($report);
|
|
|
|
// Store or queue the report
|
|
if ($this->asyncProcessing && $this->queue) {
|
|
$this->queueReport($enrichedReport);
|
|
} else {
|
|
$this->storeReport($enrichedReport);
|
|
}
|
|
|
|
$this->logInfo("Error report created", [
|
|
'report_id' => $enrichedReport->id,
|
|
'level' => $enrichedReport->level,
|
|
'exception' => $enrichedReport->exception,
|
|
]);
|
|
|
|
return $enrichedReport->id;
|
|
|
|
} catch (Throwable $e) {
|
|
// Fallback logging if reporting system fails
|
|
$this->logError("Failed to report error", [
|
|
'original_message' => $report->message,
|
|
'reporting_error' => $e->getMessage(),
|
|
]);
|
|
|
|
return $report->id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report multiple errors in batch
|
|
*/
|
|
public function reportBatch(array $reports): array
|
|
{
|
|
$reportIds = [];
|
|
|
|
foreach ($reports as $report) {
|
|
if ($report instanceof ErrorReport) {
|
|
$reportIds[] = $this->report($report);
|
|
} else {
|
|
$this->logError("Invalid report in batch", ['report' => gettype($report)]);
|
|
}
|
|
}
|
|
|
|
return $reportIds;
|
|
}
|
|
|
|
/**
|
|
* Add request context to current thread
|
|
*/
|
|
public function withRequestContext(
|
|
string $method,
|
|
string $route,
|
|
?string $requestId = null,
|
|
?string $userAgent = null,
|
|
?string $ipAddress = null,
|
|
?array $requestData = null
|
|
): RequestContextualReporter {
|
|
return new RequestContextualReporter(
|
|
reporter: $this,
|
|
method: $method,
|
|
route: $route,
|
|
requestId: $requestId,
|
|
userAgent: $userAgent,
|
|
ipAddress: $ipAddress,
|
|
requestData: $requestData
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add user context to current thread
|
|
*/
|
|
public function withUserContext(string $userId, ?string $sessionId = null): UserContextualReporter
|
|
{
|
|
return new UserContextualReporter(
|
|
reporter: $this,
|
|
userId: $userId,
|
|
sessionId: $sessionId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get error report by ID
|
|
*/
|
|
public function getReport(string $reportId): ?ErrorReport
|
|
{
|
|
try {
|
|
return $this->storage->find($reportId);
|
|
} catch (Throwable $e) {
|
|
$this->logError("Failed to retrieve error report", [
|
|
'report_id' => $reportId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recent error reports
|
|
*/
|
|
public function getRecentReports(int $limit = 100, int $offset = 0): array
|
|
{
|
|
try {
|
|
return $this->storage->findRecent($limit, $offset);
|
|
} catch (Throwable $e) {
|
|
$this->logError("Failed to retrieve recent error reports", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get error reports by criteria
|
|
*/
|
|
public function findReports(ErrorReportCriteria $criteria): array
|
|
{
|
|
try {
|
|
return $this->storage->findByCriteria($criteria);
|
|
} catch (Throwable $e) {
|
|
$this->logError("Failed to find error reports", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get error statistics
|
|
*/
|
|
public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): ErrorStatistics
|
|
{
|
|
try {
|
|
return $this->storage->getStatistics($from, $to);
|
|
} catch (Throwable $e) {
|
|
$this->logError("Failed to get error statistics", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ErrorStatistics::empty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up old error reports
|
|
*/
|
|
public function cleanup(\DateTimeImmutable $before): int
|
|
{
|
|
try {
|
|
$deleted = $this->storage->deleteOlderThan($before);
|
|
$this->logInfo("Cleaned up old error reports", ['deleted_count' => $deleted]);
|
|
|
|
return $deleted;
|
|
} catch (Throwable $e) {
|
|
$this->logError("Failed to cleanup error reports", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function applyProcessors(ErrorReport $report): ErrorReport
|
|
{
|
|
$processedReport = $report;
|
|
|
|
foreach ($this->processors as $processor) {
|
|
try {
|
|
$processedReport = $processor($processedReport);
|
|
} catch (Throwable $e) {
|
|
$this->logError("Error processor failed", [
|
|
'processor' => $processor::class ?? 'closure',
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $processedReport;
|
|
}
|
|
|
|
private function storeReport(ErrorReport $report): void
|
|
{
|
|
$this->storage->store($report);
|
|
}
|
|
|
|
private function queueReport(ErrorReport $report): void
|
|
{
|
|
$this->queue->push('error_report_processing', $report->toArray());
|
|
}
|
|
|
|
private function logDebug(string $message, array $context = []): void
|
|
{
|
|
$this->logger?->debug("[ErrorReporter] {$message}", $context);
|
|
}
|
|
|
|
private function logInfo(string $message, array $context = []): void
|
|
{
|
|
$this->logger?->info("[ErrorReporter] {$message}", $context);
|
|
}
|
|
|
|
private function logError(string $message, array $context = []): void
|
|
{
|
|
$this->logger?->error("[ErrorReporter] {$message}", $context);
|
|
}
|
|
}
|