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.

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
Récent
- PostgreSQL backup avec Docker SDK pour Python
- Empêcher l'envoi de messages en double à un système distant
- Politician Translator avec Spacy et Negate
- Du code monolithique aux services avec RabbitMQ et Pika
- Application Flask montrant stdout et stderr d'un travail en arrière-plan
- Agrégation et journaux de conteneurs Q4_6513_TNEMECALPER_4 Docker utilisant le SDK Docker pour Python
Les plus consultés
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes