How to do advanced filtering of Monolog messages in Symfony?

monolog, php, symfony

Solution

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 handlers manually is just not what I wanted.

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

Problem

I am using the `MonologBundle` in my `Symfony 2.8` project to manage log messages. Using different `Handlers` it is no problem to write logs to file and to send them by e-mail at the same time. I would like to reduce the number of messages I receive by mail. I already use the `DeduplicationHandler` and the `FingersCrossed` handler to filter by error level and to avoid duplicate messages. This works fine but is not enough. For example I would like to reduce the number of mail about `PageNotFound` errors. Of course I want to be notified if `/existingPage` is not found, but I am not interested in messages about `/.well-known/...` files. Another example are messages about errors in a third party CSV parser component. There are several known and harmless errors I am not interested in, but of course other errors are important. This these errors/messages are generated by third party code, I cannot influence the source. I could only ignore these messages completely but this is not what I want. I am looking for a solution to filter the messages by content. How can this be done in Monolog? I already tried to solve this using a `HandlerWrapper` and discussed this issue in another question: The idea was, that the `HandlerWrapper` acts as filter. The `HandlerWrapper` is called by Monolog, it checks the message content and decides wether it should be processed or not (e.g. discard all messages including the text "./well-known/"). If a messages passes, the `HandlerWrapper` should simple hand it over to its nested/wrapped handler. Otherwise the message is skipped without further processing. However this idea did not work, and the answers to the other question indicate, that a `HandlerWrapper` is not the right approach for this problem. So the new/actual question is: How to create a filter for Monolog messages, that let me control wether a specific message should be process or not?

Original source