feat: error-codes in flash messages to reference log entries

This commit is contained in:
Björn Fromme
2026-01-23 11:06:24 +01:00
parent c41524c1fd
commit aa08307500
7 changed files with 517 additions and 0 deletions
+5
View File
@@ -112,6 +112,11 @@ class LogEntry
return $this->getExtra()['request_id'] ?? '-';
}
public function getErrorCode(): string
{
return $this->getExtra()['error_code'] ?? '-';
}
public function getUri(): string
{
return $this->getExtra()['uri'] ?? '';
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Logger;
use App\Service\ErrorCodeService;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\Level;
use Monolog\LogRecord;
/**
* Adds error reference codes to log records with ERROR level or higher.
*
* When an error is logged, this processor generates a user-friendly error code
* and adds it to the log record's extra data. The code can then be displayed
* to users and used by support staff to locate the corresponding log entries.
*/
#[AsMonologProcessor]
class ErrorCodeProcessor
{
public function __construct(
private readonly ErrorCodeService $errorCodeService,
) {
}
public function __invoke(LogRecord $record): LogRecord
{
if ($record->level->value < Level::Error->value) {
return $record;
}
$errorCode = $this->errorCodeService->getErrorCode();
if (null === $errorCode) {
return $record;
}
$this->errorCodeService->markErrorOccurred();
$record->extra['error_code'] = $errorCode;
return $record;
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Generates and tracks error reference codes for user-facing error messages.
*
* Produces deterministic codes based on the current request ID, allowing users
* to report error codes to support staff who can then search logs by the code.
* The code format is E-XXXXXXXX (8 uppercase alphanumeric characters in base36).
*/
class ErrorCodeService
{
private ?string $errorCode = null;
private bool $hasError = false;
public function __construct(
private readonly RequestStack $requestStack,
) {
}
/**
* Returns the error code for the current request.
*
* Generates a deterministic code from the request ID on first call and
* caches it for subsequent calls. Sequence suffixes like _1, _2 are
* stripped before hashing to ensure all requests in a sequence share
* the same error code.
*
* @return string|null The error code or null if no request context exists
*/
public function getErrorCode(): ?string
{
if (null !== $this->errorCode) {
return $this->errorCode;
}
$request = $this->requestStack->getMainRequest();
if (null === $request) {
return null;
}
$requestId = $request->attributes->get('request_id');
if (null === $requestId) {
return null;
}
$this->errorCode = $this->generateCode((string) $requestId);
return $this->errorCode;
}
/**
* Marks that an error has occurred during this request.
*
* Called by the ErrorCodeProcessor when a log entry with ERROR level
* or higher is created.
*/
public function markErrorOccurred(): void
{
$this->hasError = true;
}
/**
* Checks whether an error has been logged during this request.
*
* @return bool True if an error has been logged, false otherwise
*/
public function hasError(): bool
{
return $this->hasError;
}
/**
* Generates an error code from a request ID.
*
* Strips sequence suffixes, hashes the normalized ID, and converts
* to a base36 representation prefixed with E-.
*
* @param string $requestId The request ID to generate a code from
*
* @return string The generated error code in format E-XXXXXXXX
*/
private function generateCode(string $requestId): string
{
$normalizedId = preg_replace('/_\d+$/', '', $requestId);
$hash = md5((string) $normalizedId);
$hexPart = substr($hash, 0, 12);
$decimal = hexdec($hexPart);
$base36 = strtoupper(base_convert((string) $decimal, 10, 36));
$padded = str_pad($base36, 8, '0', STR_PAD_LEFT);
return 'E-'.substr($padded, 0, 8);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Twig;
use App\Service\ErrorCodeService;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class ErrorCodeExtension extends AbstractExtension
{
public function __construct(
private readonly ErrorCodeService $errorCodeService,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('error_code', [$this, 'getErrorCode']),
new TwigFunction('has_error_code', [$this, 'hasErrorCode']),
];
}
public function getErrorCode(): ?string
{
return $this->errorCodeService->getErrorCode();
}
public function hasErrorCode(): bool
{
return $this->errorCodeService->hasError();
}
}