Тема
ТЗ — Мониторинг/health-check (cms/health)
Слой: 🔵 инфра-модуль (обязательный в managed-парке) · Зрелость доноров: ★★ · Донор: er (SEO health), catalog Статус: ТЗ к разработке
Назначение и возможности
Централизованный health-check сайта на базе spatie/laravel-health: агрегирует системные проверки (БД, Redis, очереди, диск, cron) и health-классы, которые объявляют о себе модули через манифест. Используется как гейт процедуры cms:upgrade и источник алертов.
Граница ядро/модуль (core-criteria §10): маршрут /api/v1/system/health и таймауты чеков — контракт ядра (жив и без установленного модуля); этот модуль поставляет содержимое агрегата — системные health-классы, дашборд, историю, алерты и эскалацию в cms/fleet-dashboard. Таймауты — два контура (решение 14.07.2026, противоречия нет): синхронный/api/v1/system/health держит бюджет ядра «≤ 2 с, чек ≤ 500 мс» (core: производительность) — эндпоинт отдаёт закешированные результаты последнего прогона, а чек, не уложившийся в 500 мс на месте, возвращает прошлый статус с пометкой stale; фоновый прогон по расписанию (где живут медленные проверки внешних API) работает с health.check_timeout_seconds (дефолт 10 с) и обновляет кеш результатов.
- Проверки: подключение БД, Redis, длина очередей Horizon, свободное место на диске, факт выполнения cron
- Сбор health-классов из
extra.cms.healthкаждого установленного модуля - Единый дашборд статусов в Filament (зелёный/жёлтый/красный)
- Публичный endpoint для внешнего аптайм-мониторинга под токеном
- Гейт для
cms:upgrade— апгрейд не стартует при красном статусе - Алерты об ухудшении статуса через
cms/notifications-bus - История статусов с таймлайном деградаций
Зависимости и выключение
requires: ядро · suggests: cms/notifications-bus
Модуль обязателен в managed-парке (§3 стандарта, подраздел «Обязательные модули managed-парка», наряду с cms/updates, cms/sentry, cms/backup, cms/attack-monitor): disable недоступен клиентским ролям — доступен только studio-роли, с обязательным подтверждением и записью в аудит; кнопка выключения в админке клиента показывает «модуль обязателен по условиям поддержки». На standalone-инсталляциях выключение доступно клиентским ролям свободнее — паттерн идентичен cms/updates. Правило «выключение = деградация» при этом не отменяется ни в одном из режимов: health-чеки не выполняются, cms:upgrade пропускает гейт с предупреждением в лог — само приложение продолжает работать, деградирует только наблюдаемость. См. также ревизию контрактов 14.07.2026.
Внешних платных API модуль не вызывает — все проверки внутренние (БД, Redis, очередь, диск, cron), критерий матрицы «стоимость внешних API» не применим.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_health_check_results | id, check_name, status, notification_message, meta (json), ended_at | снимки последних результатов проверок |
cms_health_history | id, check_name, status, checked_at | история статусов для таймлайна |
Заметки: status — PHP Enum с касом 'string', не строка; meta — JSON с касом 'array'; индекс по (check_name, checked_at) в cms_health_history под таймлайн-запросы и BRIN по checked_at при росте истории. Записи cms_health_history старше health.history_retention_days удаляются штатной командой ретеншна по расписанию — журнал не подлежит ручной чистке из UI.
ПДн-паспорт: ПДн не храню — обе таблицы содержат только технические метрики проверок (имя чека, статус, meta диагностики вроде длины очереди или процента диска, временные метки); субъект персональных данных в них не идентифицируется — регистрировать обработчик в PrivacyRegistry (cms:privacy:export/forget, п. 15 ревизии ядра) для таблиц этого модуля незачем.
Входные и выходные данные
Входы
| Источник | Данные/поля | Чем валидируется |
|---|---|---|
Filament: «Перезапустить проверки» / POST /api/v1/admin/health/checks/run | опционально check_name | permission health.manage; FormRequest whitelist — check_name должен быть зарегистрированным health-классом, иначе 422 |
GET /api/v1/system/health | заголовок системного токена | токен сверяется с config('health.system_token'), не settings-store |
Манифест модуля extra.cms.health | ФQCN health-класса | при установке модуля — класс обязан реализовывать интерфейс HealthCheck ядра, иначе модуль не проходит self-test enable |
| Health-классы модулей (внутренний вызов джобы) | status, message, meta | контракт интерфейса HealthCheck; таймаут на каждый чек обязателен (см. крайние случаи) |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Filament дашборд | статусы (зелёный/жёлтый/красный), таймлайн истории | Blade через сервис модуля |
/api/v1/system/health | агрегированный статус инсталляции | JSON, конверт data/meta |
/api/v1/admin/health/checks | детализация по каждому чеку | JSON, keyset-пагинация |
cms/updates (гейт cms:upgrade) | итог гейта pass/fail | сервис-вызов (requires, канал 4) |
cms/notifications-bus (suggests) | алерт об ухудшении статуса | сервис-вызов (канал 3, notification-channel) |
cms_health_check_results/cms_health_history | снимки статусов проверок | БД, append-only история |
Настройки (группа health)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
health.enabled | bool | true | нет | Включить проверки (обязателен в managed-парке) |
health.check_interval_minutes | int | 5 | нет | Периодичность прогона проверок |
health.check_timeout_seconds | int | 10 | нет | Таймаут одного health-класса — превышение помечает чек failed/timeout, не роняет прогон остальных |
health.flap_debounce_checks | int | 2 | нет | Число подряд неудачных прогонов до признания статуса красным (гистерезис против флаппинга) |
health.disk_threshold_percent | int | 85 | нет | Порог тревоги по заполнению диска |
health.queue_backlog_threshold | int | 500 | нет | Порог тревоги по длине очереди |
health.public_endpoint_enabled | bool | true | нет | Доступность /api/v1/system/health |
health.upgrade_gate_enabled | bool | true | нет | Блокировать cms:upgrade при красном статусе |
health.history_retention_days | int | 90 | нет | Ретеншн cms_health_history — без него журнал растёт неограниченно |
health.alert_window_minutes | int | 15 | нет | Окно группировки алертов в одно уведомление (защита от алерт-шторма) |
Лимиты и квоты (матрица v2.2): history_retention_days, queue_backlog_threshold и disk_threshold_percent — явные настройки-лимиты с дефолтами, требуемые критерием «лимиты и квоты» матрицы; достижение queue_backlog_threshold/disk_threshold_percent красит соответствующий системный чек и уходит алертом, не молчаливо игнорируется, а history_retention_days ограничивает рост cms_health_history штатной командой ретеншна (см. «Модель данных»).
Kill-switch (матрица v2.2): health.upgrade_gate_enabled — реализация критерия «kill-switch»: аварийно отключает влияние health на cms:upgrade (гейт перестаёт блокировать апгрейд при красном статусе) без выключения самого модуля health — проверки и дашборд продолжают работать, отключается только их эффект на апгрейд.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/system/health | токен (system) | Сводный статус для внешнего аптайм-монитора |
| GET | /api/v1/admin/health/checks | admin (health.view) | Детализация всех проверок |
| POST | /api/v1/admin/health/checks/run | admin (health.manage) | Принудительный перезапуск проверок |
Детализация проверок отдаётся keyset-пагинацией (не OFFSET) при росте числа health-классов.
Компоненты
Виджеты: Filament-виджет «Статус системы». Filament: страница со списком проверок и историей. Команды: cms:health:check --json (прогон всех проверок вне расписания, вывод — статус каждого чека, duration_ms, причина при failed), cms:health:gate --json (используется cms:upgrade, возвращает булево pass/fail и список красных чеков-блокеров).
Демо-сидеры: не применимо — модуль не регистрирует блоков/виджетов в BlockRegistry (галерея блоков и playground его не касаются), демо-данных для затравки не требуется.
Системные проверки по умолчанию
| Чек | Что проверяет | Дефолтный порог |
|---|---|---|
database | подключение к БД доступно, простой запрос выполняется | таймаут health.check_timeout_seconds (10 сек) |
redis | подключение к Redis доступно | таймаут health.check_timeout_seconds (10 сек) |
queue-backlog | длина очереди Horizon не превышает порог | health.queue_backlog_threshold (500) |
disk-space | доля занятого места на диске | health.disk_threshold_percent (85%) |
scheduler-heartbeat | факт выполнения запланированных задач cron за ожидаемый интервал | привязан к health.check_interval_minutes — отсутствие heartbeat дольше интервала = failed |
Каждый модуль сверх этого списка добавляет свои health-классы через extra.cms.health (см. «Входные и выходные данные»).
Граница с ядром по диску (ревизия №2, п. 2): базовый чек free-space с порогами
system.disk_warn_percent/disk_critical_percentвстроен в health-агрегат ядра и работает без этого модуля.disk-spaceмодуля — расширение (свой порог, история, алерты/эскалация); расширенные чеки (inode, прогноз роста) — тоже зона модуля. Пороги ядра и модуля независимы: ядро сигналит «до падения БД», модуль — «по политике эксплуатации».
События и обмен
| Событие | Когда | Payload |
|---|---|---|
HealthCheckFailed | проверка перешла в статус failed/warning (после debounce) | check_name, status, message, duration_ms |
HealthCheckRecovered | проверка вернулась в статус ok | check_name, previous_status |
Оба события издаются после прохождения flap_debounce_checks — промежуточные колебания статуса в рамках гистерезиса не порождают события и не долетают до подписчиков (иначе cms/notifications-bus штормит на каждое дрожание).
Слушает: манифесты модулей (extra.cms.health) для регистрации дополнительных проверок. Provides: не декларирует контрактов ядра. Алерты об ухудшении публикуются в cms/notifications-bus как потребитель его сервис-вызова (по suggests).
Таблица взаимодействий
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
Все модули (extra.cms.health) | манифест-регистрация (декларативно, вне рантайм-каналов) | in | Дополнительный health-класс регистрируется при установке модуля |
cms/updates | requires → сервис-вызов (канал 4), cms:health:gate | out | Итог гейта блокирует/пропускает cms:upgrade |
cms/notifications-bus | suggests → сервис-вызов (канал 3, notification-channel) | out | Алерт об ухудшении статуса, сгруппированный по alert_window_minutes |
cms/fleet-dashboard | косвенно, через телеметрию cms/updates | out | health_status попадает в суточную телеметрию как часть чужого payload, не прямая связь |
| БД / Redis / очередь Horizon / диск | внутренний вызов драйвера (не модуль) | in | Источник данных системных проверок |
Эскалация в fleet-dashboard (точный путь): агрегированный health_status этого модуля не передаётся напрямую — он попадает в суточный POST-запрос телеметрии, который формирует cms/updates (см. его раздел «Настройки», updates.telemetry_enabled) и отправляет в cms/fleet-dashboard. Там красный/жёлтый статус отображается в карте парка сайта и, что важнее, участвует в гейте канареечной волны: cms/fleet-dashboard останавливает волну для конкретного сайта при провале его health-check (см. fleet-dashboard.md) — красный статус этого модуля может заблокировать раскатку обновления именно на этот сайт, не влияя на остальные сайты волны. Прямого канала «health → fleet-dashboard» нет: модуль health не знает о существовании fleet-dashboard, весь путь идёт через requires-канал с cms/updates.
Фоновая работа
Джобы прогона проверок в именованной очереди health, расписание — health.check_interval_minutes через ScheduleRegistrar ядра, без прямого Schedule:: в boot. Внешние HTTP-проверки (если health-класс модуля их требует) выполняются только из очереди.
Метрики и алерты (§15 стандарта): доля красных чеков за окно (red_checks_ratio), среднее время прогона одного чека (check_duration_ms, p50/p95), флап-рейт (число переключений статуса за сутки, сигнал нестабильного чека ещё до срабатывания алерта). Алерт HealthCheckFailed — единственный инцидент-триггер модуля, издаётся только после flap_debounce_checks, дальше группируется alert_window_minutes перед отправкой в cms/notifications-bus.
Мини-ранбук (симптом → что проверить → команда):
| Симптом | Что проверить | Команда |
|---|---|---|
| Дашборд красный, алерта нет | не истёк ли flap_debounce_checks (флаппинг ещё копит подряд-неудачи) | cms:health:check --json — смотреть duration_ms и текущий счётчик неудач по чеку |
| Прогон чека висит дольше обычного | health-класс модуля без таймаута на внешнем вызове (см. «чек сам роняет систему» ниже) | cms:health:check --json, найти чек с status=timeout, проверить check_timeout_seconds |
Шквал уведомлений в cms/notifications-bus | не сработала группировка alert_window_minutes (например, окно выставлено в 0) | сверить health.alert_window_minutes в settings-store группы health |
| Дублирующиеся снимки статуса за один момент | ручной перезапуск совпал с расписанием, лок джобы не сработал | лог очереди health на предмет двух активных джоб одновременно; cms:health:check --json для форс-прогона после починки |
| Алерты не долетают вообще | cms/notifications-bus выключен или не установлен (suggests, не requires) | cms:doctor --json — проверить статус suggests-зависимости; статус при этом остаётся виден в Filament |
Бэкап/рестор: cms_health_check_results/cms_health_history — журнальные, не критичны для бэкапа: после рестора инсталляции они пересоздаются следующими прогонами по расписанию (health.check_interval_minutes), пустая история сразу после рестора — ожидаемое состояние, не повод считать рестор неполным. Общий бэкап всех cms_* (включая эти таблицы) и обязательный cms:postupgrade после рестора — по ранбуку ядра.
Производительность и кеш
Объёмы: check_interval_minutes=5 → 288 прогонов/сутки на каждый health-класс; на managed-сайте с десятками модулей это системные ~5 проверок + по одной на модуль — сотни-тысячи строк в cms_health_history в сутки. Без history_retention_days журнал растёт неограниченно — BRIN по checked_at снижает стоимость записи, но не заменяет периодическую очистку по расписанию. Горячий путь — публичный /api/v1/system/health (дёргается внешними аптайм-мониторами раз в 1-5 минут): агрегированный статус читается из последнего снимка cms_health_check_results, не пересчитывается на лету; допустим короткий кеш агрегата (TTL ≤ check_interval_minutes) на этом эндпоинте, чтобы частые внешние опросы не били в БД при каждом запросе. Индекс (check_name, checked_at) в cms_health_history под таймлайн; дополнительно нужен индекс/частичный индекс по status для быстрой агрегации «все ли зелёные» без full scan. Собственных тегов page-cache не объявляет — статусы читаются из своих таблиц, не через кеш ядра.
Безопасность
Границы входа: публичный endpoint /api/v1/system/health защищён токеном (system), не открыт анонимно; принудительный перезапуск проверок — только под health.manage. Входных форм с пользовательскими данными нет, санитизация не требуется. Специфичные векторы: подбор/утечка system-токена даёт наблюдателю карту внутреннего состояния (очереди, диск, БД) — токен ротируется как секрет, не хранится в settings-store; health-класс стороннего модуля не должен получать доступ к чужим таблицам для диагностики — только через сервисы владельца (как любой модуль, §5 стандарта); чек с неограниченным временем выполнения — сам по себе вектор DoS на воркер очереди health, поэтому таймаут обязателен на уровне контракта HealthCheck, а не дисциплины отдельного модуля.
Права: health.view, health.manage.
Матрица ролей:
| Роль | health.view (просмотр статусов/истории) | health.manage (принудительный перезапуск проверок, check_name) |
|---|---|---|
| Администратор сайта | ✅ | ✅ |
| Менеджер | ✅ | ❌ |
| Редактор контента | ❌ | ❌ (модуль внутренний, редактору не нужен) |
| Studio (студия) | ✅ | ✅ + эксклюзивное право disable модуля в managed-парке (см. «Зависимости и выключение») |
UX-требования
Админ: пустое состояние сразу после установки — «проверок ещё не было, первая через check_interval_minutes минут» вместо пустого графика; кнопка «перезапустить все проверки» — одно действие, не по одной на чек; предупреждение о времени ожидания, если среди чеков есть долгие внешние проверки; ошибки — на человеческом языке («Redis недоступен: соединение отклонено», не трассировка исключения); статус конкретного чека кликабелен и показывает историю именно по нему (не общий лог).
Посетитель: не применимо — модуль внутренний; единственная публичная поверхность (/api/v1/system/health) обязана отвечать 200 с полем status даже при частичной деградации (не 5xx), чтобы внешний мониторинг отличал «сайт лежит» от «часть проверок жёлтая».
Крайние случаи и типовые баги
- флаппинг чека (быстрое переключение ok/failed) →
flap_debounce_checksподряд неудачных прогонов до признания статуса красным; единичный сбой не поднимает алерт и не красит дашборд; - чек сам роняет систему (health-класс модуля делает тяжёлый запрос без таймаута) → таймаут (
check_timeout_seconds) обязателен на уровне контрактаHealthCheck: превышение помечает именно этот чекfailed/timeout, не блокирует остальные и не валит джобу прогона целиком; - алерт-шторм (отказ Redis красит разом все зависящие чеки) → алерты группируются в одно уведомление за
alert_window_minutes, а не рассылаются по одному на каждый упавший чек; - параллельный прогон проверок (ручной перезапуск совпал с расписанием) → идемпотентность через лок на уровне джобы: второй прогон либо ждёт первого, либо скипается с логом, не пишет дублирующие снимки статуса;
- выключение модуля посреди прогона → уже запущенная джоба дорабатывает штатно, новые проверки не планируются;
cms:upgradeпри выключенномhealthпропускает гейт с предупреждением (описано выше), не падает; - отсутствие suggests-модуля
cms/notifications-bus→ алерты не отправляются, статус виден только в Filament-дашборде и логе — health-чеки продолжают работать; - сбой/таймаут health-класса стороннего модуля (внешний API, например интеграция) → статус
unknown/warning, отдельный от явногоfailed: «не удалось проверить» — не то же самое, что «сервис сломан»; - пустые данные — свежая установка без истории → дашборд показывает «нет данных, первая проверка через X минут», не пустой график и не ошибку рендера;
- огромный объём истории (годы работы инсталляции) →
history_retention_daysобязателен, штатная команда очистки по расписанию — без него таблица растёт неограниченно даже с BRIN-индексом; - измерения locale/city/site — health-чеки работают на уровне инсталляции целиком, измерения не применимы; на
cms/multisiteс несколькими сайтами на одной БД нужно явно зафиксировать, что статус общий на инсталляцию, не per-сайт — иначе красный чек одного сайта блокирует апгрейд для всех; - противоречивая комбинация настроек:
health.upgrade_gate_enabled=trueприhealth.enabled=false→ гейта фактически нет, но конфигурация выглядит как будто он есть;cms:upgradeобязан явно сообщать «гейт включён, но health-чеки отключены — апгрейд идёт без проверки», а не молча пропускать как «зелёный»; - обязательность модуля в managed-парке и disable — зафиксировано ревизией 14.07.2026: disable инфра-модулей managed-парка (
cms/healthв их числе) недоступен клиентским ролям — доступен только studio-роли, с обязательным подтверждением и записью в аудит (см. «Зависимости и выключение» выше и §3 стандарта); на standalone-инсталляциях disable доступен клиентским ролям свободнее. Правило «выключение = деградация» при этом не отменяется: модуль всё же выключенный студией (managed) или клиентом (standalone) не роняет сайт — деградирует только наблюдаемость;
Донорский код
Донор: — (переиспользуются практики health-проверок из er (SEO health) и catalog, готовых путей не выдано)
Легаси-импорт (§16 стандарта): не применим. er/catalog — доноры кода (подходы к health-проверкам конкретных систем), не доноры данных: у клиентских legacy-платформ нет структурированной истории health-статусов, которую имело бы смысл переносить — свежая инсталляция стартует с чистой cms_health_history и накапливает её сама по расписанию. Команда cms:health:import-legacy не разрабатывается.
Тесты и приёмка
- [ ] Контрактный тест:
/api/v1/system/healthотдаёт агрегированный статус под токеном - [ ] Health-чек самого модуля health отражает работоспособность планировщика проверок
- [ ] Гейт
cms:upgradeблокирует апгрейд при красном статусе и это покрыто тестом - [ ] При выключении модуля
cms:upgradeпродолжает работать с предупреждением в лог - [ ] Права
health.view/health.manageразграничивают чтение и принудительный запуск - [ ] Алерты об ухудшении статуса уходят через
cms/notifications-busбез дублей - [ ] Флаппинг чека не поднимает алерт до
flap_debounce_checksподряд неудачных прогонов - [ ] Чек без ответа дольше
check_timeout_secondsпомечаетсяtimeoutи не блокирует остальные проверки - [ ] Одновременный ручной и плановый прогон не создаёт дублирующих снимков статуса
- [ ] Ретеншн
cms_health_historyприменяется автоматически поhistory_retention_days - [ ] Контрактный набор
cms-testingпройден, пакет протестирован в testbench-изоляции - [ ] Feature-тест на каждый роут модуля; тестовая БД только
health_test,migrate:freshзапрещён - [ ] Disable модуля клиентской ролью в managed-парке заблокирован («модуль обязателен по условиям поддержки»); studio-роль выключает с подтверждением и записью в аудит
- [ ]
health_statusдоходит доcms/fleet-dashboardчерез суточную телеметриюcms/updatesи красный статус останавливает канареечную волну для этого сайта - [ ] Состав системных чеков по умолчанию (
database,redis,queue-backlog,disk-space,scheduler-heartbeat) присутствует сразу после установки модуля и каждый чек покрыт тестом на срабатывание своего дефолтного порога