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

Agrégation et journaux de conteneurs Q4_6513_TNEMECALPER_4 Docker utilisant le SDK Docker pour Python

Utilisez grep, sed, threading et Docker SDK for Python pour consigner les erreurs dans un seul fichier.

11 décembre 2022
post main image

Le problème : Vous avez finalement une application Docker composée de nombreux (micro) services (conteneurs) et vous voulez surveiller tous ces conteneurs pour détecter les erreurs.

Les services sont pour la plupart des scripts Python qui utilisent le module de journalisation standard Python et impriment des messages vers stdout (et stderr) :

... DEBUG ... 
... INFO ... 
... ERROR ...

En utilisant le pilote de journalisation par défaut de Docker json-file , ces messages se retrouvent dans les journaux de Docker . Nous pouvons voir les journaux pour un conteneur en utilisant la commande :

docker logs <container-id>

docker logs <container-name>

Dans ce post, je présente un moyen de visualiser les erreurs de tous les conteneurs dans un seul fichier. Nous n'utilisons pas 'container_name : <container-name>' dans Docker-compose. Pour sélectionner et accéder aux conteneurs Docker , nous utilisons le SDK Docker pour Python.

Étapes :

  • Filtrer les lignes de log avec grep et sed
  • Docker et le nommage des conteneurs
  • Sélection et accès aux conteneurs avec le SDK Docker pour Python
  • Démarrage de plusieurs processus de journalisation Docker
  • Le code
  • Test de

Filtrage des lignes de log avec grep et sed

Pour tail le journal Docker nous utilisons la commande suivante qui montre aussi les messages envoyés à stderr :

docker logs --timestamps --follow <container-id> 2>&1

Dans les commandes ci-dessous, nous ajoutons un argument 'do not buffer' pour nous assurer que nous ne transmettons pas de parties de messages.

Nous ne voulons pas voir les lignes du journal contenant 'DEBUG', mais seulement les lignes contenant 'ERROR' et 'WARNING'. Parce que nous avons également d'autres services, nous recherchons également la casse :

grep --line-buffered -v DEBUG | grep --line-buffered -i -e "ERROR" -i -e "WARNING"

Comme nous avons plusieurs journaux de conteneurs, nous insérons le nom du conteneur au début d'une ligne de journal :

sed -u "s/^/<container-name> /"

Enfin, nous ajoutons ces lignes à un seul fichier journal :

>> my_app.errors.log

Voici la ligne complète :

docker logs --timestamps --follow <container-id> 2>&1 | grep --line-buffered -v DEBUG | grep --line-buffered -i -e "ERROR" -i -e "WARNING" | sed -u "s/^/<container-name> /" >> my_app.errors.log

Puis, dans une autre fenêtre de console, nous tail ce fichier :

tail - f my_app.errors.log

Docker et le nommage des conteneurs

Dans la Docker, il est possible d'attribuer un nom à un conteneur. Cela fonctionne bien s'il n'y a qu'un seul conteneur de ce type. Mais si vous voulez exécuter plusieurs instances (répliques) du conteneur, vous ne pouvez pas le faire et devez laisser Docker générer les noms des conteneurs.

Un nom de conteneur généré par Docker ressemble à ceci :

<project-name>-<service-name>_1

Les répliques multiples seront numérotées '_2', '_3', etc. La première partie du nom du conteneur est appelée le nom du projet, dans ce cas le nom du répertoire où se trouve le fichier docker-compose.yml.

Vous pouvez également spécifier le nom du projet en utilisant la variable d'environnement COMPOSE_PROJECT_NAME. Par exemple, avec les fichiers docker-compose.yml et .env :

# file: docker-compose.yml
version: "3"

services:
  web:
    image: "nginx:latest"
    env_file:
      - ./.env
    deploy:
      replicas: 3
    ports:
      - "1000-1002:80"
# file: .env
COMPOSE_PROJECT_NAME=MY_PROJECT

Puis, en faisant apparaître le projet en utilisant :

docker-compose up -d

Et le résultat est :

Creating my_project_web_1 ... done
Creating my_project_web_2 ... done
Creating my_project_web_3 ... done

Sélection et accès aux conteneurs avec le SDK de Docker pour Python

Nous spécifions les conteneurs pour lesquels nous voulons afficher les journaux en utilisant les noms de projet et de service. Si nous omettons les noms de service, alors tous les services d'un projet sont sélectionnés. Si nous voulons sélectionner tous les conteneurs du projet 'mon_projet' :

projects_services = {
    'my_project': []
}

Si nous voulons sélectionner uniquement le service 'web' du projet 'mon_projet' :

projects_services = {
    'my_project': ['web']
}

Pour sélectionner les conteneurs dans Python, nous utilisons le SDK Docker pour Python. Nous utilisons les noms des projets pour rechercher les conteneurs. Par défaut, nous sélectionnons tous les services d'un projet.

Dans votre virtual environment, installez le Docker SDK pour Python :

pip install docker

Pour obtenir tous les conteneurs :

    client = docker.from_env()
    containers = client.containers.list()

Pour chaque conteneur, le SDK Docker expose un dictionnaire de labels avec des clés incluant :

com.docker.compose.container-number
com.docker.compose.project
com.docker.compose.service 

Avec celles-ci, nous pouvons sélectionner les conteneurs.

Démarrage de plusieurs processus de journalisation Docker

Il existe plusieurs façons de lancer plusieurs processus de journalisation Docker . Comme nous utilisons Python , nous le faisons avec des threads et subprocess. Une fois que nous avons la commande, voir ci-dessus, nous démarrons le processus comme suit :

    cmd = ...
    p = subprocess.Popen(cmd, shell=True)
    p.wait()

Les conteneurs Docker peuvent être présents, non présents, en haut, en bas, ce qui signifie que nous devons continuellement (toutes les quelques secondes) vérifier si notre commande Docker logs est toujours en cours d'exécution, et si non, la relancer.

Le code

Si vous voulez essayer, voici le code :

# file: dockertail.py
import docker
import logging
import subprocess
import sys
import threading
import time

def get_logger():
    logger_format = '[%(asctime)s] [%(levelname)s] %(message)s'
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    # console
    console_handler = logging.StreamHandler(sys.stdout)                             
    console_handler.setLevel(logging.DEBUG)
    console_handler.setFormatter(logging.Formatter(logger_format))
    logger.addHandler(console_handler)                                            
    return logger

logger = get_logger()

# empty list means all services
projects_services = {
    'my_project': [],
}

class DockerTail:
    def __init__(self):
        self.client = docker.from_env()

    def get_docker_containers(self):
        return self.client.containers.list()

    def discover_docker_containers(self):
        logger.debug('containers:')
        for container in self.get_docker_containers():
            labels = container.labels
            logger.debug('- project  = {}'.format(labels['com.docker.compose.project']))
            logger.debug('- service  = {}'.format(labels['com.docker.compose.service']))
            logger.debug('- container.name = {}'.format(container.name))
            logger.debug('- short_id = {}'.format(container.short_id))
            logger.debug('- number   = {}'.format(labels['com.docker.compose.container-number']))

    def get_projects_containers_by_projects_services(self, projects_services):
        # group by project
        projects_containers = {}
        for container in self.get_docker_containers():
            labels = container.labels
            project = labels['com.docker.compose.project']
            service = labels['com.docker.compose.service']
            if project in projects_services:
                if service in projects_services[project] or len(projects_services[project]) == 0:
                    if project not in projects_containers:
                        projects_containers[project] = []
                    projects_containers[project].append(container)
        # sort projects by name
        projects_containers = dict(sorted(projects_containers.items()))            
        for project, containers in projects_containers.items():
            # sort project containers by service name
            containers.sort(key=lambda x: x.labels['com.docker.compose.service'])
        # show what we got
        logger.debug('projects:')
        for project, containers in projects_containers.items():
            logger.debug('- {}'.format(project))
            logger.debug('  services:')
            for container in containers:
                labels = container.labels
                logger.debug('  - {}'.format(labels['com.docker.compose.service']))
                logger.debug('      name:      {}'.format(container.name))
                logger.debug('      number:    {}'.format(labels['com.docker.compose.container-number']))
                logger.debug('      status:    {}'.format(container.status))
                logger.debug('      short_id:  {}'.format(container.short_id))
        return projects_containers

def docker_logs_follow(project, container):
    cmd = 'docker logs --timestamps --follow {} 2>&1'.format(container.short_id)
    cmd += ' | grep --line-buffered -v "DEBUG"'
    cmd += ' | grep --line-buffered -i -e "ERROR" -i -e "WARNING" '
    cmd += ' | sed -u "s/^/{} /"'.format(container.name)
    cmd += ' >> my_app.errors.log'
    logger.debug('cmd = {}'.format(cmd))
    p = subprocess.Popen(cmd, shell=True)
    p.wait()

def main():
    dt = DockerTail()
    dt.discover_docker_containers()

    container_name_threads = {}
    # start 'docker logs <container-id> ...' 
    while True:
        projects_containers = dt.get_projects_containers_by_projects_services(projects_services)
        for project, containers in projects_containers.items():
            for container in containers:
                t = container_name_threads.get(container.name)
                if t is None or not t.is_alive():
                    logger.debug('starting docker logs follow for {}:{}:{}'.format(project, container.labels['com.docker.compose.service'], container.name))
                    t = threading.Thread(target=docker_logs_follow, args=(project, container))
                    t.start()
                    container_name_threads[container.name] = t
        time.sleep(2)

if __name__ == '__main__':
    main()

Test

Lancez l'exemple Docker-compose avec les trois répliques comme indiqué précédemment :

docker-compose up -d

Dans une autre fenêtre de console, lancez dockertail.py :

python dockertail.py

Et dans une autre fenêtre de console tail notre fichier journal d'erreur d'application, l'agrégation des messages d'erreur de tous les conteneurs d'application :

tail -f my_app.errors.log

Maintenant dans la première fenêtre, accédons à un conteneur sans générer d'erreur :

wget 127.0.0.1:1001

Répétez cette opération avec une autre URL pour générer une erreur :

wget 127.0.0.1:1001/nothing

Cela imprimera le message d'erreur suivant dans mon_app.errors.log tail :

my_project_web_3 2022-12-11T10:44:45.239669141Z 2022/12/11 10:44:45 [error] 36#36: *7 open() "/usr/share/nginx/html/nothing" failed (2: No such file or directory), client: 172.17.82.1, server: localhost, request: "GET /nothing HTTP/1.1", host: "127.0.0.1:1001"

Bien.

Une seule ligne d'exceptions

Presque parfait. Dans l'exemple ci-dessus, lorsqu'une exception se produit dans un script Python , nous ne voyons que la première ligne. Le traceback n'est pas visible. Si nous ne voulons pas toucher à notre script dockertail.py, il n'y a qu'une seule façon d'afficher la traceback dans notre journal, et c'est d'imprimer l'exception sur une seule ligne. Vous pouvez trouver des exemples sur Internet sur la façon de le faire.

Une autre façon serait de commencer chaque ligne de traceback avec un prefix spécial et de modifier notre filtre grep ci-dessus pour qu'il passe toujours ces lignes.

Résumé

Je cherchais un moyen très simple de surveiller les journaux de plusieurs conteneurs Docker à la recherche d'erreurs. Dans de nombreuses applications basées sur Docker , les conteneurs sont nommés par Docker. Docker API pour Python est un bel outil pour découvrir et sélectionner les conteneurs Docker en utilisant le nom du projet et les noms des services. Pour filtrer les erreurs dans les journaux des conteneurs Docker , nous utilisons la commande grep Linux et utilisons la commande sed pour ajouter le nom du conteneur au message du journal.

Une fois lancé, tout ce que nous avons à faire est de regarder la sortie du fichier journal agrégé en utilisant :

tail -f my_app.errors.log

Automatisez ou arrêtez !

Liens / crédits

Docker SDK for Python - Containers
https://docker-py.readthedocs.io/en/stable/containers.html

How can a container identify which container it is in a set of a scaled docker-compose service?
https://stackoverflow.com/questions/39770712/how-can-a-container-identify-which-container-it-is-in-a-set-of-a-scaled-docker-c/39895650#39895650

How do I can format exception stacktraces in Python logging?
https://stackoverflow.com/questions/28180159/how-do-i-can-format-exception-stacktraces-in-python-logging

How to reach additional containers by the hostname after docker-compose scale?
https://stackoverflow.com/questions/36031176/how-to-reach-additional-containers-by-the-hostname-after-docker-compose-scale

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.