mirror of
https://gitverse.ru/kpa39l/monitoring.git
synced 2026-09-29 09:55:09 +00:00
Compare commits
5 Commits
3c5e9e5a71
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ee9249697d | |||
| 9cde035e5a | |||
| 62a4f1a0db | |||
| df1ebed76d | |||
| 847fcfad9c |
@@ -252,6 +252,44 @@ grafana.nixg.ru {
|
||||
- В логах caddy много ошибок renew для старых доменов — они имеют
|
||||
уже выпущенные сертификаты в 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)
|
||||
|
||||
> Дата: 2026-09-02. Ситуация: после этапа 6 (tproxy) пользователь сообщил,
|
||||
|
||||
@@ -225,4 +225,51 @@ admin-эндпоинты на loopback :8081: `/healthz`, `/readyz`, `/metrics`
|
||||
|
||||
Порядок работы: правки коммитятся и пушутся в gitverse (источник), gitea
|
||||
подтягивает их 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`).
|
||||
@@ -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 +
|
||||
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 +
|
||||
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).
|
||||
|
||||
## 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)
|
||||
- vps03 подключён к WireGuard: 10.8.0.3/24, hub vps01 (5.129.217.146:51820),
|
||||
пиры добавлены на vps01/bigbox/vps02 (AllowedIPs пира vps01 расширен до
|
||||
|
||||
+44
@@ -54,3 +54,47 @@ groups:
|
||||
severity: critical
|
||||
annotations:
|
||||
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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ providers:
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
path: /var/lib/grafana/dashboards/garage-cluster.json
|
||||
|
||||
- name: 'node-dashboards'
|
||||
orgId: 1
|
||||
@@ -26,4 +26,22 @@ providers:
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
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
|
||||
|
||||
- name: 'gotosocial-dashboards'
|
||||
orgId: 1
|
||||
folder: 'gotosocial'
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards/gotosocial
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-19
|
||||
@@ -0,0 +1,64 @@
|
||||
# Design: add-gotosocial-dashboard
|
||||
|
||||
## Context
|
||||
|
||||
См. proposal.md — Why. Метрики GtS уже поступают в VictoriaMetrics (job
|
||||
`gotosocial` в prometheus.yml active, `gotosocial_instance_total_users`
|
||||
отвечает с метками `{service="gotosocial",host="bigbox",
|
||||
instance="bigbox:9464",otel_scope_name="GoToSocial"}`). Задача — только
|
||||
визуализация: дашборд Grafana + provisioning.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**
|
||||
- Дашборд `GoToSocial` в Grafana (папка gotosocial) со стандартными
|
||||
панелями: доступность, инстанс, воркеры, HTTP, SQL, процесс.
|
||||
- Автоимпорт через provisioning, как у vesti/vinogorod/nodes.
|
||||
|
||||
**Non-Goals**
|
||||
- Алерты — отдельное изменение (как vesti-alerts).
|
||||
- Изменения docker-compose/prometheus.yml — не нужны.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Формат дашборда**: копия стиля `grafana/dashboards/vesti/vesti.json`
|
||||
(Grafana 11.1, datasource uid "Prometheus", stat/timeseries панели).
|
||||
- **Доступность**: expr `gotosocial_instance_total_users` + stat (mapping
|
||||
0=DOWN/1=UP по наличию значения; VI с блек-значением если нет данных —
|
||||
зелёный/красный как в vesti).
|
||||
- **Панели по группам** (row-группировка как vesti):
|
||||
1. Доступность (stat): instance total users (инстанс жив)
|
||||
2. Instance: `gotosocial_instance_total_{users,statuses,federating_instances}`
|
||||
3. Воркеры: 6 пулов `gotosocial_workers_*_{count,queue}` — count на одном
|
||||
графике (stacks), queue на другом (столбики)
|
||||
4. HTTP: `http_server_requests_total` (rate, все routes — sum),
|
||||
`http_server_requests_active` (gauge)
|
||||
5. SQL: `go_sql_connections_{open,in_use}`, `go_sql_query_timing_milliseconds{quantile="0.5"}`
|
||||
6. Процесс: `process_resident_memory_bytes` (GB), `process_cpu_seconds_total`,
|
||||
`process_open_fds`
|
||||
- **Метка фильтров**: везде `instance="bigbox:9464"` (или service — что
|
||||
уникальнее; у GtS instance уже bigbox:9464).
|
||||
- **Provisioning**: новый провайдер в dashboards.yml, folder: gotosocial,
|
||||
path: /var/lib/grafana/dashboards/gotosocial.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Крупный дашборд, много панелей] → писать аккуратно, проверять импорт по
|
||||
grafana.db.
|
||||
- [Панель доступности по instance_total_users] — если инстанс упадёт,
|
||||
метрика пропадёт, stat станет красным (нет данных) — ок как индикатор.
|
||||
- [Новые метрики в новых версиях GtS] — дашборд читает по именам; при
|
||||
обновлении GtS проверить, что имена не поменялись.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Создать `grafana/dashboards/gotosocial/gotosocial.json`.
|
||||
2. Добавить провайдер `gotosocial-dashboards` в dashboards.yml.
|
||||
3. Подождать 30-60с; проверить импорт через grafana.db
|
||||
(`SELECT uid FROM dashboard WHERE uid LIKE 'gotosocial%'`).
|
||||
4. Обновить STATUS.md (задача закрыта).
|
||||
5. Commit+push (gitverse) + mirror-sync.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Алерты на недоступность/очереди GtS — отдельный change, здесь нет.
|
||||
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
В STATUS.md GoToSocial открыта задача «Мониторинг: пробросить :9464 (метрики
|
||||
уже слушают), job в prometheus.yml, дашборд Grafana». Job `gotosocial`
|
||||
(127.0.0.1:9464 → /metrics) и relabel `instance=bigbox:9464` уже добавлены
|
||||
ранее и данные реально приходят в VictoriaMetrics
|
||||
(`gotosocial_instance_total_users` отвечает с метками host/service/instance).
|
||||
Осталось: дашборд Grafana для метрик GtS + provisioning-провайдер.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Новый дашборд Grafana `grafana/dashboards/gotosocial/gotosocial.json`
|
||||
(панели: доступность, instance-статистика, воркеры, HTTP, SQL, процесс).
|
||||
- Новая папка Grafana `gotosocial` + провайдер в
|
||||
`grafana/provisioning/dashboards/dashboards.yml`.
|
||||
- Автоимпорт дашборда через provisioning (~30с).
|
||||
- Обновление STATUS.md (задача закрывается).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `gotosocial-monitoring`: дашборд Grafana с метриками GtS
|
||||
(instance users/statuses, workers, HTTP, SQL, Go-runtime).
|
||||
|
||||
### Modified Capabilities
|
||||
- `vesti-alerts`, `grafana-access-control`, `vinograd-wan-monitoring`:
|
||||
не затрагиваются.
|
||||
|
||||
## Impact
|
||||
|
||||
- `/opt/monitoring/grafana/dashboards/gotosocial/gotosocial.json` — новый.
|
||||
- `/opt/monitoring/grafana/provisioning/dashboards/dashboards.yml` — +1 провайдер.
|
||||
- Grafana provisioning автоматически импортирует файл (~30с).
|
||||
- Никаких изменений в docker-compose/prometheus.yml — job уже есть.
|
||||
- Откат: удалить файл дашборда и блок провайдера из dashboards.yml
|
||||
(Grafana удалит визуализацию, данные в VM остаются).
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# gotosocial-monitoring Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Дашборд Grafana для метрик GoToSocial (bigbox): наблюдение за инстансом
|
||||
(пользователи/статусы/федерирующиеся инстансы), воркерами (очереди),
|
||||
HTTP-активностью, SQL-соединениями и ресурсами процесса.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Job gotosocial в prometheus.yml активен
|
||||
|
||||
- **MUST**: job `gotosocial` опрашивает `127.0.0.1:9464/metrics` и
|
||||
релейб`instance=bigbox:9464`.
|
||||
- **MUST**: метрики доступны в VictoriaMetrics с метками
|
||||
`service="gotosocial"`, `host="bigbox"`, `instance="bigbox:9464"`.
|
||||
|
||||
#### Scenario: Метрики отвечают
|
||||
|
||||
- **GIVEN** job gotosocial настроен в prometheus.yml
|
||||
- **WHEN** запросить
|
||||
`/api/v1/series?match[]=gotosocial_instance_total_users`
|
||||
- **THEN** ответ содержит метрику с `instance: "bigbox:9464"` и данные
|
||||
|
||||
### Requirement: Дашборд gotosocial в Grafana
|
||||
|
||||
- **MUST**: Провайдер `gotosocial-dashboards` в
|
||||
`grafana/provisioning/dashboards/dashboards.yml` импортирует
|
||||
`/var/lib/grafana/dashboards/gotosocial` (updateIntervalSeconds: 30).
|
||||
- **MUST**: Дашборд `grafana/dashboards/gotosocial/gotosocial.json` содержит:
|
||||
- stat-панель доступности (запрос `gotosocial_instance_total_users` — если
|
||||
данные есть, служба жива)
|
||||
- instance-панели: users, statuses, federating_instances
|
||||
- воркеры: по каждому пулу `gotosocial_workers_*` count+queue
|
||||
- HTTP: `http_server_requests_total`, `http_server_requests_active`
|
||||
- SQL: `go_sql_connections_in_use`
|
||||
- процесс: RSS, CPU секунды, open_fds
|
||||
- **SHOULD**: панели названы по-русски, структура соответствует стилю vesti.json.
|
||||
|
||||
#### Scenario: Дашборд импортирован
|
||||
|
||||
- **GIVEN** файл дашборда и провайдер добавлены
|
||||
- **WHEN** пройти 30-60с после изменения provisioning
|
||||
- **THEN** дашборд `GoToSocial` виден в папке
|
||||
`gotosocial` (проверка: `SELECT uid FROM dashboard WHERE uid LIKE 'gotosocial%'`
|
||||
в grafana.db)
|
||||
|
||||
### Requirement: Инстанс виден без алертов
|
||||
|
||||
- **MUST**: Дашборд не содержит алертинга (алерты GtS — отдельное изменение).
|
||||
- **SHOULD**: Панель доступности зелёная (UP), когда `gotosocial_instance_total_users`
|
||||
возвращает значение; красная — когда нет данных.
|
||||
|
||||
#### Scenario: Инстанс жив
|
||||
|
||||
- **GIVEN** GtS отвечает на 9464
|
||||
- **WHEN** проверить stat-панель «Инстанс жив»
|
||||
- **THEN** значение `gotosocial_instance_total_users` > 0 и панель зелёная
|
||||
|
||||
#### Scenario: Инстанс недоступен
|
||||
|
||||
- **GIVEN** GtS не отвечает на 9464
|
||||
- **WHEN** пройти интервал опроса (15с)
|
||||
- **THEN** метрика пропадает и панель доступности показывает DOWN/красная
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Дашборд Grafana gotosocial
|
||||
|
||||
- [x] 1.1 Создать `grafana/dashboards/gotosocial/gotosocial.json`
|
||||
(генератор: `scripts/gen_gotosocial_dash.py`) — 20 панелей:
|
||||
Доступность (stat), Инстанс (users/statuses/federating), Воркеры
|
||||
(count+queue × 6 пулов), HTTP (req/s, active, p50), SQL (соединения,
|
||||
p50), Процесс (RSS, CPU, FD).
|
||||
- [x] 1.2 Добавить провайдер `gotosocial-dashboards` в dashboards.yml
|
||||
(folder: gotosocial, path: /var/lib/grafana/dashboards/gotosocial).
|
||||
- [x] 1.3 Исправить пересечение провайдеров: `garage-dashboards` смотрел на
|
||||
весь `/var/lib/grafana/dashboards` (дублировал nodes/vinogorod/vesti/
|
||||
gotosocial → блокировал запись) → сужен до
|
||||
`/var/lib/grafana/dashboards/garage-cluster.json`.
|
||||
- [x] 1.4 Рестарт grafana (перечитывает provisioning).
|
||||
- [x] 1.5 Проверка импорта: `gotosocial-main` в grafana.db, папка
|
||||
`gotosocial` (id 23), включён в dashboard_provisioning.
|
||||
- [x] 1.6 Обновить STATUS.md (задача закрыта).
|
||||
@@ -0,0 +1,63 @@
|
||||
# gotosocial-monitoring Specification
|
||||
|
||||
## Purpose
|
||||
Дашборд Grafana для метрик GoToSocial (bigbox): наблюдение за инстансом
|
||||
(пользователи/статусы/федерирующиеся инстансы), воркерами (очереди),
|
||||
HTTP-активностью, SQL-соединениями и ресурсами процесса.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Job gotosocial в prometheus.yml активен
|
||||
|
||||
- **MUST**: job `gotosocial` опрашивает `127.0.0.1:9464/metrics` и
|
||||
релейб`instance=bigbox:9464`.
|
||||
- **MUST**: метрики доступны в VictoriaMetrics с метками
|
||||
`service="gotosocial"`, `host="bigbox"`, `instance="bigbox:9464"`.
|
||||
|
||||
#### Scenario: Метрики отвечают
|
||||
|
||||
- **GIVEN** job gotosocial настроен в prometheus.yml
|
||||
- **WHEN** запросить
|
||||
`/api/v1/series?match[]=gotosocial_instance_total_users`
|
||||
- **THEN** ответ содержит метрику с `instance: "bigbox:9464"` и данные
|
||||
|
||||
### Requirement: Дашборд gotosocial в Grafana
|
||||
|
||||
- **MUST**: Провайдер `gotosocial-dashboards` в
|
||||
`grafana/provisioning/dashboards/dashboards.yml` импортирует
|
||||
`/var/lib/grafana/dashboards/gotosocial` (updateIntervalSeconds: 30).
|
||||
- **MUST**: Дашборд `grafana/dashboards/gotosocial/gotosocial.json` содержит:
|
||||
- stat-панель доступности (запрос `gotosocial_instance_total_users` — если
|
||||
данные есть, служба жива)
|
||||
- instance-панели: users, statuses, federating_instances
|
||||
- воркеры: по каждому пулу `gotosocial_workers_*` count+queue
|
||||
- HTTP: `http_server_requests_total`, `http_server_requests_active`
|
||||
- SQL: `go_sql_connections_in_use`
|
||||
- процесс: RSS, CPU секунды, open_fds
|
||||
- **SHOULD**: панели названы по-русски, структура соответствует стилю vesti.json.
|
||||
|
||||
#### Scenario: Дашборд импортирован
|
||||
|
||||
- **GIVEN** файл дашборда и провайдер добавлены
|
||||
- **WHEN** пройти 30-60с после изменения provisioning
|
||||
- **THEN** дашборд `GoToSocial` виден в папке
|
||||
`gotosocial` (проверка: `SELECT uid FROM dashboard WHERE uid LIKE 'gotosocial%'`
|
||||
в grafana.db)
|
||||
|
||||
### Requirement: Инстанс виден без алертов
|
||||
|
||||
- **MUST**: Дашборд не содержит алертинга (алерты GtS — отдельное изменение).
|
||||
- **SHOULD**: Панель доступности зелёная (UP), когда `gotosocial_instance_total_users`
|
||||
возвращает значение; красная — когда нет данных.
|
||||
|
||||
#### Scenario: Инстанс жив
|
||||
|
||||
- **GIVEN** GtS отвечает на 9464
|
||||
- **WHEN** проверить stat-панель «Инстанс жив»
|
||||
- **THEN** значение `gotosocial_instance_total_users` > 0 и панель зелёная
|
||||
|
||||
#### Scenario: Инстанс недоступен
|
||||
|
||||
- **GIVEN** GtS не отвечает на 9464
|
||||
- **WHEN** пройти интервал опроса (15с)
|
||||
- **THEN** метрика пропадает и панель доступности показывает DOWN/красная
|
||||
@@ -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 алерт закрывается автоматически
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
# Генератор дашборда Grafana "GoToSocial" — по стилю vesti.json
|
||||
# Результат: grafana/dashboards/gotosocial/gotosocial.json
|
||||
import json, os, uuid
|
||||
|
||||
DS = {"type": "prometheus", "uid": "Prometheus"}
|
||||
F = "bigbox:9464"
|
||||
|
||||
def tgt(expr, legend="", ref="A"):
|
||||
return {"datasource": DS, "expr": expr, "legendFormat": legend, "refId": ref}
|
||||
|
||||
def stat_panel(title, expr, x, y, w=4, h=3, mapping=None, unit="short", thresholds=None):
|
||||
if mapping is None:
|
||||
mapping = {"0": {"color": "red", "text": "DOWN"}, "1": {"color": "green", "text": "UP"}}
|
||||
if thresholds is None:
|
||||
thresholds = {"mode": "absolute", "steps": [
|
||||
{"color": "red", "value": None}, {"color": "green", "value": 1}]}
|
||||
return {
|
||||
"datasource": DS,
|
||||
"fieldConfig": {"defaults": {
|
||||
"color": {"mode": "thresholds"},
|
||||
"mappings": [{"options": mapping, "type": "value"}],
|
||||
"thresholds": thresholds, "unit": unit},
|
||||
"overrides": []},
|
||||
"gridPos": {"h": h, "w": w, "x": x, "y": y},
|
||||
"id": uuid.uuid4().int & 0xFFFF,
|
||||
"options": {
|
||||
"colorMode": "background", "graphMode": "none", "justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": False},
|
||||
"textMode": "auto"},
|
||||
"pluginVersion": "11.1.0",
|
||||
"targets": [tgt(expr, "")],
|
||||
"title": title, "type": "stat",
|
||||
}
|
||||
|
||||
def ts_panel(title, exprs, x, y, w=12, h=4, unit="short", legend=False):
|
||||
# exprs: list of (expr, legend) tuples
|
||||
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": uuid.uuid4().int & 0xFFFF,
|
||||
"options": {"legend": {"calcs": [], "displayMode": "list", "placement": "bottom",
|
||||
"showLegend": legend},
|
||||
"tooltip": {"mode": "multi", "sort": "none"}},
|
||||
"targets": [tgt(e, l) for e, l in exprs],
|
||||
"title": title, "type": "timeseries",
|
||||
}
|
||||
|
||||
def row(title, y):
|
||||
return {"collapsed": False, "gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
|
||||
"id": uuid.uuid4().int & 0xFFFF, "panels": [], "title": title, "type": "row"}
|
||||
|
||||
panels = []
|
||||
y = 0
|
||||
|
||||
# ---- Доступность ----
|
||||
panels.append(row("Доступность", y)); y += 1
|
||||
panels.append(stat_panel("Инстанс жив (total users)",
|
||||
f'gotosocial_instance_total_users{{instance="{F}"}}', 0, y, w=8, h=3)); y += 3
|
||||
|
||||
# ---- Instance ----
|
||||
panels.append(row("Инстанс", y)); y += 1
|
||||
panels.append(ts_panel("Пользователи", [(f'gotosocial_instance_total_users{{instance="{F}"}}', "users")], 0, y, w=8, h=4))
|
||||
panels.append(ts_panel("Статусы", [(f'gotosocial_instance_total_statuses{{instance="{F}"}}', "statuses")], 8, y, w=8, h=4))
|
||||
panels.append(ts_panel("Федерирующиеся инстансы", [(f'gotosocial_instance_total_federating_instances{{instance="{F}"}}', "federating")], 16, y, w=8, h=4)); y += 4
|
||||
|
||||
# ---- Workers ----
|
||||
panels.append(row("Воркеры", y)); y += 1
|
||||
workers = ["client_api", "fedi_api", "processing", "delivery", "dereference", "webpush"]
|
||||
count_exprs = [(f'gotosocial_workers_{w}_count{{instance="{F}"}}', w) for w in workers]
|
||||
queue_exprs = [(f'gotosocial_workers_{w}_queue{{instance="{F}"}}', w) for w in workers]
|
||||
panels.append(ts_panel("Воркеры: count (обработано)", count_exprs, 0, y, w=12, h=4, legend=True))
|
||||
panels.append(ts_panel("Воркеры: queue (очередь)", queue_exprs, 12, y, w=12, h=4, legend=True)); y += 4
|
||||
|
||||
# ---- HTTP ----
|
||||
panels.append(row("HTTP", y)); y += 1
|
||||
panels.append(ts_panel("Запросы/с (все routes)",
|
||||
[(f'rate(http_server_requests_total{{otel_scope_name="gin",instance="{F}"}})',
|
||||
"req/s")], 0, y, w=8, h=4))
|
||||
panels.append(ts_panel("Активные запросы",
|
||||
[(f'http_server_requests_active{{instance="{F}"}}', "active")], 8, y, w=8, h=4))
|
||||
panels.append(ts_panel("Длительность (p50)",
|
||||
[(f'http_server_duration_milliseconds{{quantile="0.5",instance="{F}"}}', "p50 ms")],
|
||||
16, y, w=8, h=4)); y += 4
|
||||
|
||||
# ---- SQL ----
|
||||
panels.append(row("SQL", y)); y += 1
|
||||
panels.append(ts_panel("SQL соединения",
|
||||
[(f'go_sql_connections_open{{instance="{F}"}}', "open"),
|
||||
(f'go_sql_connections_in_use{{instance="{F}"}}', "in_use")], 0, y, w=12, h=4, legend=True))
|
||||
panels.append(ts_panel("SQL запросы (p50 ms)",
|
||||
[(f'go_sql_query_timing_milliseconds{{quantile="0.5",instance="{F}"}}', "p50")],
|
||||
12, y, w=12, h=4)); y += 4
|
||||
|
||||
# ---- Процесс ----
|
||||
panels.append(row("Процесс", y)); y += 1
|
||||
panels.append(ts_panel("RSS память (GB)",
|
||||
[(f'process_resident_memory_bytes{{instance="{F}"}}/1024/1024/1024', "RSS GB")], 0, y, w=8, h=4))
|
||||
panels.append(ts_panel("CPU (сек)",
|
||||
[(f'process_cpu_seconds_total{{instance="{F}"}}', "cpu s")], 8, y, w=8, h=4))
|
||||
panels.append(ts_panel("Открытые FD",
|
||||
[(f'process_open_fds{{instance="{F}"}}', "fds")], 16, y, w=8, h=4)); y += 4
|
||||
|
||||
dash = {
|
||||
"annotations": {"list": []},
|
||||
"editable": True,
|
||||
"fiscalYearStartMonth": 1,
|
||||
"graphTooltip": 0,
|
||||
"id": uuid.uuid4().int & 0xFFFF,
|
||||
"links": [], "panels": panels, "refresh": "30s",
|
||||
"tags": ["gotosocial"],
|
||||
"title": "GoToSocial",
|
||||
"type": "dashboard",
|
||||
"uid": "gotosocial-main",
|
||||
"version": 1,
|
||||
"schemaVersion": 1,
|
||||
}
|
||||
|
||||
outdir = os.path.join(os.path.dirname(__file__), "..", "grafana", "dashboards", "gotosocial")
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
out = os.path.join(outdir, "gotosocial.json")
|
||||
with open(out, "w") as f:
|
||||
json.dump(dash, f, ensure_ascii=False, indent=1)
|
||||
print("OK:", out, len(panels), "panels")
|
||||
Reference in New Issue
Block a user