Мониторинг AI-агентов в production: как я поднял Prometheus + Grafana + Loki за 30 минут
Мой AI-агент наглухо завис в 3:15 утра, а я даже не знал, где искать логи
Это был идеальный шторм из моего же высокомерия. Я поставил своего AI-ассистента для автоматизации расшифровки протоколов лабораторных исследований в продакшен в 11 вечера, щёлкнул docker-compose up -d, и пошёл спать с чувством выполненного долга. Агент должен был обрабатывать входящие JSON от ЛИС, структурировать их, генерировать предварительные заключения и класть результат в очередь. Всё работало на локалхосте месяц. Что могло пойти не так?
В 3:15 утра меня разбудил не звук уведомления, а его звенящая тишина. Я спал с телефоном рядом, ожидая отчёта об успешном завершении ночной пачки. Тишина. Первая мысль — «сеть легла». Вторая — «отключили электричество». Третья, когда я увидел горящий роутер и работающий NAS, была уже панической: «агент умер, а я даже не знаю, где он похоронен».
Я вскочил к ноутбуку. SSH на продакшен-сервер, который в ту ночь был просто стареньким Intel NUC в подсобке лаборатории.
ssh user@prod-nuc.local
docker ps
Контейнер висит в списке. Статус Up 3 hours. Вроде жив.
docker logs my-ai-agent-prod
И тут — ничего. Пустота. Последняя строчка в логах трёхчасовой давности: INFO: Started processing batch #42. И тишина. Что дальше? docker exec? А если он в бесконечном цикле? docker stats? CPU 0.1%, память стабильна. Он не упал, он завис. Заблокировался на какой-то операции, поглотил последнюю порцию данных и замолчал навсегда, не выбросив ошибку.
Я начал метаться. Где логи приложения? В контейнере, в /app/logs. А как их посмотреть, если контейнер не отвечает? Копировать с помощью docker cp? А если их 2 гигабайта? Где метрики? Сколько запросов он обработал? Какая была загрузка CPU перед сном? Какие были ошибки в сторонних API (а их было три: LLM-провайдер, векторная БД и внешний справочник МКБ)? Я не знал ровным счётом ничего.
Это был момент полного профессионального провала. 15 лет в медицине приучили меня к протоколам: каждый анализ — след в ЛИС, каждый прибор пишет лог, каждая критическая ошибка дублируется на пейджер. А здесь, в моём собственном digital-проекте, царила доисторическая тьма. Я был как хирург, оперирующий в полной темноте, без мониторов жизненных показателей.
Я потратил 40 минут на угадайку:
1. Перезапуск контейнера (docker-compose restart). Помогло ровно на 7 минут. Агент снова заглох.
2. Рытьё в сырых логах Docker. docker inspect, journalctl -u docker.service. Там были только сообщения о старте и стопе. Ни намёка на причину.
3. Попытка подключиться к STDIN зависшего процесса — полный фарс.
4. Отчаянная проверка диска и памяти на хосте — всё в норме.
Проблема решилась почти случайно. В отчаянии я запустил вторую копию контейнера рядом с первой, временно изменив порт.
docker run -d --name ai-agent-debug -p 8081:8080 \
-e LOG_LEVEL=DEBUG \
-v ./debug_logs:/app/logs \
my-ai-agent:latest
И буквально через минуту новый контейнер тоже завис. Но теперь, с уровнем логирования DEBUG, в свежих логах мелькнула ключевая строчка перед тишиной:
DEBUG: Attempting to acquire lock for external API: 'icd10_api'...
DEBUG: Lock acquired.
DEBUG: Sending request to https://api.icd10.ru/v3/code/F50...
Бинго. Весь процесс упирался в внешний API МКБ-10, который, как я потом выяснил, имел ночное окно обслуживания с 3:00 до 3:30. Мой код использовал синхронный HTTP-клиент без таймаута. Не получив ответа, он вечно ждал, блокируя все остальные потоки. А в продакшене я, умный такой, выставил LOG_LEVEL=INFO чтобы «не засорять диск». В итоге, сообщение о попытке соединения (DEBUG) не писалось, а ошибка таймаута (которая была на уровне WARNING) никогда не наступала. Я сидел слепым.
Этот ночной пожар длился 1 час 10 минут. За это время накопилось 127 необработанных протоколов. Утром пришлось извиняться перед коллегами и разгребать завал вручную. Цена моей беспечности — три чашки крепчайшего кофе, подмоченная репутация и чёткое понимание: продакшен без мониторинга это не продакшен, а мина замедленного действия. Ты не управляешь тем, что не можешь измерить. А я не мог измерить вообще ничего.
И прямо тогда, в 4:30 утра, с красными глазами и трясущимися от кофеина руками, я дал себе слово: к следующему вечеру у меня будет работать полноценный стек мониторинга. Не через месяц, не «когда будет время». Сейчас. Чтобы в следующий раз не гадать, а видеть: график запросов к ICD10 API обнулился в 3:02, количество активных потоков упало до 1, а в логах красуется WARNING - Request timeout. На всё это — 30 минут на подъём. Бросок на грани реальности? Безусловно. Но лучший мотиватор — это вляпаться по полной программе.
Собрать мониторинг на коленке, пока не отключили прод: история одного спасательного круга
Ситуация была проста, как перелом: прод висит, данные не обрабатываются, а я понятия не имею — сервер лег, память кончилась, или мой агент просто решил медитировать над очередным JSON. Паника — отличный мотиватор. У меня было не больше часа, чтобы понять, что происходит, иначе утренняя смена в лаборатории упрётся в пустые экраны, а я получу крайне нелабораторные вопросы от руководства.
Первое, что пришло в голову — залезть в логи. Ага. Логи. Я же их в stdout контейнера пихал, наивно полагая, что docker logs --tail 50 my_agent — это и есть продакшен-мониторинг. Когда контейнеров пять, а запросов 500 в минуту — это уже не работает. Нужна была система. Прямо сейчас.
План на спасение был таким: 1. Собрать метрики с агентов (CPU, память, кол-во запросов, ошибки) — это Prometheus. 2. Собрать логи в одно место, где можно искать — это Loki. 3. Сделать дашборд, где всё это видно одним взглядом — это Grafana. 4. Упаковать в Docker, чтобы поднялось одной командой и жило на той же машине.
Я не стал разворачивать Kubernetes или возиться с Helm. Время было против меня. Нужен был "спасательный круг" — минимальный, но рабочий стек, который можно набросать в docker-compose.yml за один присест.
Начал с Prometheus. Конфиг — это святое. Мне нужно было, чтобы он скребанул (scrape) метрики с моего Python-агента (я уже добавил туда prometheus_client), и заодно собрал метрики самой Docker-ноды через node_exporter.
Вот тот самый prometheus.yml, который родился в аврале:
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Мониторим самого Prometheus
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Мониторим ноду (сервер) через node_exporter
- job_name: 'node'
static_configs:
- targets: ['node_exporter:9100']
# Мониторим нашего AI-агента
- job_name: 'ai_agent'
static_configs:
- targets: ['ai_agent:8000'] # Порт, на котором висит /metrics эндпоинт
scrape_interval: 10s # Чаще, потому что агент — наше всё
Дальше — Loki. Тут хитрость в том, чтобы настроить драйвер логов для Docker. Вместо того чтобы куда-то писать файлы, все контейнеры будут слать логи прямиком в Loki. Добавляем в docker-compose.yml для сервиса агента:
ai_agent:
image: my-ai-agent:latest
logging:
driver: "loki"
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-external-labels: "job=docker_logs,container_name={{.Name}},service=ai_agent"
И последний штрих — Grafana. Её я настроил на предустановленные источники данных (Datasources) через provisioning, чтобы не тыкать в UI после каждого поднятия стека.
Создал файл grafana/provisioning/datasources/datasources.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: lokagraf
access: proxy
url: http://loki:3100
version: 1
editable: false
Весь этот зоопарк собрался в итоговый docker-compose.yml. Ключевой момент — правильные версии образов, чтобы всё дружило. Я зафиксировал те, что проверил:
# docker-compose.monitoring.yml (версия 1, "спасательный круг")
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.45.0
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prom_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=7d'
ports:
- "9090:9090"
networks:
- monitoring
node_exporter:
image: prom/node-exporter:v1.6.0
container_name: node_exporter
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
networks:
- monitoring
loki:
image: grafana/loki:2.8.2
container_name: loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring
grafana:
image: grafana/grafana:10.0.3
container_name: grafana
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=secret # Меняй немедленно!
- GF_INSTALL_PLUGINS=grafana-piechart-panel
ports:
- "3000:3000"
networks:
- monitoring
networks:
monitoring:
driver: bridge
volumes:
prom_data:
grafana_data:
Команда развёртывания, которая спасла мне прод:
# В папке с docker-compose.monitoring.yml
docker-compose -f docker-compose.monitoring.yml up -d
Через 30 секунд Prometheus был на http://сервер:9090, Grafana — на http://сервер:3000. Логин admin, пароль secret. Первое, что я сделал — добавил в Grafana источник Loki и простой график из Prometheus: использование памяти процессом агента. Это заняло ещё 5 минут.
И вот он, момент истины: на дашборде я увидел не просто "система жива", а конкретную проблему — график памяти моего агента неуклонно полз вверх, пока не упирался в лимит контейнера, после чего контейнер падал. Утечка памяти. Локи моментально показал последние логи перед падением — ошибка обработки определённого типа PDF-вложения, которая не освобождала ресурсы.
Это было в 3:50 утра. В 3:55 я закоммитил хотфикс, откатив обработку проблемных файлов в синхронный режим. В 4:05 новый образ был собран и задеплоен. К 4:15 метрики показали, что память стабилизировалась. Утренняя смена пришла на рабочие места.
Спасательный круг сработал. Он был кривой, временный и собран на коленке, но он дал мне то, чего не было раньше — видимость. Теперь я не гадал, я знал. А это в нашей работе — уже половина лечения.
Стэк, который заработал у меня за 30 минут: Prometheus, Grafana и Loki в Docker-связке
Счётчик пошёл. У меня было 60 минут, чтобы не просто посмотреть на ворчащий контейнер через docker logs, а получить систему, которая покажет мне: что падает, почему и что было ДО этого. Мой выбор пал на классическую связку, которую можно развернуть одним файлом: Prometheus для метрик, Grafana для дашбордов и Loki для логов. Вся философия — pull-based, всё в контейнерах.
Первый шаг — создал директорию monitoring прямо рядом с проектом и набросал docker-compose.yml. Версии — строго фиксированные, чтобы через полгода всё не рассыпалось после docker-compose pull.
# docker-compose.yml для экстренного развёртывания
version: '3.8'
networks:
monitoring:
driver: bridge
volumes:
prometheus_data: {}
grafana_data: {}
loki_data: {}
services:
prometheus:
image: prom/prometheus:v2.45.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:9.5.2
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123 # Меняй немедленно после запуска!
- GF_INSTALL_PLUGINS=grafana-clock-panel
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- "3000:3000"
networks:
- monitoring
loki:
image: grafana/loki:2.8.2
container_name: loki
restart: unless-stopped
volumes:
- ./loki/local-config.yaml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
networks:
- monitoring
promtail:
image: grafana/promtail:2.8.2
container_name: promtail
restart: unless-stopped
volumes:
- ./promtail/config.yaml:/etc/promtail/config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
command: -config.file=/etc/promtail/config.yaml
networks:
- monitoring
Ключевой момент — promtail монтирует /var/lib/docker/containers. Это волшебный путь, где лежат все JSON-логи контейнеров. Promtail будет их хвостить и отправлять в Loki. Никаких агентов в каждый сервис.
Теперь конфиги. Prometheus нужно сказать, что скребать. Я создал prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'docker'
static_configs:
- targets: ['host.docker.internal:9323']
Ага, а откуда взяться метрикам Docker? Нужно было включить метрики в самом демоне. Экстренно, без перезагрузки всей машины, я добавил в /etc/docker/daemon.json (или создал его):
{
"metrics-addr": "0.0.0.0:9323",
"experimental": true
}
И перезапустил демон: sudo systemctl restart docker. Через минуту Prometheus уже видел хост.
Конфиг Loki (loki/local-config.yaml) — минимальный, для теста:
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:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v12
index:
prefix: index_
period: 24h
И мозг всей этой лог-системы — promtail/config.yaml. Он должен знать, куда слать логи (в Loki) и что именно собирать.
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
Всё. В директории monitoring теперь лежат 4 файла. Запускаю: docker-compose up -d.
Хронометраж:
* Минута 1-5: Пишу docker-compose.yml.
* Минута 5-10: Копирую и слегка правлю конфиги из документации (самая долгая часть — не ошибиться в отступах YAML).
* Минута 10-12: sudo systemctl restart docker.
* Минута 12-13: docker-compose up -d.
* Минута 13-20: Жду, пока все 4 контейнера (prometheus, grafana, loki, promtail) поднимутся. Проверяю docker-compose ps — все в статусе Up. Это критично.
* Минута 20-25: Иду в браузер: http://localhost:9090. Prometheus жив. В таргетах (Status -> Targets) вижу docker и prometheus — оба UP. Уже хорошо.
* Минута 25-30: Логинюсь в Grafana (http://localhost:3000, admin/admin123). Первый шаг — добавить источники данных (Data Sources). Делаю это через UI, потому что на прошивку через provisioning нет времени.
В Grafana:
1. Configuration -> Data Sources -> Add data source.
2. Prometheus: URL http://prometheus:9090 (из сети Docker!). Сохраняю.
3. Loki: URL http://loki:3100. Сохраняю.
Всё. Инфраструктура готова. За 30 минут у меня появился не просто "мониторинг", а централизованное окно во всю систему. В Prometheus я могу запросить container_memory_usage_bytes{container="my_ai_agent"} и увидеть, как пожирается память. В Loki — выбрать контейнер my_ai_agent и потоково читать его логи, как tail -f, но с поиском и фильтрами по меткам.
Это был не идеальный, не отказоустойчивый и не безопасный стэк (пароль admin123!). Это был спасательный жилет, надутый за полчаса под писк тишины из прода. Он дал мне главное — видимость. Теперь можно было не гадать, а идти и смотреть, что именно пошло не так. А что пошло не так при масштабировании этого "жилета" — тема для следующей пачки обезболивающего и следующей секции.
Когда один контейнер Loki мало: масштабирование сбора логов и метрик под растущую нагрузку
Мой мониторинг прожил ровно три дня в состоянии блаженного покоя. Потом нагрузка на агентов подскочила — подключили новый филиал лаборатории. И мой уютный одинокий контейнер loki:2.9.2, который так мило собирал логи, начал тихо сходить с ума.
Grafana показывала пустые графики по логам, а в терминале я видел классику:
docker logs my_loki_1 | tail -5
level=warn ts=2024-02-15T08:47:12.123Z caller=log.go:168 msg="POST /loki/api/v1/push (500) 1.23456789s Response: \"Ingestion rate limit exceeded (limit: 8388608 bytes/sec) while attempting to ingest '8388610' lines\""
Превышен лимит скорости приёма. Лимит по умолчанию — 8 MB/сек. Мои агенты в пике начали выстреливать логи на 15-20 MB/сек. Loki начал отбрасывать данные, как перегруженный сортировочный центр. Потеря логов в продакшене — это как потеря пробирок с анализами: ты не знаешь, что было внутри, и восстанавливать уже нечего.
Время масштабироваться. Один Loki — это для пет-проектов. Нам нужна отказоустойчивость и горизонтальное масштабирование.
Я пошёл по пути Loki в режиме микросервисов (Microservices Mode), а не монолита. Это разделяет компоненты: ingester (приём и хранение в памяти), distributor (распределение запросов), querier (поиск) и compactor (уплотнение данных).
Вот docker-compose.scale.yml, который я набросал, пока пил третью чашку кофе:
# docker-compose.scale.yml
version: '3.8'
services:
# Minio как S3-совместимое хранилище для чанков Loki (вместо локальной директории)
minio:
image: minio/minio:RELEASE.2024-01-16T16-07-38Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: lokiadmin
MINIO_ROOT_PASSWORD: supersecurepassword123
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
# Consul для хранения конфигурации и сервис-дискавери Loki компонентов
consul:
image: consul:1.16
command: "agent -server -bootstrap-expect=1 -ui -client=0.0.0.0"
environment:
CONSUL_BIND_INTERFACE: eth0
volumes:
- consul_data:/consul/data
# Distributor (2 реплики) - точка входа для логов
loki-distributor-1:
image: grafana/loki:2.9.2
command: -target=distributor -config.file=/etc/loki/config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
ports:
- "3100:3100" # Только на первом дистрибьюторе
depends_on:
minio:
condition: service_healthy
consul:
condition: service_started
loki-distributor-2:
image: grafana/loki:2.9.2
command: -target=distributor -config.file=/etc/loki/config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
depends_on:
- loki-distributor-1
# Ingester (3 реплики) - хранят "горячие" логи в памяти
loki-ingester-1:
image: grafana/loki:2.9.2
command: -target=ingester -config.file=/etc/loki/config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
depends_on:
consul:
condition: service_started
loki-ingester-2:
image: grafana/loki:2.9.2
command: -target=ingester -config.file=/etc/loki/config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
depends_on:
- loki-ingester-1
# ... loki-ingester-3 (аналогично)
# Querier (2 реплики) - обрабатывают запросы из Grafana
loki-querier-1:
image: grafana/loki:2.9.2
command: -target=querier -config.file=/etc/loki/config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml
ports:
- "3101:3100" # Отдельный порт для квериеров, если нужно
depends_on:
consul:
condition: service_started
# Prometheus теперь должен знать про ВСЕ компоненты
prometheus:
image: prom/prometheus:v2.48.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prom_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
depends_on:
- loki-distributor-1
- loki-querier-1
# Grafana
grafana:
image: grafana/grafana:10.2.3
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
ports:
- "3000:3000"
depends_on:
- prometheus
volumes:
minio_data:
consul_data:
prom_data:
grafana_data:
А вот ключевой фрагмент конфига Loki (loki-config.yaml), который заставляет эту орду контейнеров работать как единое целое:
# loki-config.yaml
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 3 # Сколько копий каждого чанка хранить
ring:
kvstore:
store: consul
consul:
host: consul:8500
prefix: collectors/
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: s3
schema: v13
index:
prefix: index_
period: 24h
storage_config:
boltdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/index_cache
shared_store: s3
aws:
s3: http://minio:9000
s3forcepathstyle: true
access_key_id: lokiadmin
secret_access_key: supersecurepassword123
bucketnames: loki-data
ingester:
lifecycler:
ring:
kvstore:
store: consul
consul:
host: consul:8500
prefix: ingesters/
replication_factor: 3
final_sleep: 0s
chunk_idle_period: 1h
max_chunk_age: 2h
chunk_target_size: 1048576 # 1MB
chunk_retain_period: 30s
limits_config:
ingestion_rate_mb: 25 # Поднимаем лимит с 8 до 25 MB/сек
ingestion_burst_size_mb: 30
max_entries_limit_per_query: 10000
chunk_store_config:
max_look_back_period: 720h # 30 дней
table_manager:
retention_deletes_enabled: true
retention_period: 720h
Самое важное — настройка Prometheus. Теперь он должен скрейпить метрики не с одного Loki, а со всех её компонентов. Используем docker-compose DNS:
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'loki-distributors'
static_configs:
- targets: ['loki-distributor-1:3100', 'loki-distributor-2:3100']
- job_name: 'loki-ingesters'
static_configs:
- targets:
- 'loki-ingester-1:3100'
- 'loki-ingester-2:3100'
- 'loki-ingester-3:3100'
- job_name: 'loki-queriers'
static_configs:
- targets: ['loki-querier-1:3100', 'loki-querier-2:3100']
- job_name: 'ai-agents'
static_configs:
- targets: ['agent-api:8000'] # Твои приложения
Запуск: docker-compose -f docker-compose.scale.yml up -d --scale loki-ingester=3 --scale loki-querier=2.
Что это дало на практике?
1. Отказоустойчивость. Я убил один из ingester'ов командой docker kill. Логи продолжили писаться. Querier автоматически перенаправил запросы на другие ноды. В Grafana просто появился алерт на падение инстанса.
2. Пропускная способность. Суммарный лимит приёма стал ~75 MB/сек (25 MB/сек * 3 ingester). Мне хватило с запасом.
3. Производительность поиска. Два querier'а распределяли нагрузку от дашбордов Grafana. Отклик на сложные запросы по логам упал с 4-5 секунд до 1-2.
Цена вопроса:
* Потребление памяти выросло с ~512 MB у одинокого Loki до ~3.5 GB на всю связку.
* Задержка на запись лога увеличилась незначительно, на 10-15 мс (из-за сетевого взаимодействия между компонентами).
* Сложность управления: теперь нельзя просто docker restart loki. Нужно понимать, какой компонент перезапускаешь.
Но это плата за взрослый продакшен. Когда в 4 удяра пришёл очередной выброс запросов на расшифровку ЭКГ, моя система мониторинга даже не моргнула. Она просто записала всё, а я утром увидел красивый график пика в Grafana и понял, что пора масштабировать уже самих AI-агентов. Но это уже другая история.
Ошибки, которые съели два часа моей жизни, и как их избежать вам
Вы думаете, после того как я запустил Grafana и увидел первые графики, всё стало гладко? Ха. Настоящая битва только начиналась. Следующие два часа я потратил не на настройку красивых дашбордов, а на войну с собственным невежеством и коварством конфигурационных файлов. Вот три ошибки, каждая из которых стоила мне нервов, времени и пары седых волос.
Ошибка №1: Прометею нельзя доверять сети по умолчанию
Мой первый prometheus.yml был образцом наивности:
# prometheus/prometheus.yml (ВЕРСИЯ-РАЗБОЙНИК)
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'ai-agent'
static_configs:
- targets: ['ai-agent:8000']
Я запустил связку, открыл Prometheus на 9090 и в статусе таргетов увидел душераздирающее DOWN. «Как же так, — думал я, — контейнеры же в одной сети Docker!». Оказалось, они в одной сети, которую Docker Compose создал автоматически, но Prometheus по умолчанию использует сеть хоста, а не эту самую сеть.
Решение: Явно указать сеть в docker-compose.yml и убедиться, что все сервисы в ней.
# docker-compose.yml (исправленный фрагмент)
networks:
monitoring:
driver: bridge
services:
prometheus:
image: prom/prometheus:v2.51.2
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--web.enable-lifecycle' # Важно для перезагрузки конфига без рестарта контейнера
volumes:
- ./prometheus:/etc/prometheus
- prom_data:/prometheus
ports:
- "9090:9090"
networks:
- monitoring # Явное подключение к сети
restart: unless-stopped
ai-agent:
build: ./agent
container_name: ai-agent
# ... остальные настройки агента
networks:
- monitoring # Агент тоже в этой сети!
expose:
- "8000" # Важно: не ports, а expose для внутренней сети
И команда, которая стала моим спасением для проверки связности: docker exec -it prometheus ping ai-agent. Увидел ответ — выдохнул.
Ошибка №2: Loki не ел логи, потому что я кормил его не тем
Мой агент писал логи в stdout. В теории, Docker-драйвер loki должен был их подхватить. На практике в Grafana в разделе Loki я видел пустоту. Оказалось, проблема в двух местах.
Во-первых, в docker-compose.yml для самого агента нужно было указать правильный лог-драйвер:
ai-agent:
# ... остальной конфиг
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-external-labels: "job=docker-logs,container_name={{.Name}},service=ai-agent"
Во-вторых (и это съело львиную долю времени), контейнер loki сам по себе не собирает логи. Нужен сборщик — promtail или, в случае с Docker, тот самый драйвер. Но для уже работающих контейнеров (как мой старый агент) логи не начнут течь сами собой. Пришлось пересоздавать сервис.
Команда для принудительного применения новых настроек логов:
# Останавливаем, удаляем контейнер (том с данными останется)
docker-compose stop ai-agent
docker-compose rm -f ai-agent
# Запускаем заново — он подхватит новый конфиг логов из docker-compose.yml
docker-compose up -d ai-agent
Ошибка №3: Grafana молчала о главном
Дашборд собран, метрики requests_total и request_duration_seconds рисуются красивыми графиками. Но один из четырёх воркеров агента периодически отваливался, а Grafana хранила гордое молчание. Я не отслеживал состояние самих таргетов Prometheus.
Решение: Добавить на дашборд самый важный виджет — статус up для всех джоб.
В Grafana создаём новую панель, выбираем источник данных Prometheus и пишем простейший, но гениальный запрос:
up{job="ai-agent"}
Значение 1 — таргет жив и отвечает. 0 — мёртв. Визуализируем это как Stat (просто большое число) и, что важнее, как Table или State timeline, чтобы видеть историю сбоев.
Мой финальный конфиг prometheus.yml для агента с метриками Python (библиотека prometheus_client) стал выглядеть так:
# prometheus/prometheus.yml (ФИНАЛЬНАЯ, РАБОЧАЯ ВЕРСИЯ)
global:
scrape_interval: 15s
evaluation_interval: 30s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Мониторим самого себя!
metrics_path: /metrics
scheme: http
- job_name: 'ai-agent'
metrics_path: /metrics # Стандартный эндпоинт для prometheus_client
static_configs:
- targets: ['ai-agent:8000']
labels:
service: 'ai-core'
version: '1.2'
scrape_interval: 10s # Для агента можно чаще
scrape_timeout: 5s # Жёсткий таймаут
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
# Для мониторинга ресурсов контейнеров
И финальный штрих — алерты. Без них мониторинг слеп. Я добавил в prometheus/ файл alerts.yml и подключил его в основном конфиге. Правило, которое спасло бы меня в ту самую ночь:
# prometheus/alerts.yml
groups:
- name: ai_agent_alerts
rules:
- alert: AIAgentDown
expr: up{job="ai-agent"} == 0
for: 1m # Ждём минуту, чтобы отсечь кратковременные глюки
labels:
severity: critical
service: ai_agent
annotations:
summary: "AI Agent {{ $labels.instance }} is DOWN"
description: "The AI Agent container has been unreachable for over 1 minute. Immediate investigation required."
runbook_url: "http://wiki.internal/ai-agent-failure"
Эти два часа научили меня простой истине: в продакшене не бывает «просто поднять». Бывает «поднять, проверить каждое соединение, убедиться, что данные текут, и подготовить систему криков на случай, если они перестанут течь». Следующий шаг — автоматизировать эти крики.
Выводы: что я сделал бы иначе, и готовый docker-compose.yml в копилку
Значит, подводим итоги. Я просидел ночь с мониторингом, потратил на него в сумме часов шесть (включая те два, что съели тупые ошибки), и теперь у меня есть система, которая не даёт агентам умирать молча. Но самое ценное — не работающие графики, а осознание, что я бы сделал всё иначе, если бы стартовал сегодня.
Вот мой главный вывод, выстраданный кофеином и паникой: мониторинг — это не фича, это часть прод-окружения с минуты ноль. Нельзя сначала запустить продакшен, а потом думать, как за ним следить. Это как в лаборатории: вы же не начинаете ставить анализы, не откалибровав анализатор? Вот и тут так же.
Урок 1. Docker Compose — это черновик. Версионируй конфиги и готовься к Helm сразу.
Мой docker-compose.yml, который я приведу ниже, — это спасательный круг, а не лайнер для океана. Он идеален, чтобы за 30 минут получить хоть какое-то зрение в прод. Но он же — ловушка.
Я потратил два дня, чтобы перевести эту связку в Kubernetes (k3s, если быть точным) с помощью Helm-чартов. Почему?
* Автомасштабирование: Horizontal Pod Autoscaler для Loki-ингэстеров в пару строк конфига.
* Управление секретами: Не таскать пароли от Grafana в открытом виде в docker-compose.
* Жизненный цикл: helm upgrade --install вместо docker-compose down && docker-compose up -d.
Что делать вам: Сразу пишите docker-compose с оглядкой на будущий перенос. Используйте .env файл для переменных, разделяйте сервисы на отдельные конфиги (prometheus, loki, grafana), чтобы потом их было проще превращать в Helm-шаблоны.
Урок 2. Собирай ВСЕ логи, даже если не знаешь, зачем. Фильтровать потом — проще, чем искать потерянное.
Моя первая ошибка — попытка сэкономить место и писать в Loki только ERROR. Когда агент начал "глючить", не падая, в логах была тишина. Оказалось, он зациклился на одном запросе, но уровень был INFO.
Loki сжимает текстовые логи до 5-10% от исходного объема. Диск — дешевый. Время на отладку инцидента — дорогое.
Мой совет: Настройте в агенте или в лог-драйвере Docker сбор ВСЕХ логов (stdout, stderr). В Loki используйте легковесные метки для партиционирования (container_name, service, level), а фильтрацию по содержимому делайте уже в Grafana. Задавайте политику хранения (я ставлю 30 дней для всех логов, 90 дней — для ERROR и WARN).
Урок 3. Prometheus — не для бизнес-метрик. Отделяй мух от котлет.
Я попытался запихнуть в Prometheus всё: и потребление CPU контейнером, и количество обработанных заявок, и среднее время обработки. Для последних двух — это плохая идея. Prometheus отлично считает системные и инфраструктурные метрики (память, сеть, диск, HTTP-запросы). Но для бизнес-событий (например, "одна расшифровка протокола завершена") лучше использовать что-то вроде StatsD или сразу писать в специализированную БД вроде TimescaleDB.
Что сделал я: Поднял в том же Docker-композе контейнер statsd-exporter. Мой агент теперь шлёт UDP-пакетом событие agent.request_processed:1|c. statsd-exporter преобразует это в метрику Prometheus. Чисто, изолированно, и не засоряет код агента Prometheus-клиентом.
Урок 4. Дашборд Grafana — это код. Храни его в Git.
Первые дашборды я накликивал мышкой в интерфейсе Grafana. Потом пришлось восстанавливать их после небольшого инцидента с volume. Это был ад.
Grafana поддерживает Provisioning. Вы описываете источники данных (Data Sources) и дашборды в YAML-файлах, кладёте их в директорию /etc/grafana/provisioning. При старте Grafana подхватывает конфигурацию автоматически.
Это меняет всё:
1. История изменений дашборда.
2. Восстановление одним docker-compose up.
3. Возможность иметь разные дашборды для теста и прода (например, через переменные в конфиге).
Урок 5. Твой главный индикатор — не график, а алерт, который разбудит.
Красивые графики — это для ретроспективы и отчётов. Когда что-то ломается в 3 утра, тебе нужен не график, а оглушительный звонок в тишине.
Я настроил алерты в Grafana (можно и в Prometheus Alertmanager, но для начала хватит Grafana) на три ключевые метрики:
1. Отсутствие heartbeата от агента > 2 минут. (Простой запроса к Prometheus на up{job="ai-agent"} == 0)
2. Рост 5xx ошибок HTTP выше порога в 5% за 5 минут.
3. Аномальное потребление памяти > 90% от лимита контейнера.
Алерты уходят в Telegram-бота. Не в почту, не в Slack, который я могу не проверить. Именно в Telegram, потому что телефон всегда со мной. Настройка через curl к API Telegram и вебхук в Grafana — дело 10 минут.
Готовый docker-compose.yml в копилку
Вот тот самый файл, с которого можно начать. Он включает Prometheus, Grafana, Loki, Promtail (для сбора логов) и Node Exporter (для метрик хоста). Всё настроено на работу друг с другом. Версии зафиксированы — чтобы через год всё не развалилось.
# monitoring-stack/docker-compose.yml
version: '3.8'
networks:
monitoring:
driver: bridge
volumes:
prometheus_data: {}
grafana_data: {}
loki_data: {}
services:
# Хранилище метрик и мозг алертинга
prometheus:
image: prom/prometheus:v2.48.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring
# Сборщик логов с контейнеров и хоста
promtail:
image: grafana/promtail:2.9.2
container_name: promtail
restart: unless-stopped
volumes:
- ./promtail/config.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
networks:
- monitoring
# Хранилище и индексатор логов
loki:
image: grafana/loki:2.9.2
container_name: loki
restart: unless-stopped
volumes:
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
networks:
- monitoring
# Визуализация всего и вся
grafana:
image: grafana/grafana:10.2.3
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-supersecret} # Меняй через .env!
- GF_INSTALL_PLUGINS=grafana-piechart-panel
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning/:/etc/grafana/provisioning/:ro
ports:
- "3000:3000"
networks:
- monitoring
# Сбор метрик с хоста (CPU, память, диск)
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
networks:
- monitoring
И обязательные конфиги рядом с ним:
prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
# СЮДА ты добавишь своего AI-агента
# - job_name: 'ai-agent'
# static_configs:
# - targets: ['ai-agent-host:8000']
promtail/config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
target_label: 'container'
grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
Запускаешь ADMIN_PASSWORD=realpassword docker-compose up -d в папке monitoring-stack. Через минуту заходишь на http://твой-сервер:3000 (логин admin, пароль из переменной), добавляешь дашборды — и у тебя есть зрение.
Итоговый итог. Эти 30 минут на поднятие стека — лучшее вложение в свой сон и психическое здоровье. Теперь мой агент не может просто так взять и умереть. Он будет долго и мучительно агонизировать на красивых графиках, посылая мне телеграмные крики о помощи. А я, вместо панического погружения в логи трёхдневной давности, буду знать: кончилась память на 17-й секунде, потому что пришёл запрос с вложенным PDF в 500 страниц. И это уже не мистика, а инцидент, который можно пофиксить.
Мониторинг — это не про контроль. Это про понимание. И, в конечном счёте, про спокойный сон, пока твоё творение работает в темноте.