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

Agregar y tail Docker registros de contenedores utilizando Docker SDK para Python

Utilice grep, sed, threading y Docker SDK para Python para registrar los errores en un único archivo.

11 diciembre 2022
post main image

El problema: Finalmente tienes una aplicación Docker que consiste en muchos (micro) servicios (contenedores) y quieres monitorizar todos estos contenedores en busca de errores.

Los servicios son en su mayoría scripts Python que utilizan el módulo de registro estándar Python e imprimen mensajes a stdout (y stderr):

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

Utilizando el controlador de registro Docker json-file por defecto, estos mensajes terminan en los registros Docker . Podemos ver los logs de un contenedor usando el comando:

docker logs <container-id>

docker logs <container-name>

En este post presento una forma de ver los errores de todos los contenedores en un único fichero. No estamos utilizando 'nombre_contenedor: <nombre_contenedor>' en Docker-compose. Para seleccionar y acceder a los contenedores Docker , utilizamos el SDK Docker para Python.

Pasos a seguir:

  • Filtrado de líneas de registro con grep y sed
  • Docker y nomenclatura de contenedores
  • Selección y acceso a contenedores con el SDK Docker para Python
  • Inicio de múltiples procesos de registro Docker
  • El código
  • Probando

Filtrado de líneas de registro con grep y sed

Para tail el log de Docker utilizamos el siguiente comando que también muestra los mensajes enviados a stderr:

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

En los comandos siguientes añadimos un argumento 'do not buffer' para asegurarnos de que no pasamos partes de los mensajes.

No queremos ver líneas de log con 'DEBUG', y mostrar sólo líneas con 'ERROR' y 'WARNING'. Como también tenemos otros servicios, también buscamos sin distinguir mayúsculas de minúsculas:

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

Como tenemos varios registros de contenedores, insertamos el nombre del contenedor al principio de una línea de registro:

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

Por último, añadimos estas líneas a un único archivo de registro:

>> my_app.errors.log

Aquí está la línea completa:

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

Luego, en otra ventana de consola tail este archivo:

tail - f my_app.errors.log

Docker y el nombre del contenedor

En Docker es posible asignar un nombre a un contenedor. Esto funciona bien si sólo hay un contenedor. Pero si desea ejecutar múltiples instancias (réplicas) del contenedor, no puede hacerlo y debe dejar que Docker genere los nombres de los contenedores.

Un nombre de contenedor generado por Docker tiene este aspecto:

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

Las réplicas múltiples se numerarán '_2', '_3', etc. La primera parte del nombre del contenedor se denomina nombre del proyecto, en este caso el nombre del directorio donde se encuentra el archivo docker-compose.yml.

También puede especificar el nombre del proyecto utilizando la variable de entorno COMPOSE_PROJECT_NAME. Por ejemplo, con los ficheros docker-compose.yml y .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

Entonces, trayendo el proyecto utilizando:

docker-compose up -d

Y el resultado es:

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

Selección y acceso a contenedores con el SDK Docker para Python

Especificamos los contenedores para los que queremos mostrar los logs utilizando los nombres de proyecto y de servicio. Si omitimos los nombres de servicio, se seleccionan todos los servicios de un proyecto. Si queremos seleccionar todos los contenedores del proyecto 'mi_proyecto':

projects_services = {
    'my_project': []
}

Si queremos seleccionar sólo el servicio 'web' del proyecto 'mi_proyecto':

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

Para seleccionar los contenedores en Python, utilizamos el SDK Docker para Python. Utilizamos los nombres de los proyectos para buscar los contenedores. Por defecto seleccionamos todos los servicios de un proyecto.

En su virtual environment, instale el SDK Docker para Python:

pip install docker

Para obtener todos los contenedores:

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

Para cada contenedor, el SDK Docker expone un diccionario de etiquetas con claves que incluyen:

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

Con ellas podemos seleccionar los contenedores.

Iniciar múltiples procesos de registro Docker

Hay varias formas de iniciar múltiples procesos de registro Docker . Como estamos utilizando Python lo hacemos con hilos y subprocess. Una vez que tenemos el comando, ver arriba, iniciamos el proceso de la siguiente manera:

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

Los contenedores Docker pueden estar presentes, no presentes, arriba, abajo, lo que significa que debemos comprobar continuamente (cada algunos segundos) si nuestro comando Docker logs sigue ejecutándose, y si no, iniciarlo de nuevo.

El código

En caso de que quieras probar esto, aquí está el código:

# 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()

Probando

Arranca el ejemplo Docker-compose con las tres réplicas como se ha mostrado antes:

docker-compose up -d

En otra ventana de la consola inicia dockertail.py:

python dockertail.py

Y en otra ventana de consola tail nuestro archivo de registro de errores de la aplicación, la agregación de los mensajes de error de todos los contenedores de la aplicación:

tail -f my_app.errors.log

Ahora, en la primera ventana, accedamos a un contenedor sin generar ningún error:

wget 127.0.0.1:1001

Repite esto con una URL diferente para generar un error:

wget 127.0.0.1:1001/nothing

Esto imprimirá el siguiente mensaje de error en mi_app.errores.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.

Excepciones de una sola línea

Casi perfecto. En el ejemplo anterior, cuando se produce una excepción en un script Python sólo vemos la primera línea. El traceback no es visible. Si no queremos tocar nuestro script dockertail.py, sólo hay una forma de mostrar el traceback en nuestro log y es hacer que la excepción se imprima como una sola línea. Puedes encontrar ejemplos en internet de cómo hacerlo.

Otra forma sería comenzar cada línea de rastreo con un prefix especial y luego modificar nuestro filtro grep para que siempre pase estas líneas.

Resumen

Estaba buscando una forma muy básica de monitorizar los logs de muchos contenedores Docker en busca de errores. En muchas aplicaciones basadas en Docker , los contenedores son nombrados por Docker. Docker API para Python es una buena herramienta para descubrir y seleccionar contenedores Docker utilizando el nombre del proyecto y los nombres de servicio. Para filtrar los registros del contenedor Docker en busca de errores, utilizamos el comando grep Linux y usamos el comando sed para añadir el nombre del contenedor al mensaje de registro.

Una vez iniciado, todo lo que tenemos que hacer es mirar la salida del archivo de registro agregado utilizando:

tail -f my_app.errors.log

¡Automatizar o terminar!

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.