PHPnews.io

Dynamically changing the log level in Symfony apps

Written by Matthias Noback / Original link on Sep. 24, 2020

This is just a quick post sharing something I was able to figure out after doing some research.

The situation: our application throws exceptions by means of "talking back to the user". As developer we don't want to be notified about all these exceptions. They aren't as important as any other exception that should be considered "critical". Still, we do want to find these exceptions in the logs, because they can sometimes provide valuable feedback about the usability of the system.

So basically we just want Symfony to log the exception, but dial the standard log level "critical" for certain types of exceptions down to "info". Monolog has the concept of a processor for that (represented by the optional ProcessorInterface). The implemented process() method should return the modified log record, which is actually an array containing the different parts of a log record (the level, the message, and the context). Since not every log record is related to an exception, we don't have to do anything in most cases. Here's the code:

use Monolog\Logger;
use Monolog\Processor\ProcessorInterface;
use Symfony\Component\HttpKernel\EventListener\ErrorListener;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;

final class AdjustLogLevelForExceptions implements ProcessorInterface
{
    public function __invoke(array $record): array
    {
        if (!isset($record['context']['exception'])) {
            /**
             * Symfony's ErrorListener will provide a Throwable through the log message's context.
             * If the context has no "exception" key, we don't have to further process this log record.
             *
             * @see ErrorListener::logException()
             */
            return $record;
        }

        $throwable = $record['context']['exception'];
        if (!$throwable instanceof Throwable) {
            // For some reason the provided value is not an actual exception, so we can't do anything with it
            return $record;
        }

        // Change the log level if necessary
        $modifiedLogLevel = $this->determineLogLevel($throwable, $record['level']);

        $record['level'] = $modifiedLogLevel;
        $record['level_name'] = Logger::getLevelName($modifiedLogLevel);

        return $record;
    }

    private function determineLogLevel(Throwable $throwable, int $currentLevel): int
    {
        if ($throwable instanceof UserErrorMessage) {
            // These are exceptions that will be rendered to the user.
            return Logger::INFO;
        }

        return $currentLevel;
    }
}

Now you only have to register this processor as a service and tag it as monolog.processor:

services:
    AdjustLogLevelForExceptions:
        tags:
            - { name: monolog.processor }

If you use auto-wiring and have set up auto-configuration for the directory where the AdjustLogLevelForExceptions is located you don't even have to tag this service. The MonologBundle should automatically do that for you.

matthiasnoback

« ★ Why and how you should monitor scheduled tasks - Smarter throwing [blog] »