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

Aggregieren und tail Docker Containerprotokolle mit Docker SDK für Python

Verwenden Sie grep, sed, threading und Docker SDK für Python , um Fehler in einer einzigen Datei zu protokollieren.

11 Dezember 2022
post main image

Das Problem: Sie haben eine Docker -Anwendung, die aus vielen (Mikro-)Services (Containern) besteht, und wollen alle diese Container auf Fehler überwachen.

Die Services sind meist Python Skripte, die das Standard Python Logging Modul verwenden und Nachrichten an stdout (und stderr) ausgeben:

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

Bei Verwendung des standardmäßigen Docker -Protokollierungstreibers json-file landen diese Meldungen in den Docker -Protokollen. Wir können die Protokolle für einen Container mit dem Befehl anzeigen:

docker logs <container-id>

docker logs <container-name>

In diesem Beitrag stelle ich eine Möglichkeit vor, die Fehler aller Container in einer einzigen Datei anzuzeigen. Wir verwenden in Docker-compose nicht 'container_name: <container-name>'. Für die Auswahl und den Zugriff auf die Docker -Container verwenden wir das Docker SDK für Python.

Die Schritte:

  • Filtern von Protokollzeilen mit grep und sed
  • Docker und die Benennung von Containern
  • Auswahl von und Zugriff auf Container mit dem Docker SDK für Python
  • Starten mehrerer Docker -Protokollprozesse
  • Der Code
  • Testen von

Filtern von Protokollzeilen mit grep und sed

Um tail das Docker -Protokoll anzuzeigen, verwenden wir den folgenden Befehl, der auch die an stderr gesendeten Nachrichten anzeigt:

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

In den folgenden Befehlen fügen wir ein "do not buffer"-Argument hinzu, um sicherzustellen, dass wir keine Teile von Meldungen weitergeben.

Wir wollen keine Protokollzeilen mit 'DEBUG' sehen, sondern nur Zeilen mit 'ERROR' und 'WARNING'. Da wir auch andere Dienste haben, suchen wir auch nach Groß- und Kleinschreibung:

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

Da wir mehrere Containerprotokolle haben, fügen wir den Containernamen an den Anfang einer Protokollzeile:

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

Schließlich hängen wir diese Zeilen an eine einzige Protokolldatei an:

>> my_app.errors.log

Hier ist die vollständige Zeile:

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

Dann, in einem anderen Konsolenfenster, tail wir diese Datei:

tail - f my_app.errors.log

Docker und Container-Benennung

In Docker ist es möglich, einem Container einen Namen zuzuweisen. Das funktioniert gut, wenn es nur einen solchen Container gibt. Wenn Sie jedoch mehrere Instanzen (Replikate) des Containers betreiben wollen, können Sie dies nicht tun und müssen die Containernamen von Docker generieren lassen.

Ein von Docker generierter Containername sieht so aus:

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

Mehrere Replikate werden mit '_2', '_3', usw. nummeriert. Der erste Teil des Containernamens ist der Projektname, in diesem Fall der Name des Verzeichnisses, in dem sich die Datei docker-compose.yml befindet.

Sie können den Projektnamen auch über die Umgebungsvariable COMPOSE_PROJECT_NAME angeben. Zum Beispiel mit den Dateien docker-compose.yml und .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

Dann wird das Projekt mit aufgerufen:

docker-compose up -d

Und das Ergebnis ist:

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

Auswahl von und Zugriff auf Container mit dem Docker SDK für Python

Wir geben die Container, für die wir die Protokolle anzeigen wollen, über die Projekt- und Servicenamen an. Wenn wir die Servicenamen weglassen, dann werden alle Services eines Projekts ausgewählt. Wenn wir alle Container aus dem Projekt 'my_project' auswählen wollen:

projects_services = {
    'my_project': []
}

Wenn wir nur den Dienst "web" aus dem Projekt "my_project" auswählen wollen:

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

Um die Container in Python auszuwählen, verwenden wir das Docker SDK für Python. Wir verwenden die Projektnamen, um die Container zu suchen. Standardmäßig wählen wir alle Dienste eines Projekts aus.

Installieren Sie in Ihrem virtual environment das Docker SDK für Python:

pip install docker

Um alle Container zu erhalten:

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

Für jeden Container stellt das Docker SDK ein Wörterbuch mit Labels zur Verfügung:

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

Mit diesen können wir die Container auswählen.

Starten mehrerer Docker -Protokollprozesse

Es gibt mehrere Möglichkeiten, mehrere Docker -Protokollprozesse zu starten. Da wir Python verwenden, tun wir dies mit Threads und subprocess. Sobald wir den Befehl, siehe oben, haben, starten wir den Prozess wie folgt:

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

Docker -Container können vorhanden, nicht vorhanden, oben, unten sein, was bedeutet, dass wir ständig (alle paar Sekunden) prüfen müssen, ob unser Docker logs-Befehl noch läuft, und wenn nicht, ihn erneut starten.

Der Code

Für den Fall, dass Sie dies ausprobieren möchten, hier ist der 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()

Testen

Starten Sie das Beispiel Docker-compose mit den drei Replikaten wie zuvor gezeigt:

docker-compose up -d

In einem anderen Konsolenfenster starten Sie dockertail.py:

python dockertail.py

Und in einem weiteren Konsolenfenster tail unsere Anwendungsfehlerlogdatei, die Aggregation der Fehlermeldungen aller Anwendungscontainer:

tail -f my_app.errors.log

Greifen wir nun im ersten Fenster auf einen Container zu, ohne einen Fehler zu erzeugen:

wget 127.0.0.1:1001

Wiederholen Sie dies mit einer anderen URL, um einen Fehler zu erzeugen:

wget 127.0.0.1:1001/nothing

Dies wird die folgende Fehlermeldung in my_app.errors.log tail ausgeben:

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"

Schön.

Einzeilige Ausnahmen

Fast perfekt. Wenn im obigen Beispiel eine Ausnahme in einem Skript Python auftritt, sehen wir nur die erste Zeile. Der Traceback ist nicht sichtbar. Wenn wir unser Skript dockertail.py nicht anfassen wollen, gibt es nur eine Möglichkeit, den Traceback in unserem Protokoll anzuzeigen, und zwar indem wir die Ausnahme als einzelne Zeile ausgeben lassen. Sie können im Internet Beispiele dafür finden, wie das geht.

Eine andere Möglichkeit wäre, jede Traceback-Zeile mit einem speziellen prefix zu beginnen und dann unseren obigen grep-Filter so zu ändern, dass er diese Zeilen immer durchlässt.

Zusammenfassung

Ich habe nach einer sehr einfachen Möglichkeit gesucht, die Logs vieler Docker -Container auf Fehler zu überwachen. In vielen Docker basierten Anwendungen werden die Container von Docker benannt. Docker für Python ist ein nettes Tool, um Docker Container anhand des Projektnamens und der Servicenamen zu entdecken und auszuwählen. Um die Docker -Containerprotokolle nach Fehlern zu filtern, verwenden wir den Linux grep-Befehl und fügen mit dem sed-Befehl den Containernamen zur Protokollmeldung hinzu.

Einmal gestartet, müssen wir uns nur noch die Ausgabe der aggregierten Protokolldatei ansehen:

tail -f my_app.errors.log

Automatisieren oder Beenden!

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.