angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

journalisation des applications Python avec Docker

Les meilleures pratiques de Docker suggèrent de se connecter à stdout mais il y a quelques écueils.

5 décembre 2022
Dans Docker, Logging
post main image
https://unsplash.com/@agk42

Lorsque vous développez une application logicielle, la première chose que vous allez mettre en place est probablement la journalisation. D'abord uniquement vers la console, mais bientôt vous ajouterez des fichiers journaux.

Il y a quelques années, j'ai commencé à utiliser Docker pour le développement et la production. Je n'ai pas fait beaucoup de changements lors du passage à la Docker. Les fichiers journaux de l'application se trouvent toujours dans un répertoire de journaux sur un volume Docker . Cela signifie que les fichiers journaux font partie de l'application framework.

Les meilleures pratiques sur Internet suggèrent d'utiliser la journalisation Docker . Cela signifie que notre application Docker doit imprimer les enregistrements de journal vers stdout (et/ou sterr), au lieu de les écrire dans les fichiers journaux de notre application. Lorsque nous envoyons nos enregistrements de journaux à stdout dans Docker, nous pouvons spécifier un pilote de journalisation pour les exporter vers un outil externe tel que Syslog, en utilisant les pilotes de journaux fournis par Docker ou des pilotes de journaux tiers. Je comprends que cela puisse avoir des avantages, voire même être essentiel.

Encore une fois, je ne veux pas trop changer et m'en tenir au pilote de journalisation par défaut dans Docker : json-file. Nos enregistrements de logs seront présents dans les fichiers :

/var/lib/docker/containers/<container id>/<container id>-json.log

L'un de mes projets actuels Python est une application composée d'un grand nombre de (micro) services. Un service est représenté par un conteneur avec des fichiers journaux toujours dans le contexte / framework du service. Dans ce post, je convertis la méthode de journalisation de mon application existante à la journalisation Docker .

Changer le module de journalisation

Pour mes applications, j'ai développé un module de journalisation personnalisé, à l'intérieur de ce module il y a les méthodes de journalisation standard Python , ce qui signifie que mon code d'application a des lignes comme :

logger.debug(...)
logger.info(...)
logger.error(...)

J'utilise ce module de journalisation partout. Cela signifie que j'avais seulement besoin d'ajouter un nouveau mode de journalisation, en utilisant une variable, use_docker_logging=True, qui indique d'écrire les données de journalisation dans stdout au lieu d'un fichier.

Nous pouvons faire cela très facilement en utilisant logging.StreamHandler(stdout). En utilisant le module de journalisation Python , les données sont vidées après chaque enregistrement. Il n'est donc pas nécessaire de lancer Python en tant que 'python -u' ou d'utiliser la variable d'environnement PYTHONUNBUFFERED.

Docker Exécution, problèmes, où sont mes enregistrements de journal ?

Malheureusement, nous n'avons pas fini, il y a un problème. Je ne sais pas pour vous, mais je fais tout mon développement en utilisant les conteneurs Docker aujourd'hui. Dans de nombreux cas, j'ai la commande dans le fichier docker-compose.yml comme :

command: tail -f /dev/null

Cela signifie, démarrer le conteneur et le garder en vie. Ensuite, j'exécute Docker Exec dans le conteneur (shell) et je lance les scripts Python . Lorsque j'ai fait cela, les enregistrements de la session 'Docker Exec' n'apparaissaient pas dans les journaux de Docker .

Après une recherche sur Internet, j'ai trouvé cette page (Docker issues) 'Proposal: additional logging options for docker exec', voir les liens ci-dessous.

Il semble que lorsque vous exécutez la Docker dans un conteneur, la stdout de cette session n'est pas la stdout de la session initiale. Il y aura des raisons à cela, nous ne pouvons pas le changer, et nous devons faire avec.

La solution consiste à rediriger les enregistrements du journal vers :

/proc/1/fd/1

J'ai ensuite trouvé une autre page (Docker ) sur internet 'Echoing to /dev/stdout does not appear in docker logs', voir les liens ci-dessous. L'une des suggestions est d'enregistrer dans un fichier sym-linké. Nous créons le symlink dans le Dockerfile :

RUN ln -sf /proc/1/fd/1 /var/log/test.log

Pour revenir à la solution que nous avions jusqu'à présent, nous remplaçons la logging.StreamHandler par la logging.FileHandler :

logging.FileHandler('/var/log/test.log')

Maintenant, les enregistrements des scripts Python exécutés dans la session 'Docker Exec', apparaissent dans les journaux Docker .

Session Docker Exec, pas d'enregistrement dans la console.

Malheureusement, nous n'avons pas fini, il y a un autre problème. Parce que nous avons utilisé le logging.FileHandler, nous n'enregistrons que dans un fichier, le stdout, étant les journaux du Docker . Pour voir les enregistrements à l'écran dans la session 'Docker Exec', nous devons ajouter à nouveau la logging.StreamHandler .

Mais attendez, nous ne devons le faire que pour la session 'Docker Exec', sinon nous verrons des enregistrements en double dans les journaux de Docker .

J'ai résolu ce problème d'une manière un peu artisanale, en trouvant le nom du processus parent supérieur. Si ce nom de processus est 'sh' ou 'bash' alors je suppose que nous avons utilisé Docker Exec pour entrer dans le conteneur.

import psutil

    ...
    # get 'top' parent process of this docker exec session
    parent_process_pid = os.getpid()
    parent_process_name = None
    while True:
        #print('parent_process_pid = {}'.format(parent_process_pid))
        parent_process = psutil.Process(parent_process_pid)
        pid = parent_process.ppid()
        parent_process_name = parent_process.name()
        if pid == 0:
            break
        parent_process_pid = pid

    if parent_name in ['sh', 'bash']:
        # add console logging
        ...
    else:
        # no console logging
        ...
    ...

Lignes de journal avec des champs supplémentaires

Tous les enregistrements d'un conteneur sont maintenant dans un seul fichier journal. Si vous avez plusieurs services (processus) s'exécutant dans un conteneur, vous voudrez probablement ajouter des champs supplémentaires aux lignes de journal qui identifient le service (processus). Ceci en plus du champ logging.DEBUG, logging.ERROR etc. que nous insérons dans les lignes de journal.

Dans la journalisation Python , nous pouvons ajouter un champ supplémentaire à une ligne de journal avec la méthode logging.setLogRecordFactory() . Un exemple est donné sur la page 'Using LogRecordFactory in python to add custom fields for logging', voir les liens ci-dessous.

Autres changements

Notez qu'avec le pilote de journalisation Docker par défaut, nous ne pouvons écrire qu'une chaîne de caractères dans stdout, l'écriture d'un objet JSON (dictionnaire) n'est pas possible. C'est une limitation sévère, mais nous devons nous en accommoder.

Si nous voulons ajouter des étiquettes à tous ( !) les enregistrements du journal, nous pouvons utiliser les étiquettes dans Docker-Compose :

Exemple :

  some-service:
    image: ...
    ports:
      - "8082:8000"
    labels:
      log_for_application: "myapp"

Lors de la journalisation vers Docker, l'horodatage est ajouté automatiquement par Docker. Cela signifie que nous devons le supprimer de notre ligne de log Python . Ma chaîne finale de formatage du logger ressemble à ceci :

'%(proc_id)-15.15s %(levelname)-8.8s [%(filename)-30s%(funcName)20s():%(lineno)03s] %(message)s'

Afficher les journaux Docker de tous les conteneurs avec Dozzle

Il existe de nombreuses solutions pour afficher les journaux Docker de tous les conteneurs. J'en ai essayé quelques-unes, et la plus facile à mettre en place et à utiliser est Dozzle. Dozzle est un visualisateur de logs en temps réel pour les conteneurs docker, voir les liens ci-dessous. Pour l'exécuter, il suffit de tirer le conteneur, et vous êtes prêt à partir. Dozzle ressemble à Logs Explorer, une extension de Docker Desktop, voir les liens ci-dessous.

Attention, par défaut Dozzle vous connecte à Google Analytics, assurez-vous de lancer Dozzle avec le drapeau '--no-analytics' :

docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest --no-analytics

Une fois lancé, pointez votre navigateur vers :

http://127.0.0.1:8888

Avec Dozzle , il est très facile de visualiser vos journaux Docker .

La recherche utilise regex, ce qui signifie que si vous voulez par exemple filtrer sur deux termes, 'Term1' et 'Term2',
vous pouvez taper dans la boîte de recherche :

Term1.*?Term2

Une autre chose intéressante avec Dozzle est que nous pouvons le lancer aussi pour un seul conteneur, ou quelques conteneurs. Ce qui manque vraiment, c'est un moyen de stocker et de resélectionner vos recherches. Mais je ne me plains pas. Super outil !

Résumé

Cela a pris beaucoup plus de temps que prévu. C'était bien plus qu'une simple impression sur stdout. Cela en vaut-il la peine ? Pour l'application actuelle, qui consiste en de nombreux services représentés par des conteneurs, je pense que oui.

Maintenant que la journalisation est effectuée via Docker, je peux envisager d'autres moyens de visualiser les journaux, de générer des alertes, etc.

Liens / crédits

Docker - Configure logging drivers
https://docs.docker.com/config/containers/logging/configure

Dozzle, a real-time log viewer for docker containers
https://dozzle.dev

Echoing to /dev/stdout does not appear in 'docker logs' #19616
https://github.com/moby/moby/issues/19616

Proposal: additional logging options for docker exec #8662
https://github.com/moby/moby/issues/8662

Using LogRecordFactory in python to add custom fields for logging
https://stackoverflow.com/questions/59585861/using-logrecordfactory-in-python-to-add-custom-fields-for-logging

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.