Агрегирование и tail Docker журналов контейнеров с помощью Docker SDK для Python
Используйте grep, sed, threading и Docker SDK для Python для регистрации ошибок в одном файле.
Проблема: У вас есть приложение 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
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу