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?
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 :))