При построении микросервисной архитектуры часто возникает потребность анализировать логи из нескольких источников (баз, сервисов и т. д.). В этой статье я бы хотел поделиться решением к которому в итоге пришел.
В моем случае сервисы запускаются в Docker-контейнерах, поэтому было решено использовать следующие инструменты:
Loki - система агрегации логов;
Loki Docker Driver Client - Docker плагин для доставки логов до Loki;
Grafana - интерфейс визуализации и создания запросов;
Loki
Loki - открытое программное обеспечение, разработанное Grafana Labs, которое предназначено для хранения, индексирования и обработки логов.
В отличие от других систем агрегации логов, Loki индексирует не сами логи, а их метаданные, а именно метки. При этом сами логи сжимаются и хранятся в различных объектных хранилищах или в файловой системе. (из офиц. документации).
В нашем примере мы будем хранить логи в файловой системе.
Loki Docker Driver Client
Данный плагин собирает логи контейнеров и доставляет их до Loki. Помимо доставки этот драйвер также позволяет настраивать конвейер для логов, в рамках которого можно добавлять новые метки для индексирования, однако это тема для отдельного поста.
И так, приступим к настройке
Чтобы реализовать данный пример вам потребуется следующее:
Docker
Docker-compose
Loki Docker Driver Client
Любимая IDE
Установка Docker и Docker-compose
При локальной работе вполне допустимо использовать официальный набор инструментов Docker Desktop, который содержит все необходимое, а также графический интерфейс, упрощающий взаимодействие с образами, контейнерами и файловыми объемами (docker volumes). При реализации данного примера на сервере лучше использовать вариант установки для соответствующего дистрибутива ОС, на которой работает ваш сервер.
Установка Docker Desktop.
Установка Docker Engine и Docker Compose (для случаев когда работаем с сервером).
Установка Loki Docker Driver Client
Для установки Loki Docker Driver Client воспользуемся командой:
docker plugin install grafana/loki-docker-driver:2.9.4 --alias loki --grant-all-permissions
Создадим docker-compose проект
Начнем с того, что создадим новый пустой проект в своей любимой IDE и добавим в корень файл с названием docker-compose.yml. Для нашего примера мы будем использовать контейнер nginx в качестве объекта логирования. Добавим compose-service для nginx:
version: "3.9"
services:
nginx:
image: nginx
hostname: nginx-entrypoint
container_name: nginx-entrypoint
restart: unless-stopped
environment:
TZ: "Europe/Moscow"
ports:
- 80:80
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost" ]
interval: 10s
timeout: 10s
retries: 20
Если мы сейчас запустим наш compose файл, и посмотрим в логи контейнера, то увидим логи запуска nginx, и потом раз в 20 секунд будет писаться лог о том, что пришел GET-запрос проверки здоровья.
Теперь добавим compose-сервисы для Grafana и Loki.
loki:
hostname: loki
image: grafana/loki:latest
environment:
TZ: ${SYSTEM_TIMEZONE:-Europe/Moscow}
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
grafana:
hostname: grafana
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
# Включим доступ без авторизации
- GF_AUTH_ANONYMOUS_ENABLED=true # Не используйте **ANONYMOUS** настройки в проде
# Дадим права администратора при анонимном входе
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- TZ=${SYSTEM_TIMEZONE:-Europe/Moscow}
image: grafana/grafana:latest
ports:
- "3000:3000"
Запустим обновленный compose-файл. Теперь в нашем распоряжении уже 3 контейнера:
Если мы сейчас перейдем по адресу http://localhost:3000 то попадем на приветственную страницу Grafana, однако в данный момент там нет ничего интересного, поскольку мы не настроили ни одного источника данных, да и логи в Loki мы пока тоже не пишем.
Для того чтобы Grafana знала куда ей смотреть ей нужно дать об этом знать. Для этого создадим новый каталог в нашем проекте и назовем его grafana/provisioning/datasources. В этот каталог мы добавим конфигурационный YAML файл под названием loki.yaml. Этот файл должен иметь примерно следующее содержание:
apiVersion: 1
datasources:
- name: Loki # Отображаемое имя нашего источника данных
type: loki # Тип источника
access: proxy #
orgId: 1 # Идентификатор организации (единица адм. деления в Grafana) которой будет доступен источник
url: http://loki:3100 # Адрес откуда получать данные (здесь мы используем имя сервиса loki, т. к. компоуз создаст свою сеть в которой к контейнерам можно обращаться по имени compose-сервиса)
basicAuth: false # Для удобства демонстрации в Loki отключена авторизация, поэтому и тут она не зачем
isDefault: true #
version: 1 #
editable: false # Зпретим редактирование через интерфейс Grafana
Теперь нам нужно сделать так, чтобы данная конфигурация была доступна контейнеру Grafana при запуске, для этого добавим пару строк в конфигурацию сервиса grafana:
grafana:
hostname: grafana
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- TZ=${SYSTEM_TIMEZONE:-Europe/Moscow}
# Добавим проброс каталога файловой системы в файловую систему контейнра
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
image: grafana/grafana:latest
ports:
- "3000:3000"
Теперь, после пересоздания контейнера grafana, в разделе Explore у вас должен появиться источник данных Loki:
Теперь нам нужно сказать докеру, что логи нужно писать не просто так, а с использованием Loki Docker Driver Clinet. Для этого добавим yml-anchor, в котором опишем необходимую конфигурацию (yml-anchor нужен для того, чтобы можно было переиспользовать эту конфигурацию логирования без необходимости копировать код).
x-def-logging: &default-logging
logging:
# Указываем, какой драйвер использовать
driver: "loki"
options:
# Адрес Loki, куда складывать логи
# Обратите внимание, что здесь используется не имя сервиса loki, а локальный хост, на который проброшен порт Loki,
# это сделано потому, что логи будет писать docker engine, котрый расположен на хостовой машине,
# и он не знает имени хоста контейнера Loki, которое ему присвоил compose во внутренней сети проекта.
loki-url: "http://localhost:3100/loki/api/v1/push"
loki-batch-size: "100"
loki-retries: 2
loki-max-backoff: 1000ms
loki-timeout: 1s
Теперь когда логирование настроено, мы можем добавить anchor в конфигурацию сервиса nginx.
nginx:
image: nginx
hostname: nginx-entrypoint
container_name: nginx-entrypoint
restart: unless-stopped
<<: *default-logging
environment:
TZ: "Europe/Moscow"
ports:
- 80:80
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost" ]
interval: 10s
timeout: 10s
retries: 20
Запустим наш проект. Loki Docker Driver Client начинает экспортировать логи docker-контейнеров в Loki, поэтому нам становятся доступны такие метки как:
compose_project;
compose_service;
container_name;
host;
source.
Выберем фильтр по метке compose_service и установим значение равное nginx (имя нашего compose-сервиса) и выполним запрос.
Ответственное отношение к ресурсам
Теперь, когда мы настроили экспорт логов, имеет смысл подумать об экономии ресурсов. Нам далеко не всегда нужно хранить все логи за все время работы приложения. Чтобы сэкономить место на диске имеет смысл настроить время жизни логов в системе индексирования. Для этого создадим каталог loki в нашем проекте и положим туда следующий файл:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
# Отличие от стандарного конфигурационного файла loki, который идет из коробки
# заключается в строках ниже. Здесь мы указываем для менеджера по умолчанию,
# что он может удалять старые логи, а также, что в нашем понимании "старые логи"
# (те которые старше 168 часов).
table_manager:
retention_deletes_enabled: true
retention_period: 168h
analytics:
reporting_enabled: false
Теперь требуется пробросить эту конфигурацию в контейнер Loki с помощью docker-volume, как мы ранее делали с контейнером Grafana:
loki:
hostname: loki
image: grafana/loki:2.9.0
environment:
TZ: ${SYSTEM_TIMEZONE:-Europe/Moscow}
# Мы пробросили конфигурацию в файловую систему контейнера
volumes:
- ./loki/retention-config.yaml:/etc/loki/retention-config.yaml
ports:
- "3100:3100"
# и модифицировали команду запуска, чтобы использовалась наша конфигурация
command: -config.file=/etc/loki/retention-config.yaml
Et voilà, теперь логи старше 1 недели будут сами очищаться.
Исходники демонстрационного проекта доступны в моем gitverse (да да, поддерживаем отечественное 😉).
А что вы думаете по поводу логирования в контейнеризирвоанных средах?
Какие инструменты используете?
С какими интересными кейсами сталкивались?
Пожалуйста напишите в комментариях!