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.
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
Neueste
- Don't Repeat Yourself (DRY) mit Jinja2
- SQLAlchemy, PostgreSQL, maximale Anzahl von Zeilen pro user
- Anzeige der Werte in den dynamischen Filtern SQLAlchemy
- Sichere Datenübertragung mit Public Key Verschlüsselung und pyNaCl
- rqlite: eine hochverfügbare und distverteilte SQLite -Alternative
- Sollte ich meinen Docker Swarm auf Kubernetes migrieren?
Meistgesehen
- Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden
- Verwendung von UUIDs anstelle von Integer Autoincrement Primary Keys mit SQLAlchemy und MariaDb
- Verbindung zu einem Dienst auf einem Docker -Host von einem Docker -Container aus
- SQLAlchemy: Verwendung von Cascade Deletes zum Löschen verwandter Objekte
- PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen
- Flask RESTful API Validierung von Anfrageparametern mit Marshmallow-Schemas