Compare commits

...

4 Commits

12 changed files with 1834 additions and 5 deletions
+38
View File
@@ -252,6 +252,44 @@ grafana.nixg.ru {
- В логах caddy много ошибок renew для старых доменов — они имеют - В логах caddy много ошибок renew для старых доменов — они имеют
уже выпущенные сертификаты в caddy_data, работает всё. уже выпущенные сертификаты в caddy_data, работает всё.
## Опыт: VESTI — textfile-метрики + встроенный Prometheus Alerting (2026-09-13)
> Ситуация: добавлен дашборд и алерты на компоненты /opt/vesti. Проект
> управляется через OpenSpec: все изменения — только через openspec/change.
### 22. Textfile-коллектор node-exporter для метрик сервиса
node-exporter умеет отдавать произвольные метрики из файлов каталога
(`--collector.textfile.directory=...` ← `ARGS` в /etc/default/prometheus-node-exporter).
Скрипт раз в минуту пишет файл в Prometheus-формате (1 метрика = 1 строка
`name{labels} value`), node-exporter отдаёт их в /metrics — и Prometheus
тащит их обычным job'ом `node` (тот же 9100). Для сервиса достаточно:
`/opt/vesti/scripts/vesti-metrics.sh` → `/var/lib/node_exporter/textfile_collector/vesti.prom`.
Грабля: каталог textfile принадлежит пользователю `prometheus`, скрипт от
root должен делать `chown prometheus:prometheus` (иначе файл появится, но
node-exporter его не прочитает — права!). Запуск: cron.d (root) — каждую минуту.
### 23. Prometheus-алерты: файл alerts.yml + `/api/v1/rules`
В этом стеке алерты — НЕ Grafana, а встроенный механизм Prometheus:
`prometheus.yml` → `rule_files: /etc/prometheus/alerts.yml` (группы/правила в
YAML). Правила видны в `http://127.0.0.1:9090/api/v1/rules` (JSON: groups,
state, query). Перечитываются ТОЛЬКО рестартом прометеуса
(`docker restart prometheus`), НЕ автоматически.
Грабля: `promtool check config /etc/prometheus/prometheus.yml` считает
alerts.yml правило-файлом: «FAILED: field groups not found» при прямом
проверке alerts.yml — это НОРМАЛЬНО (это не самостоятельный конфиг;
проверять через рrometheus.yml, который находит 13 rules).
### 24. OpenSpec для /opt/monitoring
Все изменения проекта — ТОЛЬКО через `openspec/change/` (proposal/design/tasks/
specs/). Формат spec: `## ADDED Requirements` + `### Requirement: X` +
`#### Scenario:` (иначе validate ругается). После внесения: `openspec validate`,
применить, `openspec archive --yes` + обновить STATUS/README/WALKTHROUGH.
## Опыт: дашборд Grafana пустой (NO DATA) — 3 причины подряд (2026-09-02) ## Опыт: дашборд Grafana пустой (NO DATA) — 3 причины подряд (2026-09-02)
> Дата: 2026-09-02. Ситуация: после этапа 6 (tproxy) пользователь сообщил, > Дата: 2026-09-02. Ситуация: после этапа 6 (tproxy) пользователь сообщил,
+47
View File
@@ -226,3 +226,50 @@ admin-эндпоинты на loopback :8081: `/healthz`, `/readyz`, `/metrics`
Порядок работы: правки коммитятся и пушутся в gitverse (источник), gitea Порядок работы: правки коммитятся и пушутся в gitverse (источник), gitea
подтягивает их mirror'ом (каждые 8ч или вручную mirror-sync). Если bigbox подтягивает их mirror'ом (каждые 8ч или вручную mirror-sync). Если bigbox
сломается — репозиторий остаётся в облаке gitverse. сломается — репозиторий остаётся в облаке gitverse.
## VESTI (2026-09-13)
Проект /opt/vesti: web-интерфейс (:8400, systemd), publisher-контейнер
(docker `vesti-publisher`, :8410/healthz), SOCKS5-туннель Telegram (:1080),
SQLite БД (vesti.db).
### Метрики (textfile-коллектор node-exporter, job `node`, label `component="vesti"`)
Скрипт `/opt/vesti/scripts/vesti-metrics.sh` (запуск: cron.d `vesti-metrics`,
root, каждую минуту; альтернативно systemd-таймер `vesti-metrics.timer`)
пишет `/var/lib/node_exporter/textfile_collector/vesti.prom`. node-exporter
на bigbox: `ARGS="--web.listen-address=10.8.0.2:9100 --collector.textfile.directory=/var/lib/node_exporter/textfile_collector"`.
| Метрика | Смысл |
|---|---|
| vesti_web_up | web :8400 (systemd) |
| vesti_publisher_health | publisher :8410 /healthz |
| vesti_publisher_docker | контейнер publisher (Up) |
| vesti_telegram_tunnel | SOCKS5-туннель :1080 |
| vesti_web_systemd | systemd-юнит web |
| vesti_publisher_bot / _proxy / _channels | детали publisher |
| vesti_db_ok | SQLite vesti.db доступна |
| vesti_db_posts_total / _new_total / _published_total | счётчики БД |
| vesti_db_last_run_ts | Unix-ts последнего рan краулера |
### Дашборд
`grafana/dashboards/vesti/vesti.json` (uid `vesti`, папка **vesti**), 18 панелей.
Провайдер `vesti-dashboards` в `grafana/provisioning/dashboards/dashboards.yml`.
Проверка импорта: `sudo docker cp grafana:/var/lib/grafana/grafana.db /tmp/gf.db`
+ `SELECT uid,title FROM dashboard WHERE uid='vesti'`.
### Алерты (группа `vesti` в alerts.yml)
| Алерт | Выражение | Удержание | Severity |
|---|---|---|---|
| VestiWebDown | `vesti_web_up{component="vesti"} == 0` | 2m | critical |
| VestiPublisherDown | `vesti_publisher_health{component="vesti"} == 0` | 2m | critical |
| VestiPublisherContainerDown | `vesti_publisher_docker{component="vesti"} == 0` | 2m | critical |
| VestiTunnelDown | `vesti_telegram_tunnel{component="vesti"} == 0` | 2m | critical |
| VestiDbDown | `vesti_db_ok{component="vesti"} == 0` | 2m | critical |
| VestiDbStale | `time() - vesti_db_last_run_ts{component="vesti"} > 1800` | 5m | warning |
Реализовано через встроенный Prometheus Alerting (`rule_files` в prometheus.yml
→ alerts.yml; правила видны в `/api/v1/rules`). Перечитывается рестартом
прометеуса (`docker restart prometheus`).
+25 -3
View File
@@ -1,15 +1,37 @@
# Мониторинг кластера — Статус # Мониторинг кластера — Статус
Обновлено: 2026-09-08 (сессия @session:default/20260908_180858_730d1b) Обновлено: 2026-09-13 (добавлен дашборд VESTI)
## Текущее состояние ## Текущее состояние
Стек мониторинга на bigbox (/opt/monitoring): Prometheus 2.53 + Grafana 11.1 + Стек мониторинга на bigbox (/opt/monitoring): Prometheus 2.53 + Grafana 11.1 +
Loki + promtail. Собирает: Garage-кластер (vps01/bigbox/vps02 :3903), системы Loki + promtail. Собирает: Garage-кластер (vps01/bigbox/vps02 :3903), системы
всех 4 нод (node-exporter :9100 по WireGuard), tproxy (vps03, loopback-туннель). всех 4 нод (node-exporter :9100 по WireGuard), tproxy (vps03, loopback-туннель),
**VESTI (компоненты/доступность)**.
Дашборды: **4 per-node** (Node vps01/vps02/vps03/bigbox) + Garage Cluster + Дашборды: **4 per-node** (Node vps01/vps02/vps03/bigbox) + Garage Cluster +
Vinograd WAN + tproxy-панели. vps03 подключён к WG (10.8.0.3), порты наружу Vinograd WAN + tproxy-панели + **VESTI**. vps03 подключён к WG (10.8.0.3), порты наружу
не светятся. Все 4 node-таргета up. Всё в git (gitverse + gitea mirror). не светятся. Все 4 node-таргета up. Всё в git (gitverse + gitea mirror).
## VESTI (добавлено 2026-09-13)
- Метрики: textfile-коллектор node-exporter на bigbox, скрипт
`/opt/vesti/scripts/vesti-metrics.sh` (запуск: `/etc/cron.d/vesti-metrics`, каждую минуту, root).
Пишет `/var/lib/node_exporter/textfile_collector/vesti.prom`.
- Метрики `vesti_*` (label `component="vesti"`), собираются job'ом `node` (10.8.0.2:9100):
`vesti_web_up`, `vesti_publisher_health`, `vesti_publisher_bot`, `vesti_publisher_proxy`,
`vesti_publisher_channels`, `vesti_telegram_tunnel`, `vesti_publisher_docker`,
`vesti_web_systemd`, `vesti_db_ok`, `vesti_db_posts_total`, `vesti_db_new_total`,
`vesti_db_published_total`, `vesti_db_last_run_ts`.
- node-exporter на bigbox: `ARGS="--web.listen-address=10.8.0.2:9100 --collector.textfile.directory=/var/lib/node_exporter/textfile_collector"`
(в /etc/default/prometheus-node-exporter).
- Дашборд: `grafana/dashboards/vesti/vesti.json` (uid `vesti`, папка **vesti**), 18 панелей:
stat-панели доступности (web/publisher/Docker/tunnel/DB/systemd) + детали publisher
(bot/proxy/channels) + БД (posts/new/published + время с последнего рan) + история доступности.
- Провайдер `vesti-dashboards` добавлен в `grafana/provisioning/dashboards/dashboards.yml`
(path: /var/lib/grafana/dashboards/vesti).
- Запуск коллектора: cron.d `/etc/cron.d/vesti-metrics` (root, каждую минуту).
Также прописаны юниты `/etc/systemd/system/vesti-metrics.{service,timer}` —
таймер заведён и активен (systemctl is-enabled: enabled, is-active: active);
при перезагрузке можно переключиться на таймер, отключив cron.d.
## Сделано (за сессию 2026-09-08) ## Сделано (за сессию 2026-09-08)
- vps03 подключён к WireGuard: 10.8.0.3/24, hub vps01 (5.129.217.146:51820), - vps03 подключён к WireGuard: 10.8.0.3/24, hub vps01 (5.129.217.146:51820),
пиры добавлены на vps01/bigbox/vps02 (AllowedIPs пира vps01 расширен до пиры добавлены на vps01/bigbox/vps02 (AllowedIPs пира vps01 расширен до
+44
View File
@@ -54,3 +54,47 @@ groups:
severity: critical severity: critical
annotations: annotations:
summary: Vinograd WAN (Ростелеком) {{ $labels.instance }} is down or unreachable summary: Vinograd WAN (Ростелеком) {{ $labels.instance }} is down or unreachable
- name: vesti
rules:
- alert: VestiWebDown
expr: vesti_web_up{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI web (:8400, systemd) is down
- alert: VestiPublisherDown
expr: vesti_publisher_health{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI publisher (:8410) health check failed
- alert: VestiPublisherContainerDown
expr: vesti_publisher_docker{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI publisher Docker container is not Up
- alert: VestiTunnelDown
expr: vesti_telegram_tunnel{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI Telegram SOCKS5 tunnel (:1080) is down
- alert: VestiDbDown
expr: vesti_db_ok{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI SQLite database vesti.db is unavailable
- alert: VestiDbStale
expr: time() - vesti_db_last_run_ts{component="vesti"} > 1800
for: 5m
labels:
severity: warning
annotations:
summary: VESTI crawler did not run for more than 30 minutes
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
# Генератор дашборда VESTI для Grafana (provisioning).
# Соглашения те же, что в node-<host>.json: datasource uid=Prometheus,
# stat-панели с mapping 0/1, timeseries.
import json
DS = {"type": "prometheus", "uid": "Prometheus"}
# --- утилиты ---
def stat_panel(title, expr, mapping, idx, w=3, h=3, x=0, y=0, legend=""):
return {
"datasource": DS,
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": ([{"options": mapping, "type": "value"}] if mapping else []),
"thresholds": {
"mode": "absolute",
"steps": [{"color": "red", "value": None}, {"color": "green", "value": 1}],
},
"unit": "short",
},
"overrides": [],
},
"gridPos": {"h": h, "w": w, "x": x, "y": y},
"id": idx,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": False},
"textMode": "auto",
},
"pluginVersion": "11.1.0",
"targets": [{"datasource": DS, "expr": expr, "legendFormat": legend, "refId": "A"}],
"title": title,
"type": "stat",
}
def timeseries_panel(title, exprs, idx, w=12, h=8, x=0, y=0, unit="short"):
targets = []
for e, l in exprs:
targets.append({"datasource": DS, "expr": e, "legendFormat": l, "refId": chr(65 + len(targets))})
return {
"datasource": DS,
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"custom": {
"axisCenteredZero": False, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 10,
"gradientMode": "none", "hideFrom": {"legend": False, "tooltip": False, "viz": False},
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": {"type": "linear"}, "showPoints": "never",
"spanNulls": False, "stacking": {"group": "A", "mode": "none"},
"thresholdsStyle": {"mode": "off"},
},
"mappings": [],
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": None}]},
"unit": unit,
},
"overrides": [],
},
"gridPos": {"h": h, "w": w, "x": x, "y": y},
"id": idx,
"options": {
"legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": True},
"tooltip": {"mode": "multi", "sort": "none"},
},
"targets": targets,
"title": title,
"type": "timeseries",
}
panels = []
y = 0
idx = 1
# Row 1: Доступность (stat, 3x4 = 12 панелей в 2 ряда по 6)
row_title = {
"collapsed": False,
"datasource": DS,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
"id": idx, "panels": [], "title": "Доступность компонентов", "type": "row",
}
idx += 1
y += 1
panels.append(row_title)
stats = [
("Web :8400 (systemd)", "vesti_web_up{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
("Publisher :8410 (healthz)", "vesti_publisher_health{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
("Publisher (Docker)", "vesti_publisher_docker{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
("Telegram tunnel :1080", "vesti_telegram_tunnel{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
("DB vesti.db", "vesti_db_ok{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
("Web systemd unit", "vesti_web_systemd{component=\"vesti\"}", {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}),
]
x = 0
for i, (title, expr, mp) in enumerate(stats):
panels.append(stat_panel(title, expr, mp, idx, w=4, h=3, x=(i % 6) * 4, y=y + (i // 6) * 3))
idx += 1
y += 6 # 2 ряда по 3 = 6 строк
# Row 2: Publisher healthz детали (bot/proxy/channels)
row2 = {
"collapsed": False, "datasource": DS,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
"id": idx, "panels": [], "title": "Publisher — детали", "type": "row",
}
idx += 1
y += 1
panels.append(row2)
for i, (title, expr) in enumerate([
("Bot identity", 'vesti_publisher_bot{component="vesti"}'),
("Proxy SOCKS5", 'vesti_publisher_proxy{component="vesti"}'),
("Channels", 'vesti_publisher_channels{component="vesti"}'),
]):
panels.append(stat_panel(title, expr, None, idx, w=4, h=3, x=i * 4, y=y))
idx += 1
y += 3
# Row 3: БД
row3 = {
"collapsed": False, "datasource": DS,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
"id": idx, "panels": [], "title": "База данных VESTI", "type": "row",
}
idx += 1
y += 1
panels.append(row3)
# stat: постов в БД, новых, опубликовано, последний рan
panels.append(stat_panel("Постов в БД", 'vesti_db_posts_total{component="vesti"}', None, idx, w=4, h=3, x=0, y=y))
idx += 1
panels.append(stat_panel("Новых", 'vesti_db_new_total{component="vesti"}', None, idx, w=4, h=3, x=4, y=y))
idx += 1
panels.append(stat_panel("Опубликовано", 'vesti_db_published_total{component="vesti"}', None, idx, w=4, h=3, x=8, y=y))
idx += 1
# временной ряд последнего рan (ts → время)
panels.append(timeseries_panel("Последний рan краулера", [('time() - vesti_db_last_run_ts{component="vesti"}', "сек назад")], idx, w=12, h=4, x=12, y=y, unit="s"))
idx += 1
y += 4
# Row 4: Временные ряды доступности (возврат по вре)
row4 = {
"collapsed": False, "datasource": DS,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
"id": idx, "panels": [], "title": "История доступности (1/0)", "type": "row",
}
idx += 1
y += 1
panels.append(row4)
panels.append(timeseries_panel(
"Доступность компонентов",
[(f'{m}{{component="vesti"}}', t) for m, t in [
("vesti_web_up", "web"),
("vesti_publisher_health", "publisher"),
("vesti_publisher_docker", "docker"),
("vesti_telegram_tunnel", "tunnel"),
("vesti_db_ok", "db"),
("vesti_web_systemd", "web_sys"),
]],
idx, w=24, h=8, x=0, y=y, unit="short"))
idx += 1
y += 8
dashboard = {
"annotations": {"list": []},
"editable": True,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": None,
"links": [],
"panels": panels,
"refresh": "15s",
"schemaVersion": 1,
"tags": ["vesti", "bigbox"],
"time": {"from": "now-6h", "to": "now"},
"timepicker": [{"refresh": "15s"}],
"title": "VESTI",
"uid": "vesti",
"version": 1,
}
with open("/opt/monitoring/grafana/dashboards/vesti/vesti.json", "w") as f:
json.dump(dashboard, f, indent=2, ensure_ascii=False)
print("written /opt/monitoring/grafana/dashboards/vesti/vesti.json", len(panels), "pabels")
File diff suppressed because it is too large Load Diff
@@ -27,3 +27,12 @@ providers:
updateIntervalSeconds: 30 updateIntervalSeconds: 30
options: options:
path: /var/lib/grafana/dashboards/vinogorod path: /var/lib/grafana/dashboards/vinogorod
- name: 'vesti-dashboards'
orgId: 1
folder: 'vesti'
type: file
disableDeletion: false
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards/vesti
@@ -0,0 +1,61 @@
# Design
## Механизм алертов
В стеке /opt/monitoring алерты реализованы **Prometheus Alerting rules** (prometheus.yml
`rule_files: /etc/prometheus/alerts.yml`, Prometheus 3.1+ встроенный алертинг с мульти-секционной
группировкой `groups:`/`rules:`). Формат rules (алерты на базе PromQL-выражений с `for` = время
удержания условия, `labels.severity`, `annotations.summary`). Доставка уведомлений — согласно
существующей конфигурации (webhook/канал, настроенный в Prometheus или в той же alerts.yml
внешним получателем — проверить фактический механизм доставки в стеке; в референсах скилла
указано, что уведомления приходят в чат).
## Метрики VESTI (textfile-коллектор, job `node`, label `component="vesti"`)
| Метрика | Смысл | Алерт |
|---|---|---|
| vesti_web_up | web :8400 (systemd) | ==0 → critical |
| vesti_publisher_health | publisher :8410 /healthz | ==0 → critical |
| vesti_publisher_docker | контейнер publisher (docker ps) | ==0 → critical |
| vesti_telegram_tunnel | SOCKS5-туннель :1080 | ==0 → critical |
| vesti_db_ok | SQLite vesti.db доступна | ==0 → critical |
| vesti_db_last_run_ts | Unix-ts последнего рan краулера | time()-ts >1800 → warning |
## Формат rules (из alerts.yml)
```yaml
groups:
- name: vesti
rules:
- alert: VestiWebDown
expr: vesti_web_up{component="vesti"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: VESTI web (:8400) is down
```
Время реакции: `for: 2m` — алерт срабатывает после 2 минут непрерывного условия
(метрики обновляются раз в минуту скриптом vesti-metrics.sh → 2-3 пробы подряд).
## Верификация
1. `docker exec prometheus promtool check config /etc/prometheus/prometheus.yml` → OK
(проверяет и rule_files).
2. `docker compose restart prometheus` (перечитывает alerts.yml).
3. Запрос в Prometheus API: значения метрик vesti_* присутствуют.
4. Проверка алертов: имитация (временно выставить метрику в 0, убедиться что алерт firing,
вернуть обратно) — или проверка через Prometheus /api/v1/rules/дубликат.
5. Обновить README.md + EXPERIENCE.md, openspec validate + archive.
## Риски
- При `for: 2m` и интервале скрипта 1 мин алерт может срабатывать с задержкой до 3 мин
(приемлемо для внутреннего сервиса).
- Метрики пишутся только при успешном выполнении скрипта; если textfile-файл пропадёт
(сломан скрипт) — метрики исчезнут и алерт по `==0` не сработает. Для этого есть
`VestiDbStale` (time() - ts) — он тоже покроет случай пропадания данных? Нет: если файл
пропал, ts не обновится → last_run_ts останется старым → VestiDbStale сработает.
Дополнительно можно добавить expr на отсутствие данных (deadman), но это выходит за рамки
текущего change — зафиксировать в EXPERIENCE.md как известное ограничение.
@@ -0,0 +1,30 @@
# Proposal: vesti-alerts
## Why
Проект /opt/vesti (веб :8400 + publisher-контейнер :8410 + SOCKS5-туннель :1080 + SQLite БД)
получил дашборд VESTI в Grafana (2026-09-13), но **алертов на падение компонентов нет**:
при недоступности web/publisher/туннеля/БД никто не узнает в реальном времени (только постфактум
на дашборде). Нужны алерты в существующем механизме мониторинга (Prometheus rule_files → alerts.yml)
на все компоненты vesti, с уведомлением (webhook/настроенный канал).
## What Changes
- В `/opt/monitoring/alerts.yml` добавляется группа `vesti` с алертами:
- `VestiWebDown` — `vesti_web_up{component="vesti"} == 0`, for 2m, severity: critical
- `VestiPublisherDown` — `vesti_publisher_health{component="vesti"} == 0`, for 2m, severity: critical
- `VestiPublisherContainerDown` — `vesti_publisher_docker{component="vesti"} == 0`, for 2m, severity: critical
- `VestiTunnelDown` — `vesti_telegram_tunnel{component="vesti"} == 0`, for 2m, severity: critical
- `VestiDbDown` — `vesti_db_ok{component="vesti"} == 0`, for 2m, severity: critical
- `VestiDbStale` — `time() - vesti_db_last_run_ts{component="vesti"} > 1800`, for 5m, severity: warning
(краулер не работал >30 мин)
- Все алерты аннотируются summary с именем компонента.
- Верификация: `promtool check config` + запрос фактических значений в Prometheus API.
## Capabilities
### New Capabilities
- `vesti-alerts`: алерты на компоненты VESTI (web, publisher, docker, tunnel, db, краулер staleness).
### Modified Capabilities
- `monitoring-stack` (alerts.yml) — добавлена группа `vesti`.
@@ -0,0 +1,99 @@
# vesti-alerts
## ADDED Requirements
### Requirement: VestiWebDown
Алерт на недоступность web-интерфейса VESTI (:8400, systemd-юнит).
#### Scenario: web не отвечает
- Given метрика `vesti_web_up{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiWebDown` переходит в Firing (severity critical)
#### Scenario: web восстановился
- Given метрика `vesti_web_up{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiPublisherDown
Алерт на недоступность publisher-сервиса VESTI (:8410, /healthz).
#### Scenario: publisher не отвечает
- Given метрика `vesti_publisher_health{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiPublisherDown` переходит в Firing (severity critical)
#### Scenario: publisher восстановился
- Given метрика `vesti_publisher_health{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiPublisherContainerDown
Алерт на остановку Docker-контейнера publisher.
#### Scenario: контейнер publisher не в статусе Up
- Given метрика `vesti_publisher_docker{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiPublisherContainerDown` переходит в Firing (severity critical)
#### Scenario: контейнер publisher снова Up
- Given метрика `vesti_publisher_docker{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiTunnelDown
Алерт на недоступность SOCKS5-туннеля Telegram (:1080).
#### Scenario: туннель недоступен
- Given метрика `vesti_telegram_tunnel{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiTunnelDown` переходит в Firing (severity critical)
#### Scenario: туннель восстановился
- Given метрика `vesti_telegram_tunnel{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiDbDown
Алерт на недоступность SQLite-базы vesti.db.
#### Scenario: БД недоступна
- Given метрика `vesti_db_ok{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiDbDown` переходит в Firing (severity critical)
#### Scenario: БД восстановилась
- Given метрика `vesti_db_ok{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiDbStale
Алерт на устаревание данных краулера (не запускался >30 минут).
#### Scenario: краулер не запускался более 30 минут
- Given метрика `vesti_db_last_run_ts{component="vesti"}` = T
- When `time() - T` > 1800 секунд и условие держится 5 минут
- Then алерт `VestiDbStale` переходит в Firing (severity warning)
#### Scenario: краулер снова запустился
- Given метрика `vesti_db_last_run_ts{component="vesti"}` обновлена (time() - ts < 1800)
- When алерт в Firing
- Then алерт закрывается автоматически
@@ -0,0 +1,22 @@
# Tasks
## 1. Алерты в alerts.yml
- [x] 1.1 В `/opt/monitoring/alerts.yml` добавить группу `vesti`:
- VestiWebDown (vesti_web_up == 0, for 2m, critical)
- VestiPublisherDown (vesti_publisher_health == 0, for 2m, critical)
- VestiPublisherContainerDown (vesti_publisher_docker == 0, for 2m, critical)
- VestiTunnelDown (vesti_telegram_tunnel == 0, for 2m, critical)
- VestiDbDown (vesti_db_ok == 0, for 2m, critical)
- VestiDbStale (time() - vesti_db_last_run_ts > 1800, for 5m, warning)
- [x] 1.2 Проверка: `docker exec prometheus promtool check config /etc/prometheus/prometheus.yml` → OK (13 rules)
- [x] 1.3 Рестарт: `docker restart prometheus`
- [x] 1.4 Проверка: группа vesti в /api/v1/rules, 6 правил, state=inactive (норма)
## 2. Документация
- [x] 2.1 Обновить `/opt/monitoring/README.md` (секция VESTI: алерты, метрики)
- [x] 2.2 Добавить запись в `/opt/monitoring/EXPERIENCE.md` (22-24: textfile, alerting, OpenSpec)
## 3. Git и OpenSpec
- [ ] 3.1 `git add` (поимённо) + commit + push в gitverse (истина), gitea подтянет mirror
- [ ] 3.2 `openspec validate` + `openspec archive --yes`
- [ ] 3.3 Обновить STATUS.md/WALKTHROUGH.md
+102
View File
@@ -0,0 +1,102 @@
# vesti-alerts Specification
## Purpose
TBD - created by archiving change 2026-09-13-vesti-alerts. Update Purpose after archive.
## Requirements
### Requirement: VestiWebDown
Алерт на недоступность web-интерфейса VESTI (:8400, systemd-юнит).
#### Scenario: web не отвечает
- Given метрика `vesti_web_up{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiWebDown` переходит в Firing (severity critical)
#### Scenario: web восстановился
- Given метрика `vesti_web_up{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiPublisherDown
Алерт на недоступность publisher-сервиса VESTI (:8410, /healthz).
#### Scenario: publisher не отвечает
- Given метрика `vesti_publisher_health{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiPublisherDown` переходит в Firing (severity critical)
#### Scenario: publisher восстановился
- Given метрика `vesti_publisher_health{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiPublisherContainerDown
Алерт на остановку Docker-контейнера publisher.
#### Scenario: контейнер publisher не в статусе Up
- Given метрика `vesti_publisher_docker{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiPublisherContainerDown` переходит в Firing (severity critical)
#### Scenario: контейнер publisher снова Up
- Given метрика `vesti_publisher_docker{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiTunnelDown
Алерт на недоступность SOCKS5-туннеля Telegram (:1080).
#### Scenario: туннель недоступен
- Given метрика `vesti_telegram_tunnel{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiTunnelDown` переходит в Firing (severity critical)
#### Scenario: туннель восстановился
- Given метрика `vesti_telegram_tunnel{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiDbDown
Алерт на недоступность SQLite-базы vesti.db.
#### Scenario: БД недоступна
- Given метрика `vesti_db_ok{component="vesti"}` = 0
- When условие держится 2 минуты
- Then алерт `VestiDbDown` переходит в Firing (severity critical)
#### Scenario: БД восстановилась
- Given метрика `vesti_db_ok{component="vesti"}` = 1
- When алерт в Firing
- Then алерт закрывается автоматически
### Requirement: VestiDbStale
Алерт на устаревание данных краулера (не запускался >30 минут).
#### Scenario: краулер не запускался более 30 минут
- Given метрика `vesti_db_last_run_ts{component="vesti"}` = T
- When `time() - T` > 1800 секунд и условие держится 5 минут
- Then алерт `VestiDbStale` переходит в Firing (severity warning)
#### Scenario: краулер снова запустился
- Given метрика `vesti_db_last_run_ts{component="vesti"}` обновлена (time() - ts < 1800)
- When алерт в Firing
- Then алерт закрывается автоматически