Files
michaelschiemer/src/Framework/ErrorReporting/ErrorReporter.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

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