Symfony : 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 (26)
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
Rémi| 21 octobre 2015
@Pierre, il n’existe pas de handle de base permettant de faire ça. En revanche vous pouvez sûrement créer votre propre handle. Une piste ici : http://stackoverflow.com/questions/12935979/custom-monolog-handler-for-default-monolog-in-symfony-2
J’avoue que je n’ai jamais essayé. N’hésitez pas à en reparler si vous en apprenez plus sur le sujet !
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.
BigBenJr| 7 mai 2024
Bonjour et merci pour cet article. J’ai une question par rapport au code ci-dessous:
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
Est-il vraiment nécessaire de faire passer les logs par un buffer alors que le finger_crossed est déjà en lui-même un buffer ? Si j’ai bien compris il stock les logs dans son buffer et lorsqu’une erreur critique est rencontrée, il envoie son buffer à son handler. Donc pourquoi passer par un deuxième buffer ? Merci.
Rémi| 7 mai 2024
Salut, ça fait longtemps que je ne me suis pas plongé dans le sujet mais de mémoire c’est pour afficher la trace totale des logs dans le mail. Le buffer va récolter les logs, même les non critiques avant de les envoyer par mail. Sans ça, ton mail ne contiendrait que le log critique.