# Мониторинг (Prometheus + Grafana, профиль compose `monitoring`)

Независимый compose-профиль — можно поднимать вместе
с любым пресетом инсталлятора (1–5) или отдельно.

## 1. Компоненты

| Сервис | Образ | Порт (хост) | Назначение |
|---|---|---|---|
| `prometheus` | `prom/prometheus:v3.13.1` | `9090` | сбор и хранение метрик, оценка правил алертинга |
| `postgres-exporter` | `quay.io/prometheuscommunity/postgres-exporter:v0.20.1` | — (внутренний) | метрики PostgreSQL |
| `redis-exporter` | `oliver006/redis_exporter:v1.87.0-alpine` | — (внутренний) | метрики Redis |
| `node-exporter` | `prom/node-exporter:v1.8.2` | — (внутренний) | метрики хоста: CPU, память, диск, сеть, load average |
| `docker-socket-proxy` | `tecnativa/docker-socket-proxy:v0.5.0` | — (внутренний) | read-only прокси к Docker Engine API для `container-exporter` |
| `container-exporter` | сборка из `deploy/monitoring/container-exporter/` | — (внутренний) | метрики CPU/память/сеть в разбивке по контейнерам |
| `grafana` | `grafana/grafana:13.1.0` | `3001` (внутри контейнера `3000`) | дашборды «Пайплайны пост-обработки» и «Хост и контейнеры» |

`node-exporter`/`docker-socket-proxy`/`container-exporter` не публикуют
порт на хост вообще (не только 127.0.0.1) — Prometheus и сам экспортер
ходят друг к другу по имени сервиса во внутренней сети compose,
публикация на хост для этого не нужна.

### 1.1. Метрики по контейнерам — свой экспортер вместо cAdvisor

**cAdvisor не используется**: Docker Engine на сервере `1gb` использует
containerd-снапшоттер (`docker info` → `driver-type:
io.containerd.snapshotter.v1`), а не классический overlay2-драйвер, и
cAdvisor (проверено на v0.49.2 и v0.52.1, с `--docker_only` и без, через
`--containerd`/`--containerd-namespace=moby` и без) не может определить
read-write layer контейнеров — падает с «failed to identify the
read-write layer ID», метрики есть только по корневому cgroup. Открытая
проблема совместимости cAdvisor с containerd image store, флагами не
чинится — см. `.forcc/JOURNAL.md`.

Вместо этого — свой минимальный экспортер (`container-exporter`,
`deploy/monitoring/container-exporter/exporter.py`, Python/aiohttp/
`prometheus_client`) поверх Docker Engine API: `GET /containers/json` +
`GET /containers/{id}/stats?stream=false`. Эти эндпойнты не зависят от
storage-driver'а — работают и с containerd-снапшоттером, и с overlay2.
Опрос контейнеров идёт параллельно в фоновой задаче (свой интервал,
не завязан на scrape Prometheus) — `stats?stream=false` не мгновенен
(Docker считает CPU-дельту по двум внутренним замерам с разницей ~1с),
линейный опрос дюжины контейнеров легко вышел бы за scrape-интервал.
Метрики (лейбл `name` — имя контейнера):

- `vidconf_container_cpu_percent` — % от одного ядра, как `docker stats`.
- `vidconf_container_memory_usage_bytes` / `_limit_bytes` — working set
  (без переиспользуемого page cache) и лимит памяти.
- `vidconf_container_network_rx_bytes` / `_tx_bytes` — суммарно по
  интерфейсам, счётчик со старта контейнера.

**Безопасность: доступ к `/var/run/docker.sock` равносилен root на
хосте.** Флаг `:ro` при монтировании сокета не спасает — он ограничивает
права на файл-ноду, а не на протокол: через сокет можно поднять
привилегированный контейнер и получить хост. Поэтому сокет НЕ
монтируется напрямую в `container-exporter` — между ними стоит
`docker-socket-proxy` (`tecnativa/docker-socket-proxy`), которому
разрешены только `GET`-запросы по контейнерам (`CONTAINERS=1`, `POST=0`
— дефолт образа, выписан явно). Даже полная компрометация
`container-exporter` (например, через уязвимость в его зависимостях) не
даёт управлять Docker — прокси физически не пропустит `POST`. Ни один
из двух сервисов не публикует портов наружу.

Если на другом сервере используется классический overlay2 и хочется
именно cAdvisor — можно добавить его по тому же образцу, что и
`node-exporter` (свой job в `prometheus.yml`, без публикации портов),
но тогда придётся решать ту же проблему монтирования сокета отдельно
(cAdvisor тоже требует доступ к Docker API).

Файлы: `deploy/monitoring/prometheus.yml`, `deploy/monitoring/alerts.yml`,
`deploy/monitoring/grafana/provisioning/` (datasource + провайдер
дашбордов), `deploy/monitoring/grafana/dashboards/pipelines.json`,
`deploy/monitoring/grafana/dashboards/host.json`,
`deploy/monitoring/container-exporter/` (код экспортера).

## 2. Запуск

```bash
docker compose -f deploy/docker-compose.yml --env-file .env --profile monitoring up -d
```

- Prometheus: http://localhost:9090
- Grafana: http://localhost:3001 (логин/пароль — `.env`,
  `GRAFANA_ADMIN_USER`/`GRAFANA_ADMIN_PASSWORD`; `install.sh` генерирует
  пароль при первой установке)

⚠️ На боевом сервере оба порта публикуются ТОЛЬКО на `127.0.0.1`
(`deploy/docker-compose.yml`) — открытые наружу admin-панели без
дополнительной аутентификации ранее уже приводили к компрометации сервисов
на этом проекте (см. `.forcc/deploy/SESSION2-FINDINGS.md`). С другой машины
используйте ssh-туннель:
```bash
ssh -L 9090:127.0.0.1:9090 -L 3001:127.0.0.1:3001 <user>@<host>
```
и открывайте `http://localhost:9090` / `http://localhost:3001` у себя.

Дашборды «Пайплайны пост-обработки» и «Хост и контейнеры» (папка VidConf
в Grafana) появляются сразу — источник данных и дашборды провижинятся из
файлов, без ручной настройки.

## 3. Метрики backend

`GET /metrics` (`backend/api/metrics.py`, без авторизации внутри
приложения — снаружи периметра закрыт явным `return 403` в
`deploy/nginx/nginx.conf`; Prometheus ходит в backend напрямую по docker-сети,
`backend:8000/metrics`, минуя nginx):

- `vidconf_http_request_duration_seconds` (histogram, `method`/`path`/`status`) —
  латентность HTTP по шаблону маршрута.
- `vidconf_pipeline_sessions` (gauge, `status`) — число сеансов конференций в
  каждом статусе `pipeline_status`
  (recording→transcribing→summarizing→notified|failed).
- `vidconf_celery_queue_depth` (gauge, `queue`) — глубина очередей Celery
  (`transcription`/`summarize`/`notify`/`celery`, redis `LLEN`), карта
  очередей — `docs/deploy/scaling.md`.
- `vidconf_host_info` (gauge, всегда `1`, лейблы `cpus`/`ram_mb`/`gpu_name`/
  `vram_mb`) — info-метрика обнаруженного `install.sh` железа (`HW_*` в
  `.env`, ADR-004). Источник плашки с характеристиками сервера в дашборде
  «Хост и контейнеры» — живые CPU/RAM/диск там же берутся из
  node-exporter, а GPU node-exporter не знает вообще, поэтому только эта
  метрика. `«—»` в лейбле — `install.sh` не запускался (dev) либо
  видеокарта не обнаружена.

Job `llm` в `prometheus.yml` скрейпит `llm:8080/metrics`
(`LLAMA_ARG_ENDPOINT_METRICS=1`) — этот адрес резолвится ЛИБО сервисом
`llm` (CPU, профиль `llm`), ЛИБО `llm-gpu` (у него есть сетевой алиас `llm`,
см. `deploy/docker-compose.yml`) — профили `llm`/`llm-gpu` взаимоисключающи
по пресету, поэтому один job без дублирования.

**По умолчанию job `llm` закомментирован** в `prometheus.yml` (вместе с
алертом `LlmDown` в `alerts.yml`) — целевой набор профилей compose
(`media,monitoring`, см. `.env.example`) не включает `llm`/`llm-gpu`, и
таргет `llm:8080` не резолвится вовсе. Раскомментируйте job и алерт вместе,
если инсталляция запущена с профилем `llm`/`llm-gpu` (пресеты 3–5
`install.sh`) — иначе панель «LLM-сервер доступен» дашборда «Пайплайны
пост-обработки» будет показывать «No data».

## 4. Алерты (`deploy/monitoring/alerts.yml`)

| Алерт | Условие | severity |
|---|---|---|
| `PipelineFailed` | рост числа сеансов в статусе `failed` за 15 минут | critical |
| `QueueGrowing` | глубина очереди растёт 15 минут подряд и превышает 10 задач | warning |
| `LlmDown` | `up{job="llm"} == 0` дольше 2 минут | critical (закомментирован по умолчанию) |
| `HostMemoryLow` | свободно <10% RAM (≈800 МБ из 8 ГБ) дольше 10 минут | warning |
| `HostDiskLow` | свободно <10% диска (≈5 ГБ из 50 ГБ) дольше 15 минут | warning |
| `HostCpuHigh` | загрузка CPU >90% дольше 15 минут подряд | warning |

Пороги трёх алертов по железу подобраны под конкретный сервер `1gb`
(8 ГБ RAM, 4 CPU, 50 ГБ диска) — при смене сервера пересчитать
(`deploy/monitoring/alerts.yml`, группа `vidconf-host`).

`LlmDown` актуален только на инсталляциях с профилем `llm`/`llm-gpu`
(пресеты 3–5) — по умолчанию (профили `media,monitoring`, без AI) правило
закомментировано в `alerts.yml` вместе с job `llm` в `prometheus.yml`.
Раскомментируйте оба, если поднимаете профиль `llm`/`llm-gpu`.

Проверка (после раскомментирования job/алерта, на инсталляции с профилем
`llm`/`llm-gpu`) — искусственно завалить пайплайн и убедиться, что алерт
срабатывает:

```bash
# Стек с профилями media, transcribe, llm, monitoring уже поднят,
# идёт активная суммаризация (сеанс в статусе summarizing).
docker compose -f deploy/docker-compose.yml --env-file .env stop llm
# Подождать > 2 минут → Prometheus (http://localhost:9090/alerts)
# должен показать LlmDown в состоянии firing, следом — QueueGrowing
# (очередь summarize перестаёт разбираться) и, если сеанс не восстановится
# за 15 минут (recover_stuck_summaries переставит задачу, workers/celery_app.py),
# PipelineFailed.
docker compose -f deploy/docker-compose.yml --env-file .env start llm
```

## 5. Хранение

`prometheus_data`/`grafana_data` — именованные тома, переживают
пересоздание контейнеров. Ретеншен Prometheus — дефолт образа (15 дней);
для прод-инсталляций с длинным горизонтом донастраивается флагом
`--storage.tsdb.retention.time` (не задан в `deploy/docker-compose.yml` —
осознанный dev/small-prod дефолт, донастраивается отдельно при необходимости).
