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

В моем случае сервисы запускаются в 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-запрос проверки здоровья.

Рис. 1 Отображение логов контейнера в интерфейсе Intellij IDEA

Теперь добавим 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 контейнера:

Рис. 2 Контейнеры

Если мы сейчас перейдем по адресу 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:

Рис. 3 Интерфейс источника данных 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-сервиса) и выполним запрос.

Рис. 4 Фильтр на основе меток
Рис. 5 Диаграмма и логи

Ответственное отношение к ресурсам

Теперь, когда мы настроили экспорт логов, имеет смысл подумать об экономии ресурсов. Нам далеко не всегда нужно хранить все логи за все время работы приложения. Чтобы сэкономить место на диске имеет смысл настроить время жизни логов в системе индексирования. Для этого создадим каталог 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 (да да, поддерживаем отечественное ?).

А что вы думаете по поводу логирования в контейнеризирвоанных средах?

Какие инструменты используете?

С какими интересными кейсами сталкивались?

Пожалуйста напишите в комментариях!