63 lines
1.9 KiB
PHP
63 lines
1.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Logger;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Doctrine\DBAL\Types\Types;
|
|
use Monolog\Handler\AbstractProcessingHandler;
|
|
use Monolog\Level;
|
|
use Monolog\LogRecord;
|
|
|
|
/**
|
|
* Persists log entries to the database.
|
|
*
|
|
* The write goes through DBAL rather than the EntityManager on purpose. A log record can be emitted
|
|
* halfway through somebody else's unit of work, and an EntityManager flush would write out whatever
|
|
* that unit of work has pending — which is how a half-built User once reached the database with a
|
|
* NULL password. Staying out of the UnitOfWork also keeps logging alive after a failed flush has
|
|
* closed the EntityManager, which is exactly when there is something worth logging.
|
|
*
|
|
* Reads still go through the LogEntry entity and its repository.
|
|
*/
|
|
class DatabaseHandler extends AbstractProcessingHandler
|
|
{
|
|
public function __construct(private readonly Connection $connection)
|
|
{
|
|
parent::__construct(Level::Info);
|
|
}
|
|
|
|
protected function write(LogRecord $record): void
|
|
{
|
|
$this->connection->insert('log_entry', [
|
|
'channel' => $record->channel,
|
|
'message' => $this->replacePlaceHolder($record),
|
|
'context' => $record->context,
|
|
'extra' => $record->extra,
|
|
'error_code' => $record->extra['error_code'] ?? null,
|
|
'created_at' => new \DateTimeImmutable('now'),
|
|
], [
|
|
'context' => Types::JSON,
|
|
'extra' => Types::JSON,
|
|
'created_at' => Types::DATETIME_IMMUTABLE,
|
|
]);
|
|
}
|
|
|
|
private function replacePlaceHolder(LogRecord $record): string
|
|
{
|
|
$message = $record->message;
|
|
|
|
if (!str_contains($message, '{')) {
|
|
return $message;
|
|
}
|
|
|
|
$replacements = [];
|
|
foreach ($record->context as $k => $v) {
|
|
$replacements['{'.$k.'}'] = $v;
|
|
}
|
|
|
|
return strtr($message, $replacements);
|
|
}
|
|
}
|