Lines
100.00%
23 / 23
Functions and Methods
100.00%
5 / 5
Classes and Traits
100.00%
1 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ChatLogHandler | 100.00% | 23 / 23 | 100.00% | 5 / 5 | 13 | 100.00% | 1 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| handle | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| handleBatch | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 6 | |||||
| isPosted | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | |||||
| line | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 3 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace App\Log; | |
| 6 | ||
| 7 | use App\Notifier\ChatFeed; | |
| 8 | use App\Notifier\ChatSender; | |
| 9 | use App\Notifier\ChatText; | |
| 10 | use Monolog\Handler\AbstractHandler; | |
| 11 | use Monolog\Level; | |
| 12 | use Monolog\LogRecord; | |
| 13 | use Symfony\Component\HttpFoundation\Request; | |
| 14 | use Symfony\Component\HttpFoundation\RequestStack; | |
| 15 | ||
| 16 | /** | |
| 17 | * Posts errors and deprecations to Slack and Telegram (ChatFeed::Errors), one message per batch. | |
| 18 | * Used in dev and prod behind Monolog's deduplication handlers (config/packages/monolog.yaml), | |
| 19 | * which pass on whole buffers: only records of level error and up and those of the deprecation | |
| 20 | * channel are posted. A failure to post goes to PHP's error log, not to Monolog, so it cannot | |
| 21 | * come back here. | |
| 22 | */ | |
| 23 | final class ChatLogHandler extends AbstractHandler | |
| 24 | { | |
| 25 | private bool $sending = false; | |
| 26 | ||
| 27 | public function __construct( | |
| 28 | private readonly ChatSender $chatSender, | |
| 29 | private readonly RequestStack $requestStack, | |
| 30 | ) { | |
| 31 | parent::__construct(Level::Debug, false); | |
| 32 | } | |
| 33 | ||
| 34 | public function handle(LogRecord $record): bool | |
| 35 | { | |
| 36 | $this->handleBatch([$record]); | |
| 37 | ||
| 38 | return false; | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * @param array<LogRecord> $records | |
| 43 | */ | |
| 44 | public function handleBatch(array $records): void | |
| 45 | { | |
| 46 | $posted = \array_filter($records, $this->isPosted(...)); | |
| 47 | if ([] === $posted || $this->sending) { | |
| 48 | return; | |
| 49 | } | |
| 50 | ||
| 51 | $text = new ChatText(); | |
| 52 | foreach ($posted as $record) { | |
| 53 | $this->line($text, $record); | |
| 54 | } | |
| 55 | ||
| 56 | $request = $this->requestStack->getMainRequest(); | |
| 57 | $text->text($request instanceof Request ? $request->getMethod().' '.$request->getUri() : 'console'); | |
| 58 | ||
| 59 | $this->sending = true; | |
| 60 | try { | |
| 61 | foreach ($this->chatSender->send(ChatFeed::Errors, $text) as $transport => $exception) { | |
| 62 | \error_log(\sprintf('%s: a log message was not sent: %s', $transport, $exception->getMessage())); | |
| 63 | } | |
| 64 | } finally { | |
| 65 | $this->sending = false; | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | private function isPosted(LogRecord $record): bool | |
| 70 | { | |
| 71 | return $record->level->isHigherThan(Level::Warning) || 'deprecation' === $record->channel; | |
| 72 | } | |
| 73 | ||
| 74 | private function line(ChatText $text, LogRecord $record): void | |
| 75 | { | |
| 76 | $text->text('deprecation' === $record->channel ? '⚠️ ' : '🚨 ') | |
| 77 | ->bold($record->level->getName()) | |
| 78 | ->text(\sprintf(' %s: %s', $record->channel, $record->message)); | |
| 79 | ||
| 80 | $exception = $record->context['exception'] ?? null; | |
| 81 | if ($exception instanceof \Throwable) { | |
| 82 | $text->text("\n")->code($exception::class)->text(\sprintf(' at %s:%d', $exception->getFile(), $exception->getLine())); | |
| 83 | } | |
| 84 | ||
| 85 | $text->text("\n"); | |
| 86 | } | |
| 87 | } |