Observability
Problem
Заголовок раздела «Problem»Прод развёрнут и работает, но мы про него ничего не знаем. Всё, чем сейчас можно диагностировать — docker ps и docker logs по SSH.
За одну сессию развёртывания это выстрелило четыре раза:
- Контейнер
Up, приложение мертво.auth-apiиuser-apiкрутились в рестарт-петле PM2, при этомdocker psпоказывалUp N minutes. Traefik продолжал слать туда трафик, потому что проверять ему нечего — health-эндпоинта нет. - Устаревшие логи вводят в заблуждение. Дважды читал через
docker logs --since 2mошибки уже убитого контейнера и делал вывод, что фикс не сработал. Централизованных логов с таймстемпами нет — сопоставить «когда упало» и «когда задеплоили» нечем. - 404 от Traefik неотличим от лежащего приложения. Потеряли время, разбираясь, сломан ли сайт, хотя приложение отдавало 200 на своём порту, а 404 приходил от прокси.
- Тихие отказы. SMTP с неверным TLS не ошибается — он висит до таймаута. Битый YAML Traefik молча отверг, продолжив на старом конфиге. Без метрик и алертов такое живёт до жалобы пользователя.
Ни один из этих случаев не был бы загадкой при наличии метрик, централизованных логов и health-чеков.
Decision
Заголовок раздела «Decision»Ставим Prometheus + Loki + Grafana + Alloy отдельным стеком в Dokploy.
Почему этот стек, а не другой. Команда уже работает с Grafana, Prometheus и Loki на другом проекте — нулевая кривая обучения, переиспользуемые паттерны метрик и готовая интуиция при разборе инцидентов. Это перевешивает любые аргументы про элегантность альтернатив.
Почему Alloy, а не promtail + node-exporter + cAdvisor. Alloy закрывает все три роли:
| Роль | Компонент Alloy |
|---|---|
| Сбор docker-логов → Loki | loki.source.docker + discovery.docker |
| Метрики хоста (CPU/RAM/диск/сеть) | prometheus.exporter.unix |
| Метрики контейнеров (рестарты, OOM) | prometheus.exporter.cadvisor |
Один агент вместо трёх контейнеров: меньше конфигов, одно место для отладки. Promtail к тому же объявлен устаревшим в пользу Alloy.
S3 — целевое состояние, файловая система — стартовое. Loki работает на файловой системе, Prometheus пишет в локальный TSDB. При 51 GB свободного диска это полноценный вариант, а не компромисс, и он снимает блокировку от ожидания S3-кредов.
Чтобы переезд на S3 позже был сменой конфига, а не переархитектурой, стартуем сразу на TSDB + schema v13 (не на устаревшем boltdb-shipper). У Loki schema_config.configs — это список периодов, каждый со своим хранилищем. Миграция = дописать новый период с object_store: s3, начинающийся с будущей даты. Старые чанки остаются на диске и дочитываются оттуда, новые пишутся в S3. Без простоя и без переливки данных — язык конфига спроектирован ровно под этот сценарий.
Важная оговорка про метрики. У Prometheus нативного S3 нет — это не «включить позже галочкой». Долгосрочное хранение метрик в объектном хранилище означает Mimir или Thanos, то есть другую архитектуру. Поэтому «целевое S3» относится к логам; для метрик локального TSDB достаточно надолго, а переход на Mimir — отдельное решение, когда упрёмся в ретеншн или захотим федерацию.
Отдельный репозиторий
Заголовок раздела «Отдельный репозиторий»Стек живёт в crewsforge-observability, не в crewsforge-back.
Не в бэкенде — потому что там включён автодеплой по пушу в main для всех пяти сервисов (проверено на практике). Правка дашборда пересобирала бы весь бэкенд.
Не по репе на компонент, как сделано для qdrant и rebbit-mq — потому что здесь конфиги взаимозависимы: Alloy знает адрес Loki, Prometheus знает что скрейпить, Grafana знает оба datasource, дашборды осмысленны только вместе с метриками. Добавление одной цели скрейпа затрагивает 2–3 файла; в разных репах это рассинхрон. Qdrant и RabbitMQ самодостаточны — там разделение оправдано.
Один compose-сервис в Dokploy, а не четыре: компоненты поднимаются и живут вместе, внутри одного compose видят друг друга по именам без внешних сетей. Данные на volume, рестарт стека ничего не теряет.
Changes
Заголовок раздела «Changes»Новый репозиторий
Заголовок раздела «Новый репозиторий»crewsforge-observability/├── alloy/config.alloy├── prometheus/prometheus.yml├── prometheus/rules/*.yml├── loki/loki-config.yml├── grafana/provisioning/datasources/ # Prom + Loki подключаются сами├── grafana/provisioning/dashboards/├── grafana/dashboards/*.json # дашборды в git, не «нарисовал и потерял»└── dokploy/docker-compose.observability.ymlЧто собираем
Заголовок раздела «Что собираем»Слой 1 — без правок кода. Даёт результат сразу:
| Сигнал | Источник | Отвечает на вопрос |
|---|---|---|
| CPU, RAM, диск, сеть хоста | prometheus.exporter.unix | Не упрёмся ли в ресурсы |
| Рестарты, OOM, потребление контейнеров | prometheus.exporter.cadvisor | Кто перезапускается и почему |
| Логи всех контейнеров | loki.source.docker | Что происходило и в каком порядке |
| Метрики Traefik | встроенный экспортер Traefik | Коды ответов, латентность по роутерам |
| Внешние пробы доменов | prometheus.exporter.blackbox | Жив ли сайт снаружи, не истекает ли сертификат |
Метрики Traefik закрывают львиную долю RED-картины (rate/errors/duration по маршрутам) без единой строчки в приложениях — прокси видит все запросы. Это важно: слой 1 даёт наблюдаемость раньше, чем дойдут руки до кода.
Слой 2 — требует изменений в crewsforge-back (отдельный PR).
/health— которого сейчас нет вообще. Проверка Postgres, Redis, RabbitMQ. Позволит Traefik и Dokploy видеть реальное состояние, а не «процесс запущен»./metricsчерезprom-client: бизнес-метрики, которых прокси не видит — успешные и неуспешные регистрации, отправки писем, публикации в очередь, длительность запросов к БД.
Метрики HTTP проектируем по паттерну, уже используемому командой: гистограмма с лейблами {method, route}, отдельный счётчик, status_code не в гистограмме. Причина — кардинальность: status_code в лейблах гистограммы умножает число временных рядов на количество кодов, а нужен он в основном для счётчика ошибок.
Логирование: один механизм на всё
Заголовок раздела «Логирование: один механизм на всё»Сейчас логирование неоднородно: 9 мест с new Logger(), из них 13 вызовов структурных и 17 строковых. Строковые запекают значения в текст — `Client ${client.id} connected to project ${projectId}` — и фильтровать по projectId можно только регуляркой.
Плюс дефолтный логгер NestJS печатает объекты с переносами строк, а Docker отдаёт каждую строку отдельно: одно событие превращается в 10 записей в Loki. Это обнаружилось при первой же проверке сбора логов — контекст события рассыпается, счётчики ошибок завышаются в разы.
Решение: nestjs-pino, конвенция переносится из desktop-app, где она уже обкатана в бою. Не изобретаем — берём работающее:
this.logger.error({ action: 'ChatService/createChat', // Класс/метод data: { companyId, visitId, clientPhone }, // контекст err: e, // ошибка отдельным полем});Инжект PinoLogger через DI, autoLogging: false (автологирование HTTP выключено — как там и сделано), stage для разметки этапов, доменные идентификаторы верхним уровнем.
Три отличия от desktop-app:
redactобязателен. Там его нет, но там иautoLoggingвыключен, так что заголовки в лог не попадали. Здесь уже логируется email, а логи живут 30 дней и видны каждому со входом в Grafana. Маскируемauthorization,cookie, пароли, коды; email частично.requestIdиз CLS.ClsModuleс генерацией id изX-Request-Idуже настроен во всех сервисах — и никуда не идёт. Подключаем к pino: один запрос становится прослеживаемым через все строки.- Правило «только скаляры» в
data. В desktop-app встречаетсяdata: { webhook, event }— целые объекты в логе; это раздувает Loki и тянет туда что попало.
Почему логирование идёт перед метриками: оно ценно само по себе (сегодняшние четыре инцидента — все про логи, не про метрики), и метрики потом ложатся на готовый каркас с тем же принципом «одна либа, подключение одной строкой».
Правило отбора: алерт обязан требовать действия. Всё остальное — дашборд.
Стартовый набор:
| Алерт | Условие | Почему важен |
|---|---|---|
| Сервис недоступен | blackbox-проба падает 2 мин | Пользователь этого не увидит раньше нас |
| Крэшлуп | рестарты контейнера > 3 за 10 мин | Ровно случай auth-api/user-api |
| Сертификат истекает | < 14 дней | Let’s Encrypt обновляет сам, но молча ломается |
| Диск заканчивается | > 85% | Prometheus и Loki растут постоянно |
| Всплеск 5xx | доля 5xx > 5% за 5 мин по роутеру | Отличает «сломали релизом» от «пользователи ушли» |
| OOM-kill | событие в cadvisor | Тихий убийца, в логах приложения не виден |
Чего не алертим на старте: латентность до накопления базовой линии. Алерт на p95 без понимания нормы даёт ложные срабатывания и убивает доверие ко всем остальным — на другом проекте ровно это и произошло, алерт флапал с NaN.
Grafana на grafana.crewsforge.com (A-запись уже есть), TLS от Let’s Encrypt, вход через встроенную авторизацию Grafana. Prometheus, Loki и Alloy наружу не публикуются — только внутри dokploy-network; при необходимости через SSH-туннель.
Ретеншн
Заголовок раздела «Ретеншн»Исходя из 51 GB свободного диска:
- Prometheus — 15 дней (~2–4 GB при текущем объёме)
- Loki — 30 дней (~5–10 GB, зависит от разговорчивости)
Обе цифры с большим запасом; пересмотрим по факту роста. На оба хранилища ставим лимит по размеру, чтобы диск не кончился внезапно — иначе упадёт вся нода, а не только мониторинг.
Технический долг (не в этой задаче)
Заголовок раздела «Технический долг (не в этой задаче)»Инфраструктура существует только на сервере, вне git. Всплыло по ходу работы:
docker-compose.ymlRabbitMQ живёт только в БД Dokploy. Я правил его вручную — добавилdokploy-network, заменил слабый пароль из шаблона, убрал публикацию management-UI наружу. Этих правок нет нигде в git.- Postgres и Redis — Dokploy-managed, compose-файла нет в принципе.
middlewares.ymlTraefik с редиректом www → апекс лежит только на сервере (сам конфиг записан вdocs/infrastructure.md, но не как исполняемый файл).- Значения env всех сервисов — только в БД Dokploy.
Учитывая, что VPS уже терялся один раз в июле и всё пришлось разворачивать заново, это реальный риск, а не теоретический. Правильное решение — перевести инфраструктурные сервисы на git-источник и положить конфиги в crewsforge-observability (или отдельную infra-репу). В организации уже есть форк Infisical — похоже, кто-то думал в сторону управления секретами; стоит решить, используем его или нет.
Осознанно откладываем, чтобы не смешивать с задачей обсервабилити.
Unchanged
Заголовок раздела «Unchanged»- Приложения на слое 1 не трогаем вообще.
- Ни один существующий сервис не переконфигурируется и не перезапускается.
- Схема БД, домены, деплой бэкенда — без изменений.