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

Агрегирование и tail Docker журналов контейнеров с помощью Docker SDK для Python

Используйте grep, sed, threading и Docker SDK для Python для регистрации ошибок в одном файле.

11 декабря 2022
post main image

Проблема: У вас есть приложение Docker , состоящее из множества (микро) сервисов (контейнеров), и вы хотите отслеживать все эти контейнеры на предмет ошибок.

Сервисы в основном представляют собой скрипты Python , которые используют стандартный модуль логирования Python и печатают сообщения в stdout (и stderr):

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

При использовании стандартного драйвера логирования Docker json-file эти сообщения попадают в журналы Docker . Мы можем просмотреть журналы для контейнера с помощью команды :

docker logs <container-id>

docker logs <container-name>

В этом посте я представляю способ просмотра ошибок всех контейнеров в одном файле. Мы не используем 'container_name: <container-name>' в Docker-compose. Для выбора и доступа к контейнерам Docker мы используем Docker SDK для Python.

Шаги:

  • Фильтрация строк журнала с помощью grep и sed
  • Docker и именование контейнеров
  • Выбор и доступ к контейнерам с помощью Docker SDK для Python
  • Запуск нескольких процессов журнала Docker
  • Код
  • Тестирование

Фильтрация строк журнала с помощью grep и sed

Для просмотра журнала tail мы используем следующую команду, которая также показывает сообщения, отправленные в stderr:

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

В командах ниже мы добавляем аргумент 'do not buffer', чтобы убедиться, что мы не передаем части сообщений.

Мы не хотим видеть строки журнала с 'DEBUG', а показываем только строки с 'ERROR' и 'WARNING'. Поскольку у нас есть и другие службы, мы также ищем без учета регистра:

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

Поскольку у нас есть несколько журналов контейнеров, мы вставляем имя контейнера в начало строки журнала:

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

Наконец, мы добавляем эти строки в один файл журнала:

>> my_app.errors.log

Вот полная строка:

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

Затем в другом окне консоли мы tail этот файл:

tail - f my_app.errors.log

Docker и именование контейнеров

В Docker есть возможность присвоить имя контейнеру. Это хорошо работает, если такой контейнер только один. Но если вы хотите запустить несколько экземпляров (реплик) контейнера, вы не можете этого сделать и должны позволить Docker генерировать имена контейнеров.

Сгенерированное Docker имя контейнера выглядит следующим образом:

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

Несколько реплик будут пронумерованы '_2', '_3' и т.д. Первая часть имени контейнера называется именем проекта, в данном случае это имя каталога, в котором находится файл docker-compose.yml.

Вы также можете указать имя проекта с помощью переменной среды COMPOSE_PROJECT_NAME. Например, с файлами docker-compose.yml и .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

Затем, вызываем проект с помощью:

docker-compose up -d

И в результате получаем:

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

Выбор и доступ к контейнерам с помощью Docker SDK для Python

Мы указываем контейнеры, для которых хотим показать журналы, используя имена проектов и имена служб. Если мы опустим имена служб, то будут выбраны все службы проекта. Если мы хотим выбрать все контейнеры из проекта 'my_project':

projects_services = {
    'my_project': []
}

Если мы хотим выбрать только сервис 'web' из проекта 'my_project':

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

Чтобы выбрать контейнеры в Python, мы используем Docker SDK для Python. Мы используем имена проектов для поиска контейнеров. По умолчанию мы выбираем все сервисы проекта.

В вашем virtual environment установите Docker SDK для Python:

pip install docker

Чтобы получить все контейнеры:

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

Для каждого контейнера Docker SDK раскрывает словарь меток с ключами, включая:

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

С их помощью мы можем выбрать контейнеры.

Запуск нескольких процессов журнала Docker

Существует несколько способов запустить несколько процессов журнала Docker . Поскольку мы используем Python , мы делаем это с помощью потоков и subprocess. Получив команду, см. выше, мы запускаем процесс следующим образом:

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

Контейнеры Docker могут присутствовать, не присутствовать, подниматься, опускаться, то есть мы должны постоянно (каждые несколько секунд) проверять, запущена ли еще наша команда Docker logs, и если нет, запускать ее снова.

Код

Если вы хотите попробовать, вот код:

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

Тестирование

Запустите пример Docker-compose с тремя репликами, как было показано ранее:

docker-compose up -d

В другом консольном окне запустите dockertail.py:

python dockertail.py

И в другом консольном окне tail наш файл журнала ошибок приложения, совокупность сообщений об ошибках всех контейнеров приложения:

tail -f my_app.errors.log

Теперь в первом окне обратимся к контейнеру без генерации ошибки:

wget 127.0.0.1:1001

Повторите это с другим URL, чтобы сгенерировать ошибку:

wget 127.0.0.1:1001/nothing

Это выведет следующее сообщение об ошибке в my_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"

Отлично.

Исключения в одной строке

Почти идеально. В приведенном выше примере, когда исключение возникает в скрипте Python , мы видим только первую строку. Обратный след не виден. Если мы не хотим трогать наш скрипт dockertail.py, есть только один способ показать обратную трассировку в нашем журнале, и это - вывести исключение в виде одной строки. В Интернете можно найти примеры, как это сделать.

Другой способ заключается в том, чтобы начинать каждую строку обратного хода со специального prefix и затем модифицировать наш grep-фильтр выше, чтобы всегда пропускать эти строки.

Резюме

Я искал очень простой способ мониторинга журналов многих контейнеров Docker на предмет ошибок. Во многих приложениях на базе Docker контейнеры именуются Docker. Docker API для Python - это хороший инструмент для обнаружения и выбора контейнеров Docker , используя имя проекта и имена служб. Для фильтрации журналов контейнеров Docker на предмет ошибок, мы используем команду Linux grep и команду sed для добавления имени контейнера в сообщение журнала.

После запуска все, что нам нужно сделать, - это просмотреть вывод агрегированного файла журнала:

tail -f my_app.errors.log

Автоматизируйте или прекратите!

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.