J’ai bien galéré là… alors ce billet est autant pour moi (pour mémoire) que pour être utile à quelqu’un qui aurait les mêmes galères.

La problématique est simple, j’utilise Flask (pour Pydéfis par exemple), et je veux configurer des logs propres. J’aimerais pouvoir choisir l’endroit où loguer (fichier, console, les deux…), le niveau etc..

Tout est prévu dans Flask pour ça… mais j’ai quand même mis beaucoup de temps avant que ça marche correctement, et je suis tombé sur beaucoup de pages Web de personnes qui galéraient alors…

Par défaut l’application créée avec flask (on l’appelle app) crée un logger. Il est dans app.logger. On peut donc loguer des messages ainsi : app.logger.debug("Message de débug").

C’est parfait, et ça marche sans config.

Les problèmes arrivent si on veut tuner le truc. Voici deux exemples de config. J’ai retenu la première mais la seconde est celle que j’ai pu faire marcher en premier.

Configuration avec logging

Le module logging permet de configurer les loggers (ici root) par le biais d’un dictionnaire :

logger_config = {
    'version': 1,
    'formatters': {
        'default': {
            'format': '%(asctime)s :: %(levelname)s :: %(threadName)s :: %(module)s :: %(message)s',
        }
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'default',
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': "fichier_de_log.log",
            'formatter': 'default',
        },
    },
    'root': {
        'level': "INFO",
        'handlers': ['console', 'file']
    }
}
logging.config.dictConfig(logger_config)

J’ai deux handlers, un pour enregistrer dans un fichier, et un pour envoyer sur la console. le logger configuré est root, il logue à partir du niveau INFO et utilise les deux handlers. Chacun de ces handlers utiliser le même formatter, nommé default.

On règle le tout avec logging.config.dictConfig(logger_config) et c’est parti. Il faut que cette configuration soit faite avant la première utilisation de app.logger. Sinon app.logger aura lui même tenté de créer des handlers, qui ne seront pas forcément ceux que vous voudrez (et par exemple, le format de logs que vous allez obtenir ne sera pas le bon).

C’est indiqué dans la doc, mais un autre problème (lié à l’encodage) ne m’a pas permis de faire fonctionner ça initialement. Maintenant c’est OK.

Configuration tardive

La configuration peut aussi être faite éventuellement après le premier appel à app.logger, mais il faut s’y prendre complètement différemment :

# Réglage du level
app.logger.setLevel("INFO")
# Éventuellement, on vire tous les handlers déjà créés
# peut être automatiquement par Flask
for h in app.logger.handlers:
    app.logger.removeHandler(h)
# Et finalement, on ajoute son propre handler (ici j'en ajoute un seul)
handler = FileHandler("fichier_de_log.log")
# On peut régler le niveau du handler indépendamment (on pourrait avoir
# 2 handlers qui logues avec deux niveaux différentes)
handler.setLevel("INFO")
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(threadName)s :: %(message)s')
handler.setFormatter(formatter)
app.logger.addHandler(handler)
# Si on vérifie, il doit y avoir un seul élément dans la liste
print(app.logger.handlers)
# Au passage, on peut utiliser le même handler pour les messages
# qui viennent de werkzeug
logging.getLogger("werkzeug").addHandler(handler)

Locales, encodage etc

Il se trouve que j’ai eu un autre problème, les locales étaient mal réglées sur le système (la variable LANG était correcte, mais les locales n’avaient pas été créées).

Si ça arrive, Python utiliser comme encodage de l’ascii, et si vous loguez des messages avec des accents, ça ne marche plus…

On peut diagnostiquer le problème ainsi :

>>> import locale
>>> locale.getlocale()
(None, None)  # <=== ça va pas !!!

Normalement, on devrait avoir :

>>> import locale
>>> locale.getlocale()
("fr_FR", "UTF-8")

On peut forcer l’utilisation d’un encodage particulier dans certains cas (pour FileHandler par exemple, mais c’est bien plus tordu pour StreamHandler)

# Dans le cas de la méthode 1
...
         'file': {
             'class': 'logging.FileHandler',
             'filename': "fichier_de_log.log",
             'formatter': 'default',
             'encoding': 'utf8',
         }
# Dans le cas de la méthode 2
handler = FileHandler("fichier_de_log.log", encoding="utf8")

Ça règle le problème si on logue dans un fichier, mais on peut pas le faire simplement pour la console. Le mieux est de réparer le système pour que Python voit bien que vous voulez de l’UTF8 et pas de l’ASCII.