feat(gotosocial): Grafana dashboard GoToSocial (20 panels); fix dashboard provisioning provider overlap; openspec: gotosocial-monitoring

This commit is contained in:
kpa39l
2026-09-19 18:44:24 +00:00
parent 9cde035e5a
commit ee9249697d
9 changed files with 1790 additions and 2 deletions
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -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
@@ -36,3 +36,12 @@ providers:
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,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 остаются).
@@ -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/красная
+139
View File
@@ -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")