How to do advanced filtering of Monolog messages in Symfony? How to do advanced filtering of Monolog messages in Symfony? symfony symfony

How to do advanced filtering of Monolog messages in Symfony?


I'm not sure why using a HandlerWrapper is the wrong way to do it.

I had the same issue and figured a way how to wrap a handler in order to filter certain records.

In this answer I describe two ways to solve this, a more complex and an easy one.

(More or less) complex way

First thing I did, was to create a new class wich extends the HandlerWrapper and added some logic where I can filter records:

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

Then I created a service definition and a CompilerPass where I can wrap the 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();    }}

As you can see I go over all definitions here and find the ones which have the GroupHandler set as their class. If this is the case, I add a new definition to the container which decorates the original handler with my CustomHandler.

Side note: At first I tried to wrap all handlers (except the CustomHandler of course :)) but due to some handlers implementing other interfaces (like the ConsoleHandler using the EventSubscriberInterface) this did not work and lead to issues I didn't want to solve in some hacky way.

Don't forget to add this compiler pass to the container in your AppBundle class

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

Now that everything is in place you have to group your handlers in order to make this work:

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

Easy way

We use the same CustomHandler as we did in the complex way, then we define our handlers in the config:

app/config(_prod|_dev).yml

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

Decorate the handler in your services.yml with your own CustomHandler

services.yml

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

For the decorates property you have to use the format monolog.handler.$NAME_SPECIFIED_AS_KEY_IN_CONFIG, in this case it was graylog.

... and thats it

Summary

While both ways work, I used the first one, as we have several symfony projects where I need this and decorating all handlersmanually is just not what I wanted.

I hope this helps (even though I'm quite late for an answer :))