Как выполнить расширенную фильтрацию сообщений Monolog в Symfony?

Я использую MonologBundle в моем проекте Symfony 2.8 для управления сообщениями журнала. С помощью разных Handlers нет проблем писать записи в файл и отправлять их по электронной почте одновременно.

Я хотел бы уменьшить количество сообщений, получаемых по почте. Я уже использую DeduplicationHandler и обработчик FingersCrossed для фильтрации по уровню ошибок и для предотвращения дублирования сообщений. Это работает отлично, но этого недостаточно.

Например, я хотел бы уменьшить количество сообщений о ошибках PageNotFound . Конечно, я хочу получать уведомление, если /existingPage не найден, но мне не интересны сообщения о /.well-known/...

Другим примером являются сообщения об ошибках стороннего компонента парсера CSV. Есть несколько известных и безобидных ошибок, которые мне не интересны, но, конечно, важны другие ошибки.

Эти ошибки / сообщения генерируются сторонним кодом, я не могу влиять на источник. Я мог полностью игнорировать эти сообщения, но этого я не хочу.

Я ищу решение для фильтрации сообщений по содержанию. Как это можно сделать в Монологе?

Я уже пытался решить это с помощью HandlerWrapper и обсудил этот вопрос в другом вопросе : идея заключалась в том, что HandlerWrapper действует как фильтр. HandlerWrapper вызывается HandlerWrapper , он проверяет содержимое сообщения и решает, как его обрабатывать или нет (например, отбросить все сообщения, включая текст «./well-known/»). Если сообщения проходят, HandlerWrapper должен просто передать его своему вложенному / завернутому обработчику. В противном случае сообщение пропускается без дальнейшей обработки.

Однако эта идея не сработала, и ответы на другой вопрос указывают на то, что HandlerWrapper не подходит для этой проблемы.

Таким образом, новый / актуальный вопрос: как создать фильтр для сообщений Монолога, который позволяет мне контролировать, какое конкретное сообщение должно быть процессом или нет?

Я не уверен, почему использование HandlerWrapper – неправильный способ сделать это.

У меня была такая же проблема, и я понял способ переноса обработчика для фильтрации определенных записей.

В этом ответе я описываю два способа решения этого, более сложный и простой.

(Более или менее) сложным способом

Первое, что я сделал, – создать новый класс, который расширяет HandlerWrapper и добавляет некоторую логику, где я могу фильтровать записи:

 use Monolog\Handler\HandlerWrapper; class CustomHandler extends HandlerWrapper { public function isHandling(array $record) { if ($this->shouldFilter($record)) { return false; } return $this->handler->isHandling($record); } public function handle(array $record) { if (!$this->isHandling($record)) { return false; } return $this->handler->handle($record); } public function handleBatch(array $records) { foreach ($records as $record) { $this->handle($record); } } private function shouldFilter(array $record) { return mt_rand(0, 1) === 1;; // add logic here } } 

Затем я создал определение службы и CompilerPass, где я могу обернуть GroupHandler

services.yml

 CustomHandler: class: CustomHandler abstract: true arguments: [''] 
 use Monolog\Handler\GroupHandler; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class CustomMonologHandlerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition(CustomHandler::class)) { return; } $definitions = $container->getDefinitions(); foreach ($definitions as $serviceId => $definition) { if (!$this->isValidDefinition($definition)) { continue; } $cacheId = $serviceId . '.wrapper'; $container ->setDefinition($cacheId, new ChildDefinition(CustomHandler::class)) ->replaceArgument(0, new Reference($cacheId . '.inner')) ->setDecoratedService($serviceId); } } private function isValidDefinition(Definition $definition): bool { return GroupHandler::class === $definition->getClass(); } } 

Как вы можете видеть, я просматриваю все определения здесь и нахожу те, которые имеют GroupHandler в качестве своего класса. Если это так, я добавляю новое определение в контейнер, который украшает оригинальный обработчик моим CustomHandler.

Замечание : сначала я попытался обернуть все обработчики (за исключением CustomHandler, конечно :)), но из-за некоторых обработчиков, реализующих другие интерфейсы (например, ConsoleHandler с использованием EventSubscriberInterface ), это не сработало и привело к проблемам, которые я не хотел решить в какой-то хакерский путь.

Не забудьте добавить этот компилятор в контейнер в классе AppBundle

 class AppBundle extends Bundle { public function build(ContainerBuilder $container) { $container->addCompilerPass(new CustomMonologHandlerPass()); } } 

Теперь, когда все на месте, вам нужно сгруппировать обработчиков, чтобы сделать эту работу:

app/config(_prod|_dev).yml

 monolog: handlers: my_group: type: group members: [ 'graylog' ] graylog: type: gelf publisher: id: my.publisher level: debug formatter: my.formatter 

Простой способ

Мы используем тот же CustomHandler, что и мы, сложным способом, затем определяем наши обработчики в config:

app/config(_prod|_dev).yml

 monolog: handlers: graylog: type: gelf publisher: id: my.publisher level: debug formatter: my.formatter 

Украсьте обработчик в своих службах.yml своим собственным CustomHandler

services.yml

 CustomHandler: class: CustomHandler decorates: monolog.handler.graylog arguments: ['@CustomHandler.inner'] 

Для свойства decorates вы должны использовать формат monolog.handler.$NAME_SPECIFIED_AS_KEY_IN_CONFIG , в этом случае это был graylog.

… вот и все

Резюме

Хотя оба способа работают, я использовал первый, так как у нас есть несколько проектов symfony, где мне это нужно, и оформление всех обработчиков вручную – это просто не то, что я хотел.

Надеюсь, это поможет (хотя я довольно поздно для ответа :))