# Мониторинг и автоматизация серверов: infra

*Для разработчиков. Состояние на 28.09.2026. Подробности, переменные ролей и команды — README репозитория `infra` и `files/heartbeat/README.md`. Как считается, кто и чем пользуется, — статья «Телеметрия использования: usage\_telemetry».*

## Коротко

Репозиторий `infra` — единственный источник правды о серверах: инвентарь Ansible, роли базовой настройки, стек мониторинга, правила алертов, дашборды Grafana, статус-страница и служба окон обслуживания. Главное правило проекта: **изменение, сделанное мимо репозитория, не существует**. Накликанное на сервере или в веб-интерфейсе затрётся следующим прогоном роли.

<table id="bkmrk-%D0%A7%D1%82%D0%BE%D0%93%D0%B4%D0%B5-%D0%B2-%D1%80%D0%B5%D0%BF%D0%BE%D0%B7%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8"><thead><tr><th>Что</th><th>Где в репозитории</th><th>Снаружи</th></tr></thead><tbody><tr><td>Prometheus, Alertmanager, blackbox\_exporter, Pushgateway, Grafana</td><td>`roles/monitoring_server`, правила — `prometheus/rules/`</td><td>Grafana — grafana.donula26.ru (вход по логину); остальное наружу не смотрит</td></tr><tr><td>Агент метрик на каждом сервере</td><td>`roles/monitoring_agent`</td><td>—</td></tr><tr><td>Метрики PostgreSQL</td><td>`roles/postgres_exporter`</td><td>—</td></tr><tr><td>Статус-страница</td><td>`roles/status_page`</td><td>status.donula26.ru</td></tr><tr><td>Окна обслуживания (админка и MCP)</td><td>`roles/status_admin`</td><td>status.donula26.ru/admin/, status.donula26.ru/mcp</td></tr><tr><td>Heartbeat периодических задач</td><td>`files/heartbeat/heartbeat.py`</td><td>push.donula26.ru</td></tr><tr><td>GlitchTip (ошибки приложений)</td><td>роли нет; проба и реестр проектов — `docs/projects.md`</td><td>monitoring.donula26.ru</td></tr><tr><td>Бэкапы баз, фаервол, базовая настройка, аудит</td><td>`roles/db_backup`, `roles/firewall`, `roles/base`, `playbooks/audit.yml`</td><td>—</td></tr><tr><td>Коллектор телеметрии использования</td><td>`roles/telemetry_collector`</td><td>—</td></tr></tbody></table>

## Метрики и алерты

- **Prometheus** опрашивает цели раз в 30 секунд и хранит данные 90 дней. Список серверов для опроса собирается из инвентаря (`inventory/hosts.yml`), поэтому новый сервер появляется в мониторинге только после прогона `monitoring_server`.
- **Сервер изнутри** — node\_exporter с TLS и basic auth, плюс скрипты для textfile collector: состояние контейнеров (статус, число перезапусков, healthcheck, время старта), метрики хоста, возраст свежих бэкапов. cAdvisor не используется, поэтому CPU и памяти по отдельным контейнерам в метриках нет.
- **Снаружи** — blackbox\_exporter проверяет публичные адреса из `monitoring_probe_targets` (`inventory/group_vars/all/main.yml`) и срок действия TLS-сертификатов. По умолчанию модуль `http_2xx`, для остальных случаев модуль задаётся явно. Пробы бьют в `/health`, где он есть.
- **Базы** — postgres\_exporter у `amo_db` и у баз мобильного оператора: соединения, архивация WAL, отставание синхронизации.
- **Правила** — `prometheus/rules/*.yml`: `hosts`, `containers`, `blackbox`, `backups`, `amo_db`, `umobile`, `contur_transcribe`, `meta` (сам мониторинг и heartbeat). В Grafana алерты не заводятся.

Alertmanager шлёт уведомления в Telegram через прокси (напрямую `api.telegram.org` недоступен), в два чата:

<table id="bkmrk-%D0%90%D0%BB%D0%B5%D1%80%D1%82%D0%9A%D1%83%D0%B4%D0%B0-%D0%B8-%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0-se"><thead><tr><th>Алерт</th><th>Куда и когда</th></tr></thead><tbody><tr><td>`severity: critical`</td><td>В критический чат сразу, с 08:00 до 23:00 по Москве. Если сработал ночью, придёт в 08:00, при условии что проблема ещё держится. Повтор раз в час.</td></tr><tr><td>`severity: warning` + `delivery: prompt`</td><td>В тот же чат, днём сразу, ночью придерживается до утра. Повтор раз в 6 часов. Метка для того, что не срочно, но нужно узнать в тот же день.</td></tr><tr><td>Остальные `warning` и `info`</td><td>Утренняя сводка: одно сообщение в окне 08:00–08:20.</td></tr></tbody></table>

Подавления: при `HostDown` молчит всё, что есть на этом хосте. При `DockerDown` и `DockerMetricsStale` молчат контейнерные алерты. При `AmoDbPostgresDown` молчат производные проверки `amo_db`. При `DiskUsageCritical` молчат предупреждения о том же диске. Истёкшие тишины Alertmanager хранит 30 дней: это журнал объявленных работ.

Дашборды Grafana лежат в папке «Инфраструктура»: «Инфраструктура: серверы», «Инфраструктура: контейнеры», «Доступность снаружи», «Периодические задачи», «amo\_db», «Использование приложений». Их JSON генерируется скриптами `generate.py` и `generate_usage.py` в `roles/monitoring_server/files/dashboards/`.

Правка дашборда в браузере не сохранится: включено `allowUiUpdates: false`. Дашборды меняют в генераторе, потом прогоняют роль.

## GlitchTip

GlitchTip на monitoring.donula26.ru собирает исключения приложений через `SENTRY_DSN`. Своя запись в нём есть у каждого нашего проекта. Одной переменной `SENTRY_DSN` мало: в коде нужны зависимость `sentry-sdk` и вызов инициализации. Проще всего взять `init_sentry()` из пакета `usage_telemetry`: `install_telemetry()` вызывает её сам.

- Об ошибке в чат приходит уведомление, когда одна и та же ошибка случилась пять раз за десять минут. Разовые ошибки видны только в интерфейсе GlitchTip.
- Uptime-мониторы в GlitchTip есть, но уведомления о доступности от них выключены с 18.09.2026. О том, что сервис недоступен, сообщает только Alertmanager, чтобы одно событие не приходило двумя сообщениями.
- Роли Ansible для GlitchTip нет: проекты и правила уведомлений заводятся в его интерфейсе. Сам GlitchTip проверяется внешней пробой. Эндпоинта здоровья у него нет, поэтому проба смотрит корень сайта.

## Статус-страница

status.donula26.ru открыта без авторизации и сделана для сотрудников. Это не Uptime Kuma и не отдельный монитор: страницу генерирует скрипт `roles/status_page/files/generate.py`. Он запускается по таймеру раз в минуту, берёт из Prometheus ту же метрику `probe_success`, на которой построены алерты, и пишет статический HTML. Поэтому страница не может показывать не то, что пишут в чат алерты.

Список сервисов задаётся в `status_page_services`. Поля: `probe` — адрес пробы, как в `monitoring_probe_targets`; `title` — название, понятное сотруднику; `visible` — показывать ли на странице (инфраструктурные сервисы проверяются, но скрыты); `project` и `job` на странице не видны, по ним окно обслуживания глушит алерты. Внутренние имена, адреса и версии на страницу не попадают.

<table id="bkmrk-%D0%A1%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0-%D0%A0%D0%B0%D0%B1%D0%BE%D1%82"><thead><tr><th>Состояние</th><th>Когда</th></tr></thead><tbody><tr><td>Работает</td><td>проба успешна, и за сутки вне окон обслуживания сбоев не было</td></tr><tr><td>С перебоями</td><td>сейчас отвечает, но за сутки были сбои; или падает, но меньше двух минут подряд</td></tr><tr><td>Не работает</td><td>все пробы за последние 2 минуты неуспешны (`status_page_down_window`)</td></tr><tr><td>На обслуживании</td><td>сейчас идёт объявленное окно; перекрывает все остальные состояния</td></tr><tr><td>Нет данных</td><td>по пробе нет метрик, например недоступен Prometheus</td></tr></tbody></table>

У каждого сервиса есть полоса за сутки из 48 получасовых сегментов и процент доступности за сутки. Минуты внутри окон обслуживания в процент не входят.

## Окна обслуживания

Любой деплой, обновление или фикс сервиса, у которого есть внешняя проба, идёт в окне обслуживания. Одна запись об окне делает три вещи:

- на статус-странице сервис становится синим, с бейджем «На обслуживании», а причина работ видна в подсказке к сегменту;
- его сбои во время окна не портят процент за сутки;
- в Alertmanager заводятся тишины на все имена сервиса: `instance` (проба), `project` (контейнеры) и `job` (отметки heartbeat). Алерты по сервису в чат не приходят.

**Порядок работ:** открыть окно, выкатить и проверить, **сразу** закрыть окно. Пока окно открыто, мониторинг сервиса слепой, поэтому лучше открыть короткое окно и продлить его, чем забыть длинное. Причину пишут человеческим языком, её видит любой сотрудник: «Обновление дашборда», а не «деплой v2.3.1».

### Через админку

1. Открыть status.donula26.ru/admin/. Вход по именному логину: имя вошедшего записывается автором окна.
2. Выбрать «Сервис», заполнить «Причина», проверить «Начало» и «Конец». По умолчанию окно идёт от текущего момента на 30 минут.
3. Нажать «Объявить работы».
4. После работ нажать «Закончить сейчас» в строке окна. Конец окна переносится на текущий момент, тишины снимаются, запись остаётся в истории.

Кнопка «Убрать» удаляет запись целиком, вместе с синим сегментом. Она нужна только для окон, заведённых по ошибке. В таблице видны окна за последние трое суток и все предстоящие.

### Через MCP

Та же служба отвечает по MCP на `https://status.donula26.ru/mcp`. Имя сервера — `status-maintenance`, авторизация — заголовок `Authorization: Bearer <токен>`. Токен у каждого разработчика свой. Пример подключения в Claude Code:

```
claude mcp add --transport http status-maintenance https://status.donula26.ru/mcp \
  --header "Authorization: Bearer $STATUS_MCP_TOKEN"
```

<table id="bkmrk-%D0%98%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%9F%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B%D0%A7"><thead><tr><th>Инструмент</th><th>Параметры</th><th>Что делает</th></tr></thead><tbody><tr><td>`list_maintenance`</td><td>`hours` (по умолчанию 72)</td><td>идущие, предстоящие и недавние окна с их `id`</td></tr><tr><td>`open_maintenance`</td><td>`service`, `minutes` (не больше 120), `reason`, необязательный `start` в ISO по Москве</td><td>открывает окно; по умолчанию оно начинается сейчас</td></tr><tr><td>`close_maintenance`</td><td>`id`</td><td>закрывает окно сейчас и снимает тишины</td></tr></tbody></table>

- В `service` передают название со статус-страницы или адрес пробы. Если название подходит нескольким сервисам (например, «МПП2» — и дашборду, и виджету), служба возвращает ошибку со списком подходящих, а не выбирает первый.
- Автором окна записывается «помощник &lt;владелец токена&gt;».
- Окно длиннее двух часов через MCP не открыть: такие работы объявляются вручную в админке.

Доступы задаются в Ansible Vault: `status_admin_web_users` (логины админки) и `status_admin_mcp_tokens` (токены MCP), затем прогоняется `playbooks/status_admin.yml`. Чтобы отозвать доступ у одного человека, его строку удаляют и снова прогоняют роль. Остальные доступы продолжают работать.

## Heartbeat

Проблема: Celery Beat или APScheduler умирают, не роняя контейнер. Процесс жив, healthcheck зелёный, проба отвечает 200, а задача не запускалась третьи сутки. Такое видит только сама задача, поэтому после каждого успешного прогона она отправляет отметку в Pushgateway.

- **Подключение.** Скопировать `files/heartbeat/heartbeat.py` в проект (зависимостей нет) и вызвать одну функцию: `install_celery(celery_app, job="…")`, `install_apscheduler(scheduler, job="…")` или `ping(job=…, task=…, max_age_seconds=…)` для разовой отметки. Сейчас так подключены шесть проектов.
- **Переменные:** `HEARTBEAT_URL` (https://push.donula26.ru; пустое значение выключает отправку — так работает локальная разработка), `HEARTBEAT_USER`, `HEARTBEAT_PASSWORD` (лежит в Vault), `HEARTBEAT_INSTANCE` (имя источника; для контейнеров его задают явно).
- **Порог** задача объявляет сама метрикой `heartbeat_max_age_seconds`. По умолчанию это полтора интервала расписания плюс пять минут, отдельные задачи переопределяют его через `max_age_overrides`. Правило `HeartbeatMissing` (critical) срабатывает, если отметки нет дольше порога.
- Отметку отправляет воркер после успешного выполнения. Упавшая задача отметку не обновляет, и молчание становится сигналом. Ошибки отправки пишутся в лог и не роняют задачу.

## Ansible: как выкатывать

- Запуск — с ноутбука разработчика по ssh, Ansible стоит в `.venv` внутри проекта (`pip install -r requirements.txt`). Секреты лежат в `inventory/group_vars/all/vault.yml` под Ansible Vault. Пароль от хранилища — в файле `.vault_pass`, в git он не попадает, копия есть в VaultWarden.
- Порядок любого прогона: `--check --diff`, затем `--limit` на одном хосте, затем на всех.
- Новый сервер: завести в `inventory/hosts.yml`, затем прогнать `base` → `firewall` → `monitoring_agent` → `monitoring_server`. Если фаервол настроить после агента, агент не создаст правило для своего порта.
- Службу окон обслуживания ставят до nginx: сначала `playbooks/status_admin.yml`, потом `playbooks/web_proxy.yml`.
- `playbooks/audit.yml` только читает: что запущено на серверах и что требует внимания. Результат попадает в `reports/`, этот каталог в git не коммитится.
- Работы на сервисе с пробой проводятся в окне обслуживания, см. выше.

## На что наступают

- **У сервиса три имени, тишина нужна на каждое.** Если у сервиса в `status_page_services` не заполнены `project` и `job`, окно заглушит только пробу, а контейнеры и heartbeat продолжат писать в чат. Лишнее имя тоже вредно: чужой `project` заглушит соседний сервис. Заполняют только то, что сверено с метками в Prometheus.
- **Сервиса нет в пробах — окно открыть не на что.** Сначала сервис заводят в `monitoring_probe_targets` и `status_page_services`, только потом объявляют для него работы.
- **Окно, заведённое задним числом,** красит историю, но тишины не создаёт: Alertmanager не принимает тишины в прошлом.
- **Обслуживание не прячет соседнюю аварию.** Сегмент на полосе синеет, только если все неудачные минуты получаса попали внутрь окна.
- **Heartbeat после деплоя молчит до первого прогона.** Правило отсчитывает возраст от более позднего из двух событий: последней отметки или последнего старта контейнеров проекта. Это работает, только если `job` отметок совпадает с именем compose-проекта. Проект с другим именем заалертит сразу после деплоя.
- **Переехал проект — поправьте `HEARTBEAT_INSTANCE`** и удалите старую группу из Pushgateway. То же с задачей, убранной из расписания: Pushgateway помнит отметки бесконечно, и старая группа будет держать алерт.
- **Порог heartbeat считается при старте процесса.** Новое расписание подхватится только после перезапуска контейнера.
- **Лимит docker-логов не действует сразу.** Роль `base` пишет `daemon.json`, но нужен полный перезапуск docker (в окне обслуживания) и пересоздание контейнеров.
- **Docker публикует порты в обход ufw.** Роль `firewall` закрывает этот обход. Порт контейнера, который должен смотреть наружу, открывают маршрутным правилом через `firewall_route_rules`. В файлах правил ufw пишите комментарии только латиницей, иначе ufw падает.
- **Ansible ставят только в `.venv`.** Фильтр `password_hash` не работает (`passlib` несовместим с `bcrypt 5`), поэтому bcrypt-хеши хранятся в Vault готовыми.
- **`--check` на чистой машине падает** на первой же задаче `apt`: нет `python3-apt`. Первый прогон на пустом сервере делается сразу боевым.
- **С ноутбука ssh может прийти не на тот сервер:** локальный прокси подменяет адрес. Признаки — «Permission denied» с рабочим ключом и предупреждение о смене ключа хоста. Сервер проверяют по `hostname`, а не по тому, что подключение прошло.
- **Хост вне инвентаря не мониторится.** Инвентарь пополняется по мере того, как доступы переносятся в VaultWarden.