Symfony : Configuration des logs Monolog

Configuration des logs Monolog

Symfony utilise Monolog pour gérer les logs.

Les logs te permette de garder une trace de ce qui se passe sur ton application. Souvent ils se révèlent une source très précieuse d’informations lorsqu’un utilisateur soulève un bug ou un comportement anormal.

De plus, avec Symfony, tu peux mettre en place des mécanismes simples pour déclencher des actions en fonction du niveau d’alerte des logs, par exemple pour recevoir un mail lorsqu’un problème survient. N’hésites pas à logger le plus d’informations possible, Symfony te permet de gérer simplement la rotation des fichiers de log pour ne pas perdre indéfiniment en espace disque.

Niveaux de logs

Comme beaucoup de systèmes de log, Monolog utilise plusieurs niveaux. Par ordre croissant, du moins alertant au plus critique des logs :

  • DEBUG : Utilisé en général pour développer ou débugger une application afin de vérifier une valeur ou un bon déroulement.
  • INFO : Information sur un événement commun et normal (exemple : un utilisateur qui se connecte).
  • NOTICE : Comportement normal signifiant mais pas d’erreur.
  • WARNING : Événement exceptionnel mais sans erreur (exemple : Utilisation d’une fonction dépréciée).
  • ERROR :Erreur d’exécution qui ne demande pas d’intervention immédiate mais qui doit être enregistrée. (exemple : une erreur 404, un objet non trouvé en base avec tel identifiant …).
  • CRITICAL : Exception inattendue soulevée pendant l’exécution de l’application. Cette action est généralement accompagnée d’alerte mail. (exemple : un paramètre manquant dans la configuration d’un module).
  • ALERT : « Alerte rouge », tout le service ou sa base de données est indisponible. Cette action est généralement accompagnée d’alerte sms et / ou d’alerte monitoring sonore. (exemple : le site est inaccessible par votre outil de monitoring).
  • EMERGENCY : Le système est inutilisable, tout est complètement cassé et nécessite une grosse intervention pour tout remettre d’aplomb. Des données sont perdues / corrompues. Bref … Je vous laisse imaginer la catastrophe que ça peut être. Je vous souhaite de ne jamais voir apparaître ce genre de log ! (exemple : détection d’un hacking bien hard de votre site).

Logger avec Symfony

Pour utiliser le système de log de Symfony :

Dans un contrôler

public function index(Logger $logger): Response {
    $logger->info('Tout va bien');
    $logger->error('Je ne peux pas trouver la voiture n°53');
    $logger->critical('Ca ne marche pas !!');
}

Dans un service

namespace App\Mailer;
use Monolog\Logger;

class Mailer
{
    protected $logger;

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

    public function faireQuelqueChose() {
        $this->logger->info('Je fais quelque chose');
        $this->logger->critical('Mais je n\'ai pas réussi ...');
    }
}

Configuration du logger

La configuration par défaut que l’on trouve pour un environnement de production ressemble à celle-ci :

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_http_codes: [404]
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine"]

Ici main, nested, console sont appelés des handlers (des gestionnaires), le nom donné est arbitraire. Pour chaque handler, on définit un type. Nous verrons les différents types de handler possible plus bas.

Chaque handler est ensuite appelé dans l’ordre défini (Attention, on a parfois des handlers imbriqués. Ils ne sont pas appelés par défaut ! C’est le cas ici puisque nested est imbriqué dans main).

Pour expliquer cette configuration :

On a ici un FingersCrossedHandler (handler qui en déclenche un autre, ici il s’agit de main) qui se déclenche seulement lorsque le niveau de log attendu est atteint (ici on attend un log de type error). Ce handler, une fois déclenché, appelle le handler nested. Nested est de type stream (handler qui écrit les logs) qui va écrire les logs dans un fichier à partir d’un level défini (ici tout les logs plus importants ou égaux à debug).

Le handler Nested n’est pas déclenché par défaut car il est imbriqué dans main.

Le handler console est déclenché quant à lui pour tous les logs, nous ne verrons pas ce handler dans cet article, si vous désirez en savoir plus, lisez cet article du blog de Symfony.

Différents types de handles

Il existe plusieurs types de handler avec chacun une fonctionnalité précise :

  • finders_crossed : Ce handler stocke dans un buffer tout les logs qui passe. Lorsqu’un des logs dépasse le niveau minimum requis, il appelle un autre handler avec tous les logs contenus dans son buffer.
  • stream : Ce handler écrit le log qu’il reçoit dans un fichier si son niveau dépasse le niveau minimum requis.
  • rotating_file : Ce handler fait la même chose que stream mais fait une rotation des fichiers pour effacer les logs anciens.
  • group : Ce handler envoit le log reçu à plusieurs handles (exemple : pour écrire le log ET l’envoyer par mail)
  • buffer : Ce handler stocke dans un buffer tout les logs qu’il reçoit puis envoit le buffer à un handler à la fin de l’exécution de la requête.
  • swit_mailler : Ce handle envoit par mail les logs (souvent passé par un handler de type buffer)
  • console : Ce handler permet de définir les niveaux d’affichage de log dans la console.

Nous avons vu avec l’exemple par défaut comment marche les handler finder_crossed et stream.

Nous verrons dans les exemples ci-dessous comment sont utilisés les handlers pour faire ce que l’on veut.

Envoyer les alertes par mail

monolog:
    handlers:
        mail:
            type:         fingers_crossed
            action_level: critical
            handler:      buffered
        buffered:
            type:    buffer
            handler: swift
        swift:
            type:       swift_mailer
            from_email: contact@domaine.com
            to_email:   error@domaine.com
            subject:    Une erreur critique est survenue
            level:      info

Ici on attend un log de niveau critical pour déclencher le handle buffered. Une fois déclenché, le handler buffered va stocker tout les logs et les passer à la fin de l’éxécution de la requête du client au handler swift. Ce dernier va envoyer un mail en triant les logs reçus et en ne gardant que ceux de niveau minimum info.

Contrairement à un handler de type finger_crossed, un handler de type buffer appelle un handler une seule fois avec le contenu de son buffer alors que finger_crossed appelle un autre handler pour chaque log qu’il rencontre.

Rotation des logs

Pour faire la rotation des logs, on va utiliser simplement le handler rotating_file au lieu de steam :

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

Ici on écrit tous les logs de niveau supérieur à debug dans un fichier en rajoutant la date du jour dans le nom du fichier. Au bout de 10 fichiers créés, le plus vieux est supprimé automatiquement dès qu’un nouveau est créé et ainsi de suite. On a donc au minimum 10 jours de logs derrière nous. Vous pouvez augmenter ce paramètre avec max_files.

Les channels

Les logs utilisent des channels pour s’identifier. Par exemple, les logs de doctrine sont sur le channel « doctrine » et ceux sur les authentifications sont sur le channel « security ».

Ainsi on peux lancer des handlers différents en fonction du type de channel.

monolog:
    handlers:
        main:
            type: stream
            path: /var/log/symfony.log
            channels: [!doctrine, !security]
        doctrine:
            type: stream
            path: /var/log/doctrine.log
            channels: doctrine
        login:
            type: stream
            path: /var/log/auth.log
            channels: security

Ici on écrit tous les logs qui ne viennent pas de doctrine, ni de security dans un fichier symfony.log (car le type de handler est stream). On écrit tous les logs de doctrine dans doctrine.log et tout ceux de security dans auth.log.

Vos propres channels

Vous pouvez bien évidemment créer vous aussi vos propres channel pour logger vos log comme vous le souhaitez. Pour cela, ajouter un tag monolog.logger dans vos services :

App\Services\MonService:
    arguments: [@logger]
    public: true
    tags:
        - { name: monolog.logger, channel: mon_channel }

Le channel utilisé pour tous les logs du service sera ici : mon_channel.

Appeler plusieurs handler à partir d’un seul

Imaginons que vous souhaitez que lorsqu’un log de type critical est lu, un mail soit envoyé et qu’il soit écrit dans un fichier avec rotation.

monolog:
    handlers:
        main_critical:
            type:           fingers_crossed
            action_level:   critical
            handler:        grouped
        grouped:
            type:           group
            members:        [streamed, buffered]
        streamed:
            type:           rotating_file
            max_files:      15
            path:           %kernel.logs_dir%/%kernel.environment%.critical.log
            level:          info
        buffered:
            type:           buffer
            handler:        swift
        swift:
            type:           swift_mailer
            from_email:     %email.from%
            to_email:       %email.super_admin%
            subject:        Critical Error Occurred
            level:          error

Et voilà ! Dès qu’un log critical est lu, le handler main_criticale appelle le handler grouped. Le handler grouped qui est de type group va envoyer chaque log vers le handler streamed et vers le handler buffered en même temps. Ces deux handler vont ensuite remplir leurs fonctions : l’un écrire avec rotation de fichier et l’autre stocker dans un buffer avant de l’envoyer au handler qui envoit un mail.

Un exemple complet

Voici un exemple que j’utilise généralement sur mes environnements de prod :

monolog:
    handlers:
        main:
            type:           rotating_file
            max_files:      3
            path:           %kernel.logs_dir%/%kernel.environment%.all.log
            level:          info

        login:
            type:           rotating_file
            max_files:      15
            path:           %kernel.logs_dir%/%kernel.environment%.auth.log
            level:          info
            channels:       security

        main_error:
            type:           fingers_crossed
            action_level:   error
            handler:        streamed_error
        streamed_error:
            type:           rotating_file
            max_files:      15
            path:           %kernel.logs_dir%/%kernel.environment%.error.log
            level:          info

        main_critical:
            type:           fingers_crossed
            action_level:   critical
            handler:        grouped_critical
        grouped_critical:
            type:           group
            members:        [streamed_critical, buffered_critical]
        streamed_critical:
            type:           rotating_file
            max_files:      15
            path:           %kernel.logs_dir%/%kernel.environment%.critical.log
            level:          info
        buffered_critical:
            type:           buffer
            handler:        swift_critical
        swift_critical:
            type:           swift_mailer
            from_email:     contact@domain.com
            to_email:       error@my-domain.com
            subject:        Une erreur critique est survenue !
            level:          info

Tu n’as pas compris ? Explications :

Le handler main sera déclenché pour tous les logs de niveau supérieur ou égal à info et il écrira à chaque fois le log dans un fichier dans app/logs/prod.all-2015-01-05.log (avec la date du jour).

Le handler login fera la même chose mais seulement pour le channel security (les authentifications) et stocke le tout dans un fichier prod.auth-2015-01-05.log

Les logs de niveau error déclencheront le handler main_error qui appellera streamed_error qui va écrire tout les logs du buffer dans un fichier prod.error-2015-01-05.log

Les logs de niveau critical déclencherons le handler main_critical qui va à la fois écrire tout les logs dans un fichier prod.critical-2015-01-05.log et à la fois envoyer un mail avec le buffer pour prévenir de l’erreur survenue (les deux handlers sont déclenchés par le handler group : grouped_critical).

C’est fini ! N’hésitez pas à poser vos questions dans les commentaires et à partager cet article ! Merci

Commentaire (24)

  • zogs| 13 mars 2015

    Merci, très utile et bien expliqué !

  • greg| 5 mai 2015

    Hello,

    Je conseil aussi TRES fortement d’activer les logs PHP dans monolog.
    J’en ai parlé lors du dernier SFLive: https://speakerdeck.com/lyrixx/symfony-live-2015-paris-monitorer-sa-prod

  • ypereirareis| 5 mai 2015

    Merci,

    C’est très clair.

  • sebastien| 7 mai 2015

    par hasard saurais-tu s’il y a un moyen de définir les channels au niveau des handlers imbriqués, exemple :

    main:
    type: fingers_crossed
    action_level: error
    handler: grouped
    grouped:
    type: group
    members: [streamed_application, streamed_security, streamed_doctrine]
    streamed_application:
    type: rotating_file
    max_files: 15
    path: « %kernel.logs_dir%/%kernel.environment%_application.log »
    channels: [‘!security’, ‘!doctrine’]
    level: info
    streamed_security:
    type: rotating_file
    max_files: 15
    path: « %kernel.logs_dir%/%kernel.environment%_security.log »
    channels: [‘security’]
    level: info
    streamed_doctrine:
    type: rotating_file
    max_files: 15
    path: « %kernel.logs_dir%/%kernel.environment%_doctrine.log »
    channels: [‘doctrine’]
    level: info

    car sinon j’ai l’impression que la seule alternative est de créer 3 « main handler » en finger_crossed avec pour chacun leur channel… je trouve ça bizarre que ça ne soit pas possible ou je ne comprends pas bien la logique.

    merci d’avance

    • Rémi| 8 mai 2015

      @sebastien, apparemment, les channels sont vérifiés seulement pour les handles de premier niveau. Un handle de premier niveau, s’il est déclenché, appelle ses enfants « de force » sans vérifier qu’ils correspondent ou non au channel.

      C’est vrai qu’il aurait pu être appréciable de pouvoir l’utiliser sur les handles enfants aussi mais si je ne me trompe pas, ce n’est pas prévu comme ça …

      Si tu trouve plus d’infos sur le sujet, je suis intéressé !

  • Cyril| 9 mai 2015

    Merci pour ce tuto vraiment très clair.

  • Pierre| 21 octobre 2015

    Comment dois je m’y prendre si je souhaites écrire les logs dans une base de données MySQL ?
    Je ne trouve rien de concret sur ce besoin …

    Si quelqu’un voit une façon de le faire je suis preneur.

    Dans mon cas j’étais parti sur un service loggator qui se chargerai d’écrire chaque log dans une base mais je ne sais pas trop comment m’y prendre, quel type de handle choisir ?

    Merci

  • MathieuM| 18 juillet 2016

    Merci, très bien expliqué !

  • Mcsky| 7 octobre 2016

    Tu détailles les fonctionnalités les plus intéressantes de Monolog, très utile merci !

  • Hybernatus| 26 octobre 2016

    Salut, comment je peux faire pour empecher l ecriture de mes logs persos en prod?

    • Rémi| 28 octobre 2016

      @Hybernatus, il faut définir un channel à vos logs persos ou un niveau de criticité bas (debug par exemple). Puis il faut l’exclure, exemple :
      main:
      type: rotating_file
      max_files: 3
      path: %kernel.logs_dir%/%kernel.environment%.log
      level: info
      channels: !perso

  • Amine| 16 janvier 2017

    Merci pour ce tuto 🙂

  • Mapito| 17 février 2017

    Rémi,
    Cet article est très clair et très utile.

    merci 🙂

  • Marco| 22 février 2017

    Merci beaucoup Remi pour ce tuto !
    Très clair, et efficace ! 😀

  • Krakott| 12 avril 2017

    Merci pour ce partage, très clair, très sympa.

  • Draeli| 8 juillet 2017

    Merci pour ce tuto bien plus clair que la documentation officiel, limite tu devrai proposer ta version dessus 🙂

  • skp| 2 octobre 2017

    Merci beaucoup pour ce tuto et aussi pour ton exemple que j’ai utilisé comme base pour mon projet.

    Outre les petits changements, j’ai remplacé le handler:
    buffered_critical:
    type: buffer
    handler: swift_critical

    par:
    deduplicated_critical:
    type: deduplication
    time: 15
    handler: swift_critical

    du coup j’ai modifié le handler:
    grouped_critical:
    type: group
    members: [streamed_critical, deduplicated_critical]

    Merci encore.

  • Thinh| 29 novembre 2017

    2 ans après, toujours très utile, merci !

  • Jassem| 29 mars 2018

    Plus de 3 ans, et toujours très utile,
    Merci beaucoup pour ce tuto !

  • Someone| 29 mars 2018

    Je confirme, merci pour la doc’

  • aguidis| 26 mai 2018

    Merci pour cet article qui est tjrs aussi utile 😉

  • Harry79| 22 juin 2018

    Trop cool et trop bien expliqué. Pour ma part c’est tellement bien fait.
    Merci beaucoup.

  • Caplande| 26 janvier 2020

    Merci pour le gain de temps que vous m’avez apporté dans la compréhension du processus.

  • Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *