---
title: Journalisation d'événements
author: Steve Kossouho
---

# Journaliser des événements en Python

----

## Journaliser ?

Lorsque l'on exécute un programme au long cours, souvent un service, un serveur ou même une application graphique,
il est souvent important de pouvoir retracer ce qui s'est produit avant qu'une exception soit levée, ou qu'un comportement
inattendu montre le bout de son nez.

----

La journalisation est un outil formidable à cet effet, puisqu'elle vous permet de consigner, dans la console ou tout
autre support, une liste chronologique de messages et d'événements que vous pouvez retracer _a posteriori_. Pour peu
que vous consigniez assez de messages, ils peuvent être capitaux, ne serait-ce que pour fournir du support à 
vos utilisateurs.

----

## À quoi ça ressemble ?

La journalisation est sensiblement identique sur tous les systèmes. Vous consignez des messages avec des niveaux de sévérité divers.

En voici un exemple arbitraire :

```log
2021-01-01 23:51:15 - DEBUG - Logging message 3
2021-01-01 23:51:10 - DEBUG - Logging message 2
2021-01-01 23:50:37 - WARNING - Logging message 1
```

----

Un administrateur ou un outil peuvent consulter ce genre de fichier
pour en savoir plus sur leur système. En marquant les messages comme possédant un niveau de sévérité, il est aussi possible de voir
rapidement quels messages doivent être traités en priorité.

----

Les niveaux de sévérité d'un système de journalisation sont les suivants :

- `DEBUG` : Pour les développeurs, remplace le `print`.
- `INFO` : Information standard (version, action effectuée etc.)
- `WARNING` : Attention à porter à un problème potentiel.
- `ERROR` : Une erreur s'est produite, gênante pour les utilisateurs.
- `CRITICAL` : Une erreur irrécupérable à corriger immédiatement s'est produite.

----

## Le module `logging`

La bibliothèque standard de Python propose un module, qui semble inspiré d'une API C++ ou Java (typographie) et qui offre
les outils nécessaires à organiser une bonne journalisation.

----

```{.python .numberLines}
import logging

logger = logging.getLogger(__name__)
```

Récupère le logger portant le nom du module en cours. Attention, si le module est celui que vous avez lancé spécifiquement, la variable
`__name__` (ou l'attribut `__name__` du module) prendra la valeur `"__main__"`.

----

```{.python .numberLines}
import logging

logger = logging.getLogger(__name__)
logger.log(logging.WARNING, "Message.")
logger.warning("Same message as before.")
```

Consignation de nouveaux messages.

----

## Système de loggers

Un logger est un objet que vous utilisez pour consigner de nouveaux messages dans votre système. Cet objet doit être associé à un ou plusieurs objets de type `handler`, qui sont des canaux de sortie pour le logger.

----

## Hiérarchie des loggers

Les loggers portent tous un nom. Le système de journalisation de Python fonctionne avec un système de hiérarchie, où le nom d'un logger définit où il se trouve dans la hiérarchie. Un message consigné par un logger est transmis à son logger parent, jusqu'au logger racine.
<div style="text-align: right">→</div>

----

![Exemple de configuration](assets/images/logging-hierarchy.png) 

----

Dans l'exemple précédent,

* Un message consigné avec le logger nommé `data.excel` remonte jusqu'au au logger racine.
* Tous les handlers associés aux loggers rencontrés en remontant la hiérarchie sont utilisés pour traiter le message.
* Si aucun handler n'a été rencontré après être arrivé au sommet, le handler de dernier recours (`lastResort`) est utilisé pour consigner l'événement.
* Ce handler affiche les messages de sévérité `>= logging.WARNING`{.python} dans la console d'erreur (rouge).

----

## Utiliser les handlers

Le clou du spectacle consiste à configurer des handlers pour les associer à des loggers. Pour configurer correctement un handler, il nous faut définir :

- Son type (sortie fichier, console);
- Le format textuel des événements consignés;
- Son niveau de sévérité minimum pour accepter des messages.

----

### Configuration d'un handler

```{.python .numberLines}
import logging
handler = logging.FileHandler("filename.log", encoding="utf-8")
# Chaîne de formatage des événements
formatting = logging.Formatter("%(asctime)s - %(message)s")
handler.setFormatter(formatting)
# Le handler acceptera tous les messages >= DEBUG
handler.setLevel(logging.DEBUG)
```

[Référence des spécifieurs pour le format de texte](https://docs.python.org/3/library/logging.html#logrecord-attributes)

----

## Types de handlers

Les types de handlers disponibles sont disponibles ici :

- `logging.FileHandler` : sérialise dans un fichier ([documentation](https://docs.python.org/3/library/logging.handlers.html#filehandler)).
- `logging.StreamHandler` : sérialise dans un flux, généralement console (voir slide suivant).
- `logging.handlers.*` : handlers plus avancés, dont fichiers avec rotation.

[Handlers supplémentaires](https://docs.python.org/3/library/logging.handlers.html)

----

## Types de flux console

Pour les handlers vers un flux, voici la liste des flux classiques disponibles :

- `sys.stdout` : sortie console normale.
- `sys.stderr` : sortie console d'erreur (affiché en rouge).

*(voir le slide suivant pour l'utilisation de ces flux)*

----

## Ajouter un handler à un logger

Une fois un handler configuré, vous pouvez l'associer à un logger (puisque c'est sur le logger que vous allez utiliser des méthodes de journalisation).

```{.python .numberLines}
import logging
import sys

logger = logging.getLogger("logger.name")
handler = logging.StreamHandler(sys.stdout)
# Imaginons que l'on configure notre handler correctement
# (ici, il manque un format à spécifier)
# On peut *ajouter* notre handler à notre logger
logger.addHandler(handler)
```

----

Un logger peut également définir son propre niveau minimal de sévérité, qui sera utilisé pour potentiellement limiter la portée de ses handlers.

```{.python .numberLines}
import logging
import sys

logger = logging.getLogger("logger.name")
handler = logging.StreamHandler(sys.stdout) # il faudrait spécifier un format
logger.addHandler(handler)
# Limiter tous les handlers associés au niveau WARNING
logger.setLevel(logging.WARNING)
```

----

## Le logger racine

Le logger racine est souvent utilisé pour configurer une journalisation globale pour toute votre application, puisque tout logger remonte toujours ses messages vers celui-ci.

Pour le configurer, utilisez la variable `root` du module `logging` :

```{.python .numberLines}
import logging
# Exemple minimaliste
handler = logging.FileHandler("filename")
# Tous les loggers vont passer par ce handler
logging.root.addHandler(handler)
```

----

## Récapitulatif minimal

```{.python .numberLines}
import logging

# Les 3 types d'objets nécessaires à la configuration
logger = logging.getLogger("myname")  # nouveau logger nommé "myname"
handler = logging.FileHandler("myfile.log", encoding="utf-8")  # handler fichier
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")  # format de messages

# Configuration du handler
handler.setFormatter(formatter)  # configurer le handler
handler.setLevel(logging.DEBUG)  # niveau de sévérité minimum

# Configuration du logger
logger.addHandler(handler)  # ajouter le handler au logger
logger.setLevel(logging.INFO)  # niveau de sévérité minimum du logger
```

----

### Exemple de logger à deux handlers

![Exemple d'usage d'un logger](assets/images/logging-building.png)

--- 

## Plus : Fichier de configuration

On peut configurer sa journalisation et créer d'une traite un ensemble de `loggers` via un fichier 
de configuration (il est possible d'utiliser un dictionnaire), qui
sera stocké sous la forme d'un fichier ini. Le contenu à insérer dans ce fichier est décrit dans la section 
[Configuration file format](https://docs.python.org/3/library/logging.config.html#configuration-file-format)
de la documentation Python.

Les fichiers **ini** ont une structure qui est toujours de la forme :

```ini
[section1]
property1 = value
property2 = value

[section2]
property3 = value
```

----

### Charger un fichier de configuration

Lorsque vous avez défini un fichier de configuration avec tous ses `loggers`, `handlers` et 
`formatters`, vous pouvez l'utiliser via la fonction [`logging.config.fileConfig()`](https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig) :

```{.python .numberLines}
from logging.config import fileConfig

# Initialiser des loggers depuis une fonction
# Le fichier devrait être stocké dans un dossier à l'arborescence bien
# choisie.
fileConfig("logging.conf.ini", disable_existing_loggers=False, encoding="utf-8")
```

----

## Bibliothèques simplifiées de journalisation

- [Loguru](https://loguru.readthedocs.io/en/stable/overview.html)
