О чём статья

Хочу показать как развернуть готовый и полностью работающий стек мониторинга Grafana + Loki + Prometheus + Pushgateway + Promtail за пару минут (в прямом смысле этого слова), без необходимости устанавливать на машину дополнительное ПО (плагины docker и плагины логирования) и собирать логи со всех контейнеров машины (или сразу со всех нод, в случае с swarm).

Как это выглядит (на примере логов nginx). Никаких дополнительных плагинов, exporter-ов при этом не использовано, логи и графики данного примера построены с официального docker image nginx, со стандартными настройками логирования.
Как это выглядит (на примере логов nginx). Никаких дополнительных плагинов, exporter-ов при этом не использовано, логи и графики данного примера построены с официального docker image nginx, со стандартными настройками логирования.

Сама статья получилось не маленькая, но я постарался построить её таким образом, что основные моменты изложены в блоке Quick start, а всё что после, описывает дальнейшую конфигурацию решения, и является опциональным.

Предыстория

Спрячу под кат.

Я долгое время безуспешно искал во всемирной паутине (не только в рунете) решение по мониторингу и логированию docker swarm, docker compose, которое можно было бы развернуть быстро, в контейнерах (т.е. без необходимости устанавливать на сами «машины» дополнительное ПО, драйверы-логгеры для docker, ...), но которое было бы всё же достаточно полным и использовало бы популярные инструменты (будь то elk или grafana).

Да, я прекрасно понимаю, что большая часть, кто занимается такими задачами, уже гуру k8s, гуру elk и/или grafana, и да, моё решение не применимо в таком виде для окружений где уже всё очень очень серьёзно, где нужен high availability мониторинг, с репликацией, с сотней нод.

Но, я вполне уверен, что много кейсов, где решение будет полезно, хотя бы мониторинг каких-то не очень больших, или своих проектов, или кто то хочет просто понять как это устроено и «пощупать».

 Из того, что я находил, все решения имели один или несколько недостатков, среди которых:

  • Надо устанавливать различные пакеты на сами «машины» (ноды, VM). Это время, это нельзя быстро попробовать, по хорошему нельзя сразу «накатить» на свой кластер (ведь если не понравится — потом удалять?, да и зачем что то лишнее ставить?)

  • Надо вносить изменения в конфигурации логирования уже созданных swarm и compose стеков

  • Описано слишком сложно, и в целом, проще разобраться самому по офф. документации, чем задеплоить пример, в то же время сам пример не готов хотя бы к тестовой работе «из коробки»

  • Обратное предыдущему — всё сделано за вас, без описания действия автора, «на вкус» автора, без вариантов конфигурации под себя, и, естественно, без возможности использовать по прямому назначению.

  • Описаны очень старые версии

Моё решение

  • Устанавливать на сами «машины» (ноды, VM) ничего не надо, сбор логов со всех контейнеров (будь то все контейнеры «машины», если у нас одна нода, будь то все контейнеры кластера, если у нас swarm) работает сразу же, по команде docker compose up уже получаем работающее решение, которое можно использовать.

  • Исходя из вышеописанного, можно «затестить» хоть на работающем окружении, хоть где удобно: никакие изменения в ПО нод вносить не надо.

  • Постарался понятно описать: в readme.md есть готовый вариант запуска «одной командой», при этом, все основные настраиваемые параметры вынесены либо в docker-compose.yaml либо в конфигурации отдельных сервисов — prometheus.yaml, loki.yaml, promtail.yaml. При этом, каждая настройка из примера описана, со ссылкой на офф. документацию. В итоге: всё прозрачно, офф. образы (docker image), всё что изменено — явно описано.

  • Есть необходимый минимум для использования по прямому назначению, «из коробки» настроена авторизация в prometheus, pushgateway, показана возможность использования docker secrets (обычно никто не «заморачивается» с этими тонкостями в примерах, в то же время, моё решение никак не усложняет пример)

  • Версии сервисов — последние стабильные версии на сегодняшний день

Если одним предложением:

Моё решение предлагает вариант, который можно запустить и попробовать одной командой «из коробки», с работающими и настроенными grafana, prometheus, pushgateway, loki, promtail, который можно использовать как MVP даже без каких-либо изменений (а можно и с изменениями — весь конфиг детально описан).

Quick start

Репозиторий на GitHub: github.com/skl256/grafana_stack_for_docker

Если не терпится запустить стек Grafana + Loki + Prometheus + Pushgateway + Promtail и пощупать как это работает - можно уже это сделать, а после читать дальше.

Этот код скачивает содержимое репозитория skl256/grafana_stack_for_docker, создаёт папки /mnt/common_volume/swarm/grafana, /mnt/common_volume/grafana, помещает туда конфигурации и запускает контейнеры. Никакие изменения в настройки хоста, докера и не вносятся, всё что запускается - запускается в контейнерах без аффекта на окружение.

git clone https://github.com/skl256/grafana_stack_for_docker.git && \
cd grafana_stack_for_docker && \
sudo mkdir -p /mnt/common_volume/swarm/grafana/config && \
sudo mkdir -p /mnt/common_volume/grafana/{grafana-config,grafana-data,prometheus-data,loki-data,promtail-data} && \
sudo chown -R $(id -u):$(id -g) {/mnt/common_volume/swarm/grafana/config,/mnt/common_volume/grafana} && \
touch /mnt/common_volume/grafana/grafana-config/grafana.ini && \
cp config/* /mnt/common_volume/swarm/grafana/config/ && \
mv grafana.yaml docker-compose.yaml && \
docker compose up -d
для docker swarm
git clone https://github.com/skl256/grafana_stack_for_docker.git && \
cd grafana_stack_for_docker && \
sudo mkdir -p /mnt/common_volume/swarm/grafana/config && \
sudo mkdir -p /mnt/common_volume/grafana/{grafana-config,grafana-data,prometheus-data,loki-data,promtail-data} && \
sudo chown -R $(id -u):$(id -g) {/mnt/common_volume/swarm/grafana/config,/mnt/common_volume/grafana} && \
touch /mnt/common_volume/grafana/grafana-config/grafana.ini && \
cp config/* /mnt/common_volume/swarm/grafana/config/ && \
docker stack deploy -c grafana.yaml grafana

Docker swarm должен быть уже инициализирован, в противном случае инициализируйте: docker swarm init

Важно, прочитайте, если в вашем swarm более одной ноды

Важно: в данном примере подразумевается, что /mnt/common_volume это общий том, который доступен со всех нод в кластере swarm (NFS, GlusterFS, ...). Если в вашем swarm пока нет тома, который общий для всех нод, вы можете привязать сервисы (grafana, prometheus, pushgateway, loki) к запуску только на одной ноде (см. docs.docker.com/compose/compose-file/deploy/#placement), где будут храниться их конфигурации и логи, а для сервиса promtail (он запускается используя deploy.mode global, т.е. на каждой ноде) создать отдельный одинаковый promtail.yaml на каждой ноде, и отдельную папку для хранения данных - в примере это /mnt/common_volume/grafana/promtail-data (это корректно, что данные в этой папке будут в таком случае на каждой ноде разные, promtail там будет хранить позиции чтения логов docker, на каждой ноде они свои).

Важно: в коде, что выше, а также в docker-compose.yaml (grafana.yaml для swarm) подразумевается, что конфигурационные файлы данного стека храним в /mnt/common_volume/swarm/grafana, а файлы данных (логов, ...) храним в /mnt/common_volume/grafana. Возможно, не очень правильно с моей стороны было указывать в примере такие абсолютные пути, зато вы можете легко заменить их на свои, используя авто замену (ну или использовать мои, код сам создаст папки).

Готово! Переходите пожалуйста в браузере по адресу http://ip.адрес.машины:3000, откроется Grafana (стандартные логин admin, пароль admin).

Всё, уже с этого самого момента, со всех контейнеров собираются логи и записываются в Loki.

Как посмотреть логи в Grafana

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

  1. Если ещё не открыли Grafana: переходим в браузере по адресу http://ip.адрес.машины:3000, (ip.адрес.машины - той, где запускали скрипт), (стандартные логин admin, пароль admin).

  2. В левом верхнем углу, левее надписи Home, три горизонтальные полоски, меню. В меню выбираем Connections - Add new connection, находим и выбираем Loki, по нажатию правее выбираем Add new data source.

  3. В графе Connection - url заполняем http://loki:3100 (именно так, ведь внутри сети docker обращаемся с сервису по его имени, никакие IP-адреса подставлять не надо), пролистываем до низа страницы, и Save & Test.

  4. Возвращаемся в меню (в котором в п.2 выбирали Connections), только теперь выбираем Explore. Заполняем поисковый запрос, используя синтаксис loki (grafana.com/docs/loki/latest/query/log_queries/) - можно выбрать (переключатель справка Builder и Code) будем писать запрос с помощью builder или кодом, для начала можно просто выбрать job = containers_logs и нажать Run query (см. пример на иллюстрации ниже).

  5. Всё, внизу будут видны логи всех контейнеров (скорее всего, сейчас большинство логов будут самих контейнеров нашего стека grafana+..., ничего страшного, чуть далее в статье будет описано, как настроить логирование docker, чтобы в логах можно было бы искать и фильтровать запросами по имени стека, имени сервиса, имени compose project и т.д.)

    Пример запроса, который выведет логи всех контейнеров
    Пример запроса, который выведет логи всех контейнеров
    Пример логов
    Пример логов

Единственное изменение, которое всё-таки рекомендуется сделать в настройках docker на каждой ноде - это включить вывод labels в логи контейнеров. Это изменение даст нам возможность фильтровать и искать логи по имени compose проекта, compose сервиса, по имени swarm стека или swarm сервиса, и ещё по некоторым labels (их список и имена можно настроить под себя, или использовать те, которые настроены в примере: node_hostname, node_id, stack_name, service_name, service_id, task_name, task_id, compose_project, compose_service, container_id, stream) .

Для настройки необходимо открыть файл конфигурации docker daemon /etc/docker/daemon.json (он может отсутствовать, это нормальная ситуация, просто создаём его в таком случае) и добавить в него следующие настройки:

{
"log-driver": "json-file",
"log-opts": {
"labels-regex": "^.+"
}
}

После сохранения изменений в daemon.json необходимо перезапустить docker daemon (на большинстве ОС, основанных на Debian, Ubuntu это делается командой sudo systemctl restart docker). Из наблюдений: возможно потребуется пересоздать контейнеры compose (docker compose down && docker compose up -d для каждого compose проекта), чтобы новые настройки для них вступили в силу, для контейнеров swarm, как показала практика, это не потребуется, они пересоздадутся сами.

Поиск логов по конкретному сервису
Поиск логов по конкретному сервису
Так выглядят labels по которым можно строить поисковые запросы в loki, после включения в daemon.json
Так выглядят labels по которым можно строить поисковые запросы в loki, после включения в daemon.json
так выглядит тоже самое для compose проектов
На примере - compose project простого сайта из nginx (web), php-fpm (app), postgres (db), в скобках обозначены названия services в docker-compose.yaml, по ним и проставляется автоматически label в логах compose_service
На примере - compose project простого сайта из nginx (web), php-fpm (app), postgres (db), в скобках обозначены названия services в docker-compose.yaml, по ним и проставляется автоматически label в логах compose_service

В данном стеке у нас есть prometheus и pushgateway, в статье не стал описывать, как пользоваться данными инструментами, т.к. это уже выходит за рамки статьи и быстрого старта. Напишу лишь то, что prometheus и pushgateway тоже готовы к работе, зайти в их веб-интерфейс можно в браузере по адресу http://ip.адрес.машины:9090 и http://ip.адрес.машины:9091 соответственно (в примере настроены логин admin, пароль admin). В самих prometheus и pushgateway конечно не будет логов (вернее - метрик), т.к. для этого вам необходимо установить то, с чего эти метрики будете собирать (например, Node Exporter) и настроить сбор метрик (как настроить, поверхностно коснёмся в конце статьи).

Конфигурация

compose file (grafana.yaml / docker-compose.yaml)
version: "3"

services:

  grafana:
    image: "grafana/grafana-oss:10.2.2"
    ports:
      - 3000:3000
    volumes:
      # touch /mnt/common_volume/grafana/grafana-config/grafana.ini;
      # mkdir -p /mnt/common_volume/grafana/{grafana-config,grafana-data};
      - /mnt/common_volume/grafana/grafana-config:/etc/grafana
      - /mnt/common_volume/grafana/grafana-data:/var/lib/grafana
    user: "root"
    environment:
      # Uncomment GF_SERVER_DOMAIN, GF_SERVER_ROOT_URL for using reverse proxy
      #GF_SERVER_DOMAIN: "grafana.domain.example" # change "grafana.domain.example" to your domain
      #GF_SERVER_ROOT_URL: "https://grafana.domain.example/" # change "grafana.domain.example" to your domain
      GF_RENDERING_SERVER_URL: "http://grafana-image-renderer:8081/render"
      GF_RENDERING_CALLBACK_URL: "http://grafana:3000/"
      GF_UNIFIED_ALERTING_SCREENSHOTS_CAPTURE: "true"
      GF_LOG_FILTERS: "rendering:debug"
      # Uncomment for using PostgreSQL (recommended) instead SQLite
      #GF_DATABASE_TYPE: "postgres"
      #GF_DATABASE_HOST: "postgres:5432" # change "postgres:5432" to your PostgreSQL database host and port
      #GF_DATABASE_NAME: "postgres" # change "postgres" to your database name
      #GF_DATABASE_USER: "postgres" # change "postgres" to your database user name
      #GF_DATABASE_PASSWORD: "postgres" change "postgres" to your database password # or use GF_DATABASE_PASSWORD
      #GF_DATABASE_PASSWORD__FILE: "/run/secrets/grafana_database_password" # for docker secrets # or use GF_DATABASE_PASSWORD
      #GF_DATABASE_SSL_MODE: "disable"
    # Uncomment for using docker secrets 
    #secrets:
    #  - grafana_database_password
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        order: stop-first
      resources:
        limits:
          memory: 1024M
          
  grafana-image-renderer:
    image: "grafana/grafana-image-renderer:3.9.0"
    environment:
      TZ: "Europe/Moscow" # change to your timezone
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        order: start-first
      resources:
        limits:
          memory: 1024M
          
  prometheus:
    image: "prom/prometheus:v2.48.0"
    ports:
      - 9090:9090
    volumes:
      # touch /mnt/common_volume/swarm/grafana/config/{prometheus.yaml,web-config.yaml};
      # mkdir -p /mnt/common_volume/grafana/prometheus-data
      - /mnt/common_volume/swarm/grafana/config/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
      - /mnt/common_volume/swarm/grafana/config/web-config.yaml:/etc/prometheus/web-config.yaml:ro
      - /mnt/common_volume/grafana/prometheus-data:/prometheus
    user: "root"
    # Uncomment for using docker secrets
    #secrets:
    #  - node_exporter_password
    #  - grafana_prometheus_password
    command:
      - "--config.file=/etc/prometheus/prometheus.yaml"
      - "--web.config.file=/etc/prometheus/web-config.yaml"
      - "--storage.tsdb.retention.time=7d"
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        order: stop-first
      resources:
        limits:
          memory: 1024M
          
  pushgateway:
    image: "prom/pushgateway:v1.6.2"
    ports:
      - 9091:9091
    volumes:
      - /mnt/common_volume/swarm/grafana/config/web-config.yaml:/etc/prometheus/web-config.yaml:ro
    command:
      - "--web.config.file=/etc/prometheus/web-config.yaml"
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        order: start-first
      resources:
        limits:
          memory: 512M
          
  loki:
    image: "grafana/loki:2.9.0"
    #  - "3100:3100" port closed because loki does not support basic auth
    volumes:
      # touch /mnt/common_volume/swarm/grafana/config/loki.yaml;
      # mkdir -p /mnt/common_volume/grafana/loki-data;
      - /mnt/common_volume/swarm/grafana/config/loki.yaml:/etc/loki/loki.yaml:ro
      - /mnt/common_volume/grafana/loki-data:/loki
    command: 
      - "--config.file=/etc/loki/loki.yaml"
    user: "root"
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        order: stop-first
      resources:
        limits:
          memory: 1024M
          
  promtail:
    image: "grafana/promtail:2.9.0"
    volumes:
      # touch /mnt/common_volume/swarm/grafana/config/promtail.yaml;
      # mkdir -p /mnt/common_volume/grafana/promtail-data;
      - /mnt/common_volume/swarm/grafana/config/promtail.yaml:/etc/promtail/promtail.yaml:ro
      - /mnt/common_volume/grafana/promtail-data:/var/promtail
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/log:/var/log:ro
    environment:
      HOST_HOSTNAME: "{{.Node.Hostname}}"
    command:
      - "--config.file=/etc/promtail/promtail.yaml"
      - "--config.expand-env=true"
    deploy:
      mode: global
      update_config:
        order: stop-first
      resources:
        limits:
          memory: 512M

# Uncomment for using docker secrets
# create docker secret first: docker secret create [OPTIONS] SECRET [file|-]
# https://docs.docker.com/engine/reference/commandline/secret_create/
# if you create secret from stdin use Ctrl+D after input, do not use Enter
#secrets:
#  grafana_database_password:
#    external: true
#  node_exporter_password:
#    external: true
#  grafana_prometheus_password:
#    external: true

Опишу, как настраивал каждый сервис в стеке.
Полный код стека и файлов конфигурации пожалуйста смотрите в репозитории (ссылка по тексту выше). В самом же тексте, я не буду заниматься копипастом кода, а опишу только особенности конфигурирования, думаю так будет проще читателю.

Grafana

Образ: grafana/grafana-oss:10.2.2
Документация: grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/

Вместо редактирования файла конфигурации grafana.ini, конфигурирование можно выполнять с помощью переменных окружения (в документации по ссылке описано, как сопоставляются их имена с секциями и параметрами .ini файла).

  • В примере используется БД по умолчанию - SQLite. Рекомендуется использовать MySQL или PostgreSQL (для этого необходимо просто раскомментировать строки с переменными GF_DATABASE_* в .yaml стека, и заполнить их значениями).

  • В примере настроен Grafana image renderer (о нём - ниже).

  • Grafana можно использовать за reverse proxy при необходимости, для настройки самого reverse proxy воспользуйтесь документацией для конкретного ПО (примеры: grafana.com/tutorials/run-grafana-behind-a-proxy/) и раскомментируйте, заполните строки с переменными GF_SERVER_DOMAIN, GF_SERVER_ROOT_URL в .yaml стека).

  • Здесь и далее, в сервисах, где используются bind volumes, внутри контейнеров используется пользователь root, это необходимо для того, чтобы избежать необходимости chown, chmod для перемонтированных папок (если по каким-либо причинами, для вас это не приемлемо, измените).

Grafana image renderer

Образ: grafana/grafana-image-renderer:3.9.0
Документация: grafana.com/grafana/plugins/grafana-image-renderer/

Используется для отправки скриншотов с dashboards вместе с алертами (поддерживаются не все мессенджеры).

  • Необходимо устанавливать переменную TZ, упоминания об этом в документации не нашёл, однако без этого image-renderer отказывался работать (в логах noje.js ругаясь на TZ). В примере уже установлено (необходимо поменять на свой часовой пояс).

  • В переменных основного сервиса (grafana) необходимо настроить использование image-renderer - установить server url, callback url (url менять не нужно, если не меняем названия сервисов в .yaml файле стека), включить снятие скриншотов, и (опционально) логирование рендеринга. В примере уже настроено.
    GF_RENDERING_SERVER_URL: "http://grafana-image-renderer:8081/render" GF_RENDERING_CALLBACK_URL: "http://grafana:3000/" GF_UNIFIED_ALERTING_SCREENSHOTS_CAPTURE: true GF_LOG_FILTERS: "rendering:debug"

  • Если данный функционал не нужен - можно удалить сервис grafana-image-renderer из .yaml файле стека (при этом, не забыть удалить вышеописанные переменные из переменных основного сервиса - grafana).

Loki

Образ: grafana/loki:2.9.0
Документация: grafana.com/docs/loki/v2.9.x/configure/

Используется для работы с логами (которое отправляются в данном примере в loki из promtail).

  • Loki (в отличие от prometheus и pushgateway) не поддерживает на данный момент настройку авторизации, поэтому я не стал открывать порт (3100), он доступен только внутри docker. Loki будет доступен в сети docker, поэтому в данном примере и нет необходимости в дополнительных действиях. Если вы захотите открыть доступ к loki из вне сети docker можно воспользоваться альтернативными методами (ограничением доступа на уровне сети, или, наиболее простой способ, добавить в стек nginx в качестве reverse proxy к loki с basic auth: docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/).

  • При запуске в сервис loki передаём пусть к примонтированному в контейнер файлу конфигурации loki.yaml. За основу был взять дефолтный файл конфигурации  /etc/loki/local-config.yaml , в который внесены следующие изменения:

    • добавлен блок analytics с отключением отправки отчётности

    • добавлены блоки limits_config, chunk_store_config, compactor, которые необходимы для автоматического удаления старых логов (по умолчанию - никогда, в примере - 7 дней: 7d необходимо поменять в трёх местах на своё значение, при необходимости). Описание данной настройки: grafana.com/docs/loki/latest/operations/storage/retention/

  • Остальные настройки оставлены по умолчанию, на самом деле, loki скорее всего самый сложный и объёмный в плане костюмных настроек сервис, из сервисов представленных в данном стеке, поэтому не правильно пытаться описать все настройки тут. Для быстрого старта этих настроек достаточно, далее можно поиграться например со свойствами max_outstanding_requests_per_tenant, max_concurrent если не будут работать большие дашборды (с большим кол-вом одновременных запросов в логи).

если логи видны только за последние пару часов

Этот баг предположительно проявляется, при использовании очень медленных дисков. Логи пишутся, но видны с провалами. Что-то изменилось, начиная с 2.9.0. Пока решения не нашёл (есть несколько открытых issues на официальном GitHub Loki), но на 2.8.4 не проявляется (для отката, просто заменить образ grafana/loki:2.9.0 на grafana/loki:2.8.4и передеплоить loki, для compose - docker compose up -d). Скорее всего вы не увидите данной проблемы, проявилось на конфигурации где GlusterFS на медленной сети (ниже 100 Mbit/s) и 5400rpm HDD-ы, одним словом специфичное окружение. Но раз уж "поймал" проблему, способ который помог написал.

Promtail

Образ: grafana/promtail:2.9.0
Документация: grafana.com/docs/loki/latest/send-data/promtail/configuration/

Используется непосредственно для чтения файлов с логами docker и отправки их в Loki.

  • Запускается с deploy.mode: global, т.е. на каждой ноде. К сервису дополнительно монтируются (в read-only) пути с хоста, где хранятся логи docker: /var/lib/docker/containers и логи системы /var/log (в примере они монитрованы, но сам promtail не стал настраивать на чтение последних).

  • Promtail должен хранить позиции считывания логов, в примере он это хранит в bind volume, монтированному к /var/promtail, но в целом, можно использовать и обычный volume, эти данные не нужно перемещать между нодами.

  • При запуске передаём путь к файлу конфигурации, и команду --config.expand-env=true, которая разрешает сервису подставлять enviroment variables в файл конфигурации (например, чтобы мы могли подставлять в логи hostname ноды, как в примере).

Самое сложное, интересное и уникальное здесь, это сам файл конфигурации promtail.yaml.
Именно благодаря данной конфигурации реализована главная фича решения - сбор логов непосредственно из "родных" файлов логов docker.

promtail.yaml
server:
  http_listen_address: 0.0.0.0
  http_listen_port: 9080

positions:
  filename: "/var/promtail/positions_${HOST_HOSTNAME}.yaml" # remove "_${HOST_HOSTNAME}" if you do not use docker swarm

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:

- job_name: containers
  static_configs:
  - targets:
      - localhost
    labels:
      job: containers_logs
      node_hostname: "${HOST_HOSTNAME}" # remove line if you do not use docker swarm
      __path__: /var/lib/docker/containers/*/*log

  pipeline_stages:
  - json:
      expressions:
        log: log
        stream: stream
        time: time
        tag: attrs.tag
        # docker compose
        compose_project: attrs."com.docker.compose.project"
        compose_service: attrs."com.docker.compose.service"
        # docker swarm
        stack_name: attrs."com.docker.stack.namespace"
        service_name: attrs."com.docker.swarm.service.name"
        service_id: attrs."com.docker.swarm.service.id"
        task_name: attrs."com.docker.swarm.task.name"
        task_id: attrs."com.docker.swarm.task.id"
        node_id: attrs."com.docker.swarm.node.id"
  - regex:
      expression: "^/var/lib/docker/containers/(?P<container_id>.{12}).+/.+-json.log$"
      source: filename
  - timestamp:
      format: RFC3339Nano
      source: time
  - labels:
      stream:
      container_id:
      tag:
      # docker compose
      compose_project:
      compose_service:
      # docker swarm
      stack_name:
      service_name:
      service_id:
      task_name:
      task_id:
      node_id:
  - output:
      source: log

За основу взят стандартный файл конфигурации, так-же очень много подсмотрел в части настройки секции pipeline_stages (самой важной) в комментарии пользователя Pl8tinium в одном из GitHub gists.

Что сконфигурировано:

  • Секция positions - позиции считывания логов, о них было выше, добавлю что подставляем туда hostname (т.к. у меня volume, где храниться этот файл, общий для всех нод, в ином случае, в этом нет необходимости, но и мешать не будет).

  • Секция clients - url сервиса loki, куда будут отправляться логи (в примере менять ничего не нужно, понадобится изменить, если захотите использовать потом "другой" loki, не внутри данной сети docker, там же можно на этот случай указать данные аутентификации, если ваш будущий loki её требует, в примере не требует, т.к. находится внутри изолированной сети docker).

  • Ниже описываются сами настройки сбора логов. Для compose (без swarm) можно удалить строку node_hostname: "${HOST_HOSTNAME}", т.к. во-первых она и не нужна (зачем писать в лог hostname, если он у нас один), во-вторых compose не умеет вот так вот подставлять имя хоста в контейнер, т.е. в логах так и будет lablel "${HOST_HOSTNAME}". Если какие-то из labels использовать не планируется, то их можно удалить (из секций expressions и labels).

Prometheus

Образ: prom/prometheus:v2.48.0
Документация: prometheus.io/docs/prometheus/latest/configuration/configuration/

Используется для сбора метрик с так называемых exproters (экспортеров). Например, сбор метрик с linux (и не только) машин:

Так выглядят метрики с node exporter
Так выглядят метрики с node exporter

Есть много готовых exporter-ов для популярного ПО: nginx, postgres, nextcloud, многое ПО умеет выдавать метрики для prometheus "из коробки" (тот-же docker). В данном примере мы не настраиваем exporter-ы, т.е. это вам предстоит сделать самостоятельно. В конфигурационном файле prometheus.yaml я показал пример настройки сбора метрик с различных exporter-ов, а также, закомментил ссылки на документации от них. В плане данных настроек всё выглядит несложным, более подробно описывать в настоящей статье не буду, а остановлюсь на базовой конфигурации самого prometheus.

  • Порт 9090, открываем его наружу из сети docker, для того, чтобы мы могли получить доступ к веб-интерфейсу prometheus.

  • Время и максимальный объём хранимых логов можно настроить с помощь команд запуска --storage.tsdb.retention.time=15d, --storage.tsdb.retention.size=0 (напр. 512MB), подробнее: prometheus.io/docs/prometheus/latest/storage/

  • При запуске в сервис prometheus передаём пусть к примонтированному в контейнер файлу конфигурации prometheus.yaml, и файл web-config.yaml (последний, используется для настойки аутентификации, чтобы веб-интерфейс был бы закрыт basic auth).

Отдельно про basic auth:

Документация: github.com/prometheus/prometheus/blob/main/documentation/examples/web-config.yaml

web-config.yaml
basic_auth_users:
  admin: $2y$10$K7gXeAs0VbhjHMdlV1Hn0OlWcqIoK7P9s/dVKB3HoyYcLuscxSpXe # change "$2y$10..." to basic auth password_hash
  bobi: $2y$10$1sYkKxi49lGpFdlu7aDkTeWkzvkqaeCTb4PDBR/pNxeETO8N3shZS # you can add more users

  • В данном примере один web-config.yaml будет использоваться и для prometheus и для pushgateway

  • password_hash (который виден справа от имени пользователя) генерируется командой htpasswd -nBC 10 "" | tr -d ':\n'

    • Если htpasswd не установлен: для Debian, Ubuntu sudo apt install apache2-utils

    • для ОС использующих yum sudo yum install -y httpd-tools

Что сконфигурировано в файле конфигурации

prometheus.yaml
global:
  scrape_interval: 15s

scrape_configs:

# node exporter (https://github.com/prometheus/node_exporter)
- job_name: 'node'
  static_configs:
  - targets: ['192.168.100.2:9100', '192.168.100.4:9100', '192.168.100.6:9100'] # change 192.168.100.x to your nodes IPs
  basic_auth:
    username: 'admin' # change
    password: 'admin' # change # or use password_file instead (docker secrets)
    # password_file: '/run/secrets/node_exporter_password'

# docker daemon (https://docs.docker.com/config/daemon/prometheus/)
- job_name: 'docker'
  static_configs:
  - targets: ['192.168.100.2:9323', '192.168.100.4:9323', '192.168.100.6:9323'] # change 192.168.100.x to your nodes IPs

# pushgateway
- job_name: 'pushgateway'
  honor_labels: true
  static_configs:
  - targets: ['pushgateway:9091']
  basic_auth:
    username: 'admin' # change
    password: 'admin' # change # or use password_file instead (docker secrets)
    # password_file: '/run/secrets/node_exporter_password'

# nginx exporter (https://github.com/nginxinc/nginx-prometheus-exporter)
- job_name: 'nginx'
  static_configs:
  - targets: ['192.168.2.104:8080', '192.168.2.104:8080'] # change 192.168.100.x to your nodes IPs

# postgres exporter (https://github.com/prometheus-community/postgres_exporter)
- job_name: 'postgres'
  static_configs:
  - targets: ['192.168.100.4:9187'] # change 192.168.100.x to your nodes IPs

  • scrape_interval - как часто будем собирать метрики, 15s - по умолчанию.

  • scrape_configs - сами targets (цели) с которых prometheus будет собирать метрики. В примере показано, как настроить для различных exporter-ов, как использовать авторизацию, в т.ч. безопасно передавать пароли используя docker secrets. Используя ссылки можно установить требуемые exporter-ы согласно документации, заменить IP-адреса на адреса своих целей.

  • Конфигурация из примера запустится, просто ожидаемо не будет собирать метрики с того, что у вас не установлено, за исключением pushgateway, он у нас есть и для него конфигурация в примере вполне "боевая".

Pushgateway

Образ: prom/pushgateway:v1.6.2
Документация: github.com/prometheus/pushgateway

Используется совместно с prometheus для сбора метрик с сервисов и приложений, которые по определённым причинам не удобно использовать с концепцией prometheus (когда последний сам собирает с них метрики), в случает с pushgateway, мы можем сами отправлять в него метрики (best practices и примеров использования в приведено очень много в интернете, для многих языков программирования есть библиотеки, которые помогают работать с pushgateway).

  • Порт 9091, открываем его наружу из сети docker, для того, чтобы мы могли получить доступ к веб-интерфейсу pushgateway (и для того, чтобы мы могли пушить в него метрики).

  • При запуске в сервис pushgateway передаём пусть к примонтированному в контейнер файлу web-config.yaml (в примере используем тот же самый, что и для promtheus, в описании конфигурации promtheus описано назначение данного файла).

В целом, больше в контексте настоящей стати про pushgateway описать нечего, отдельно ещё раз обращу внимание, в отличие от настроек по умолчанию, наш pushgateway закрыт basic auth, т.е. если будете его использовать, пушить в него метрики, не забудьте использовать авторизацию (а если она вдруг не нужна, просто удалите из command "--web.config.file=/etc/prometheus/web-config.yaml").

Заключение

Благодарю за прочтение статьи. Буду неоригинален: попрошу не судить строго, т.к. это первая моя статья на Хабр. В то же время, очень надеюсь, что она действительно будет полезна, и буду рад комментариям, мнениям.

Комментарии (12)


  1. aleks_raiden
    02.01.2024 14:13
    +2

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


    1. ALexhha
      02.01.2024 14:13

      Что такое много/долго/существенным ? А то у всех свои понятия этих величин


      1. aleks_raiden
        02.01.2024 14:13
        +1

        Согласен. Много это десятки тысяч метрик / лейблов, такого порядка. Ну и нагрузка по чтению/записи когда речь идет 100+ rps. Долго - ну у меня ретеншин 10 лет выставлен, можно считать что вечное хранение.


    1. yttrium
      02.01.2024 14:13
      +1

      Виктория больше про распределенное кластерное решение, в композе выигрыша не заметишь


  1. sire
    02.01.2024 14:13
    +1

    Спасибо за идею с grafana-image-renderer - не знал. И вообще хорошая, полезная статья.

    Один момент: "reverse", а не "reserve" proxy.


    1. skl256 Автор
      02.01.2024 14:13

      Спасибо, поправил опечатку.


      1. sire
        02.01.2024 14:13

        Там ещё осталось "resrve".

        А для отправки скриншототов из графаны ещё, ведь, нужен alertmanager, который остался за рамками статьи?


        1. skl256 Автор
          02.01.2024 14:13

          Ещё раз спасибо, почему то мои пальцы (а затем и глаза, при проверке) пропускают такие ошибки, буду думать, что с этим делать на будущее.

          Касательно alertmanager - в Grafana есть внутренний alertmanager: grafana.com/docs/grafana/latest/alerting/fundamentals/alertmanager/, с ним данный функционал работает.


          1. sire
            02.01.2024 14:13

            Спасибо.


  1. KNN3477
    02.01.2024 14:13
    +1

    Жаль что статья не закрыла вопросы:
    1) Подробный парсинг Nginx (не PLUS версии) лога запросов проекта для детального анализа (пример какой запрос упал с ошибкой 500)

    2) Ошибка 429 кажется, когда идет большой поток инфы и Loki не успевает принять.

    3) Использование Mimir раз уж используем Grafana стек

    И вопрос отдельный, не упадет ли контейнер экспортера если на сервере Loki закончилось дисковое пространство?


    1. skl256 Автор
      02.01.2024 14:13

      Касательно парсинга логов nginx вопрос действительно интересный, особенно интересен ещё тем, что благодаря pattern parser в версиях LogQL (2.3+) такие текстовые логи можно парсить чуть удобнее (чем, например, regex). Но, тут тоже есть некоторые нюансы, по-хорошему для этого нужна отдельная статья.

      То же самое думаю и про Grafana Mimir (и вышеупомянутый в другом комментарии VictoriaMetrics), отлично что эти инструменты упомянуты, но, мне кажется, они находятся чуть повыше чем посыл "быстро и просто" (в заголовке статьи).

      Про status code 429, да, когда логов станет много, в любом случае придётся "тюнить" конфигурации под свою ситуацию (есть разные способы - изменить лимиты в секции limits_config со стороны Loki, изменить интенсивность отправки логов со стороны Promtail, даже вплоть до крайних мер - "дропа" лишних). По понятным причинам все эти настройки оставлены по умолчанию, т.к. вряд ли можно подобрать значение, которое будет лучше для всех.

      Про ситуацию с дисковым пространством, как мне кажется, что если на сервере заканчивается дисковое пространство, то ожидаемо, что работать нормально не будет (но, что именно произойдёт, упадёт контейнер ли, не пробовал). Но если это "боевой" сервер/VM, то, наверное, правильнее изначально продумать так, чтобы ситуации не возникло (например, замониторить и заалертить то самое дисковое пространство), ну и частично уменьшит вероятность попадания в такую ситуацию правильная настройка retention (для очистки старых логов и освобождения места, о последнем в статье упомянул).


  1. mayorovp
    02.01.2024 14:13

    А можно ли как-нибудь обойтись без "log-driver": "json-file"?

    Эта гадость же трёт логи при каждом пересоздании контейнера.