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

Samenvoegen en tail Docker containerlogs met behulp van Docker SDK voor Python

Gebruik grep, sed, threading en Docker SDK voor Python om fouten in een enkel bestand te loggen.

11 december 2022
post main image

Het probleem: U heeft uiteindelijk een Docker applicatie die bestaat uit vele (micro) services (containers) en wilt al deze containers monitoren op fouten.

De services zijn meestal Python scripts die de standaard Python logging module gebruiken en berichten afdrukken naar stdout (en stderr):

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

Met behulp van de standaard Docker json-file logging driver, komen deze berichten terecht in de Docker logs. We kunnen de logs voor een container bekijken met het commando:

docker logs <container-id>

docker logs <container-name>

In deze post presenteer ik een manier om de fouten van alle containers in een enkel bestand te bekijken. We gebruiken geen 'container_name: <container-naam>' in Docker-compose. Om de Docker containers te selecteren en te openen, gebruiken we de Docker SDK voor Python.

Stappen:

  • Filteren van logregels met grep en sed
  • Docker en containernaamgeving
  • Containers selecteren en openen met de Docker SDK voor Python
  • Meerdere Docker logboekprocessen starten
  • De code
  • Het testen van

Filteren van logregels met grep en sed

Om tail het Docker logboek te laten zien, gebruiken we het volgende commando dat ook de berichten toont die naar stderr worden gestuurd:

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

In de onderstaande commando's voegen we een 'niet bufferen' argument toe om ervoor te zorgen dat we geen delen van berichten doorgeven.

We willen geen logregels zien met 'DEBUG', en tonen alleen regels met 'ERROR' en 'WARNING'. Omdat we ook andere diensten hebben, zoeken we ook hoofdletterongevoelig:

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

Omdat we meerdere containerlogs hebben, voegen we de containernaam in aan het begin van een logregel:

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

Tenslotte voegen we deze regels toe aan een enkel logbestand:

>> my_app.errors.log

Hier is de volledige regel:

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

Vervolgens voegen we in een ander consolevenster tail dit bestand toe:

tail - f my_app.errors.log

Docker en containernaamgeving.

In Docker is het mogelijk een naam toe te kennen aan een container. Dit werkt prima als er maar één zo'n container is. Maar als je meerdere instanties (replica's) van de container wilt draaien, kun je dit niet doen en moet je Docker de containernamen laten genereren.

Een door Docker gegenereerde containernaam ziet er als volgt uit:

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

Meerdere replica's worden genummerd als '_2', '_3', enz. Het eerste deel van de containernaam heet de projectnaam, in dit geval de naam van de directory waar het docker-compose.yml bestand staat.

U kunt de projectnaam ook opgeven met de omgevingsvariabele COMPOSE_PROJECT_NAME. Bijvoorbeeld met de bestanden docker-compose.yml en .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

Dan, het project ophalen met:

docker-compose up -d

En het resultaat is:

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

Containers selecteren en openen met de Docker SDK voor Python.

We specificeren de containers waarvoor we de logs willen tonen met behulp van de projectnamen en servicenamen. Als we de servicenamen weglaten, worden alle services van een project geselecteerd. Als we alle containers van het project 'mijn_project' willen selecteren:

projects_services = {
    'my_project': []
}

Als we alleen de dienst 'web' uit het project 'mijn_project' willen selecteren:

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

Om de containers in Python te selecteren, gebruiken we de SDK voor Docker . We gebruiken de projectnamen om de containers te zoeken. Standaard selecteren we alle services van een project.

In uw virtual environment installeert u de Docker SDK voor Python:

pip install docker

Om alle containers te krijgen:

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

Voor elke container stelt de Docker SDK een labels woordenboek bloot met sleutels waaronder:

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

Hiermee kunnen we de containers selecteren.

Meerdere Docker log processen starten

Er zijn verschillende manieren om meerdere Docker log processen te starten. Omdat we Python gebruiken, doen we dit met threads en subprocess. Zodra we het commando hebben, zie hierboven, starten we het proces als volgt:

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

Docker containers kunnen aanwezig zijn, niet aanwezig zijn, omhoog, omlaag, wat betekent dat we continu (elke paar seconden) moeten controleren of ons Docker logs commando nog loopt, en zo niet, het opnieuw starten.

De code

Mocht je dit willen proberen, hier is de 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

Breng het Docker-compose voorbeeld met de drie replica's zoals eerder getoond:

docker-compose up -d

Start in een ander consolevenster dockertail.py:

python dockertail.py

En in een ander consolevenster tail ons applicatiefoutenlogbestand, de samenvoeging van foutmeldingen van alle applicatiecontainers:

tail -f my_app.errors.log

Laten we nu in het eerste venster een container openen zonder een fout te genereren:

wget 127.0.0.1:1001

Herhaal dit met een andere URL om een fout te genereren:

wget 127.0.0.1:1001/nothing

Dit zal de volgende foutmelding afdrukken in mijn_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"

Nice.

Enkele regel uitzonderingen

Bijna perfect. In het bovenstaande, wanneer een uitzondering optreedt in een Python script zien we alleen de eerste regel. De traceback is niet zichtbaar. Als we ons dockertail.py script niet willen aanraken, is er maar één manier om de traceback in ons logboek te tonen en dat is om de uitzondering als een enkele regel af te drukken. Op het internet zijn voorbeelden te vinden over hoe dit moet.

Een andere manier zou zijn om elke traceback-regel te beginnen met een speciale prefix en dan ons bovenstaande grep-filter zo aan te passen dat deze regels altijd worden doorgelaten.

Samenvatting

Ik was op zoek naar een zeer eenvoudige manier om de logs van veel Docker containers te controleren op fouten. In veel op Docker gebaseerde applicaties worden de containers benoemd door Docker. Docker API voor Python is een mooi hulpmiddel om Docker containers te ontdekken en te selecteren aan de hand van de projectnaam en servicenamen. Om de Docker containerlogs te filteren op fouten, gebruiken we het commando Linux grep en gebruiken we het commando sed om de containernaam toe te voegen aan het logbericht.

Eenmaal gestart, hoeven we alleen maar te kijken naar de uitvoer van het geaggregeerde logbestand met behulp van:

tail -f my_app.errors.log

Automatiseren of beëindigen!

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.