Symfony utilise le désormais célèbre monolog. Il est facilement accessible sous forme de service (merci l’injection de dépendance) :

namespace App\Controller\Api;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Psr\Log\LoggerInterface;

class FooController extends AbstractController
{
    public function edit(LoggerInterface $logger)
    {
        $logger->info('Hey ! I am writing in logs !!');
    }
}

Handlers (gestionnaires)

A chaque fois qu’on va envoyer une entrée vers monolog (comme ci dessus), ce dernier va appeler la liste (dans l’ordre) des « handlers » (on appelle ça une pile) définis et leur passer la dite entrée. Un « handler » peut être perçu comme un gestionnaire qui va écrire le message quelque part (fichier, base de données, mail, etc) sous certaines conditions. Sous Symfony 4, en mode dev, on a les gestionnaires suivants (config/packages/dev/monolog.yaml) :

monolog:
    handlers:
        # Handler name
        main:
            # Handler type
            type: stream
            # Where to write the entry log
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # Handler level
            level: debug
            # Handler channels
            channels: ["!event"]
        console:
            type:   console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]
  • main et console sont les noms des gestionnaires
  • type défini le type du gestionnaire. Pour l’instant on se cantonne à stream (système de fichier)
  • level est le niveau d’erreur minimal requis pour déclencher le gestionnaire
  • channels permet d’indiquer quelles catégories d’entrées sont autorisées ou non

J’admets, ça déroute un peu au début… La question est : que fait le gestionnaire lorsqu’il reçoit une entrée ? Réponse : cela dépend d’abord du niveau d’erreur et du « channel ».

Les gestionnaires sont appelés dans l’ordre dans lequel ils sont définis dans la clé handlers. Pour cette raison, il est conseillé de ne pas utiliser plusieurs fichiers de configuration : un fichier dans le répertoire /prod et un dans le répertoire /dev (et éventuellement un dans /test).

Niveaux d’erreurs

Dans l’ordre, les niveaux d’erreurs (levels) disponibles dans monolog :

  • debug
  • info
  • notice
  • warning
  • error
  • critical
  • alert
  • emergency

Pour reprendre l’exemple précédent, le gestionnaire main a un niveau (clé level) d’erreur fixé à debug. C’est le niveau le plus faible, ce gestionnaire sera donc toujours utilisé quelque soit le niveau d’erreur :

use Psr\Log\LoggerInterface;

class FooController extends AbstractController
{
    public function edit(LoggerInterface $logger)
    {
        $logger->info('Hey ! I am writing in logs !!');
        $logger->critical('Oops something bad is happening');
    }
}

Autrement dit, en mode dev, tout est écrit dans le fichier var/dev.log (clé path). Si maintenant on défini le niveau d’erreur du gestionnaire main à critical, l’entrée info ne sera pas écrite. A noter que la pile des gestionnaires est toujours appelée en totalité, c’est à dire qu’une même entrée peut être écrite à plusieurs endroits (l’utilisation d’un gestionnaire n’arrête pas le traitement de la pile).

Channels

Les channels servent à identifier les entrées log. A chaque channel correspond un service monolog.logger.XXX (remplacez XXX par le nom du channel). Pour lister les channels utilisés dans Symfony, on liste les services correspondant :

$ ./bin/console debug:container --show-private monolog.log

# Output
[0 ] monolog.logger
[1 ] monolog.logger_prototype
[2 ] monolog.logger.request
[3 ] monolog.logger.console
[4 ] monolog.logger.cache
[5 ] monolog.logger.translation
[6 ] monolog.logger.profiler
[7 ] monolog.logger.php
[8 ] monolog.logger.event
[9 ] monolog.logger.router
[10] monolog.logger.security
[11] monolog.logger.doctrine
[12] monolog.logger.debug

Lorsqu’on écrit quelque chose comme :

$logger->info('Hi there');

C’est la classe de $logger (donc le service utilisé) qui va définir le channel de l’entrée log. Par défaut, Symfony va utiliser le service monolog.logger. Dans l’exemple précédent, cela se traduit par les deux entrés suivantes (dans le fichier var/dev.log) :

[2018-10-06 10:18:36] app.INFO: Hey ! I am writing in logs !! [] []
[2018-10-06 10:18:36] app.CRITICAL: Oops something bad is happening [] []

Le service monolog.logger correspond donc au channel app. A noter qu’on on voit le niveau d’erreur juste après le channel (format channel.LEVEL). Maintenant, comment faire pour utiliser un autre channel ?

Il suffit juste d’injecter le service approprié. Par exemple, pour utiliser le channel doctrine, on modifie légèrement notre code précédent :

namespace App\Controller\Api;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Psr\Log\LoggerInterface;

class FooController extends AbstractController
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function edit()
    {
        $this->logger->info('Hey ! I am writing in logs !!');
        $this->logger->critical('Oops something bad is happening');
    }
}

Puis on spécifie le service à utiliser dans services.yaml :

services:
    App\Controller\Api\FooController:
        arguments:
            $logger: '@monolog.logger.doctrine'

On vérifie que le channel doctrine est bien utilisé :

[2018-10-06 13:07:46] doctrine.INFO: Hey ! I am writing in logs !! [] []
[2018-10-06 13:07:46] doctrine.CRITICAL: Oops something bad is happening [] []

Type de gestionnaire

Pour le moment on a utilisé un seul type de gestionnaire : stream. Il en existe d’autres :

  • fingers_crossed : ce gestionnaire stocke dans un buffer les entrées log qu’il reçoit. Si une entrée atteint un certain niveau d’erreur, cela déclenche le « vidage » du buffer en direction d’un autre handler. Plus d’explications juste après.
  • rotating_file : permet de ne conserver les logs (dans le cas d’un stockage dans un système de fichier) qu’un certain nombre de jours. Plus d’explications juste après.
  • syslog : ce gestionnaire utilise la fonction php syslog pour l’entrée log reçue.

Plusieurs fichiers de logs

Grâce aux channels, on va pouvoir séparer nos logs dans différents fichiers. Supposons que je veuille que les entrées log de doctrine soient stockées dans un fichier séparé. Il suffit de créer un gestionnaire qui va prendre en compte que le channel doctrine, et pas les autres :

monolog:
    handlers:
        doctrine_logging:
            type: stream
            path: "%kernel.logs_dir%/doctrine.%kernel.environment%.log"
            level: debug
            channels: ['doctrine']

On a maintenant un fichier /var/log/doctrine.dev.log qui contient, entre autres, nos deux entrées log :

[2018-10-06 21:28:35] doctrine.INFO: Hey ! I am writing in logs !! [] []
[2018-10-06 21:28:35] doctrine.CRITICAL: Oops something bad is happening [] []

Mais ces deux entrées se trouvent encore dans /var/log/dev.log. C’est normal, il faut modifier la configuration du gestionnaire principal pour qu’il écarte le channel doctrine :

monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event", "!doctrine"]

Voyez la syntaxe pour exclure un channel : !channel_name.

Rotation des logs

Il suffit de remplacer stream par rotating_file. Dans ce cas, monolog crée un fichier par jour.

monolog:
    handlers:
        main:
            type:  rotating_file
            path:  '%kernel.logs_dir%/%kernel.environment%.log'
            level: debug
            max_files: 10

Utilisez max_files pour modifier le nombre de jours à conserver.

Filtrer les messages d’erreurs

En production, on a souvent besoin d’une journalisation réduite. Dans ce cas, on utilise le gestionnaire de type fingers_crossed qui va agir comme un buffer : il stocke toutes les entrées de la requête courante puis les transfère à un autre gestionnaire que lorsqu’une entrée log a un niveau d’erreur au moins équivalente à un seuil (qu’on défini bien sûr). Voyons la configuration du mode production de Symfony :

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_404s:
                # regex: exclude all 404 errors from the logs
                - ^/
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug

La clé action_level défini le seuil de déclenchement, handler défini le gestionnaire à utiliser. Ici, dès qu’une erreur a niveau au moins égal à error, toutes entrées (y compris celle qui déclenche le gestionnaire) sont envoyées au gestionnaire nested qui lui va va écrire dans le fichier prod.log. Comme précédemment, la clé level du gestionnaire nested défini le niveau d’erreur minimal des entrées à inscrire dans le fichier.
L’intérêt de ce gestionnaire est majeur : s’il n’y a pas d’erreurs d’un certain niveau, on n’encombre pas les logs inutilement… Par contre, en cas d’erreur sérieuse, on a tous les messages d’erreur à notre disposition.

Logs système

Un type de gestionnaire intéressant est syslog. Il permet de stocker des entrées log via le système de log de la machine qui fait tourner l’application. Par exemple, en production, on peut ajouter un tel gestionnaire :

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_404s:
                # regex: exclude all 404 errors from the logs
                - ^/
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
        # still passed *all* logs, and still only logs error or higher
        syslog_handler:
            type: syslog
            level: debug

Souvenez vous, les gestionnaires sont appelés dans l’ordre. Donc le gestionnaire syslog sera toujours appelé et il inscrira toutes les entrées dans les logs de la machine. Par exemple, sur une machine debian, le fichier est /var/log/syslog.

Happy debugging

monolog est compatible firephp et chromephp. Il suffit d’ajouter un gestionnaire :

monolog:
    handlers:
        firephp:
            type: firephp
            level: info

L’extension du même nom doit être installée et activée sur le navigateur. Ensuite, les entrées log s’affichent dans la console. A réserver au mode dev bien sûr…

Références externes