New readme

This commit is contained in:
Magnus Root 2026-02-25 15:29:05 +03:00
parent dbe38ec816
commit 7b711c2317
2 changed files with 410 additions and 316 deletions

367
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,367 @@
# grafana_gen — архитектура и устройство утилиты
Утилита автоматически генерирует Grafana-дашборды и правила алертинга для клиентов WAF на основе данных из PostgreSQL и шаблонов из Git-репозитория.
---
## Обзор потока данных
```
PostgreSQL
sp_info + manual_info (UNION)
apps_settings
client_info
fetchClientsData()
map[clientTitle]ClientData
├──────────────────────────────────────┐
▼ ▼
generateSingleDashboard() generateAndSendAlerts()
│ │
▼ ▼
dashboard_template.json alert_rules_template.json
(Git-репозиторий) (Git-репозиторий)
│ │
▼ ▼
POST /api/dashboards/db POST/PUT /api/v1/provisioning/alert-rules
(Grafana API) (Grafana Provisioning API, параллельно x5)
│ │
▼ ▼
state.json ──── clients_hash state.json ──── alerts_hash
└─── alert_template_hash
```
---
## Структура файлов
| Файл | Назначение |
|---|---|
| `main.go` | Точка входа, оркестрация всего потока |
| `config.go` | Константы, структуры `Config`, `ClientData`, `DomainInfo`, `DashboardTemplate` |
| `database.go` | Подключение к PostgreSQL, `fetchClientsData()` |
| `dashboard.go` | Генерация JSON дашборда из шаблона и данных клиентов |
| `panels.go` | Построение отдельных панелей, реестр query-режимов, `applyRPSThreshold()` |
| `alerts.go` | Генерация и отправка правил алертинга, параллельный upsert |
| `grafana.go` | HTTP-клиент для Grafana API (дашборды, папки) |
| `template.go` | Загрузка `dashboard_template.json` и `alert_rules_template.json` |
| `state.go` | Чтение/запись `state.json`, обнаружение изменений |
| `git.go` | `git clone` / `git pull` шаблонов из репозитория |
| `env.go` | Загрузка `.env` файла |
| `dashboard_template.json` | JSON-шаблон дашборда (Git-репозиторий) |
| `alert_rules_template.json` | JSON-шаблон алертов (Git-репозиторий) |
---
## База данных
### Источник данных
Данные всегда берутся из **обеих** таблиц одновременно через `UNION`:
```sql
SELECT ... FROM (
SELECT sid, domain_name, aliases FROM sp_info WHERE sid IS NOT NULL
UNION
SELECT sid, domain_name, aliases FROM manual_info WHERE sid IS NOT NULL
) s
LEFT JOIN apps_settings a ON s.sid = a.l7resourceid
LEFT JOIN client_info ci ON a.client_title = ci.client_title
```
Дубликаты по SID схлопываются автоматически (`UNION` без `ALL`).
### Таблицы
**`sp_info` / `manual_info`** — список ресурсов:
- `sid` — идентификатор ресурса
- `domain_name` — основной домен
- `aliases` — JSON-массив дополнительных доменов
**`apps_settings`** — маппинг SID → клиент:
- `l7resourceid` — SID ресурса
- `client_title` — название клиента (имя тенанта)
**`client_info`** — настройки мониторинга клиента:
- `client_title` — название клиента
- `rps_limit` — порог RPS для алерта (NULL = алерт не создавать)
- `rps_commercial_limit` — коммерческий лимит RPS для синей линии на графике (NULL = 100)
- `four_hundred` — порог 4xx ошибок в % (NULL = алерт не создавать)
- `five_hundred` — порог 5xx ошибок в % (NULL = алерт не создавать)
### Пример добавления нового клиента
```sql
-- 1. Ресурс появится автоматически из sp_info
-- 2. Привязать к клиенту
INSERT INTO apps_settings (l7resourceid, client_title)
VALUES ('SID12345', 'Название клиента');
-- 3. Задать лимиты для алертов (опционально)
INSERT INTO client_info (client_title, rps_limit, rps_commercial_limit, four_hundred, five_hundred)
VALUES ('Название клиента', 1800, 1200, 20, 10);
```
---
## Шаблоны
Шаблоны хранятся в отдельном Git-репозитории и обновляются при каждом запуске через `git pull`. Изменение шаблона автоматически приводит к пересозданию дашборда и/или алертов.
### dashboard_template.json
Описывает структуру дашборда: метаданные, Grafana-переменные, строки (rows), панели.
**Ключевые секции:**
```json
{
"version": "3.1.0",
"datasource": { "type": "...", "uid": "..." },
"panels": {
"rps": { "query_mode": "bucket_logs", ... },
"status_codes": { ... },
"response_time": { ... },
"traffic_combined": { ... }
},
"layout": {
"client_panels": [...],
"overview_panels": [...],
"overview_row_title": "Обзор"
},
"templating": { ... }
}
```
**Query-режимы панелей** (`query_mode`):
| Режим | Описание |
|---|---|
| `bucket_logs` | Запрос к OpenSearch по одному SID |
| `multi_sid` | Запрос к OpenSearch по всем SID клиента |
| `combined_sids` | Объединённый запрос по SID + алиасам |
| `static` | Статические данные без запроса |
**Обзорные панели** (`overview_panels`) используют Grafana-переменные (`$SID`, `$tenant`, `$node_name`) и отображаются в свёрнутой строке «Обзор» поверх клиентских строк.
### alert_rules_template.json
Описывает шаблон для алертов.
**Структура:**
```json
{
"version": "1.3.0",
"defaults": {
"receiver": "Telegram PTAF Grafana",
"folder": "WAF - PTAF",
"group": "PTAF Grafana"
},
"rps_alert": {
"relative_time_range_from": 1800,
"annotation_summary": "Превышен порог в {rps_limit} RPS в {client_title}"
},
"static_alerts": [ ... ]
}
```
---
## Генерация дашборда
Создаётся **один дашборд** со всеми клиентами. Структура дашборда:
```
┌─────────────────────────────────────────┐
│ Grafana-переменные: tenant, SID, node │
├─────────────────────────────────────────┤
│ ▶ Обзор (свёрнутая строка) │
│ overview_status_codes │
│ overview_tenant_per_nodes │
│ overview_tenant_per_node │
│ overview_uri_top15 │
├─────────────────────────────────────────┤
│ ▼ Клиент A (развёрнутая строка) │
│ RPS | Status codes | Response time │
│ Traffic combined │
│ [панели по доменам если > 1 домена] │
├─────────────────────────────────────────┤
│ ▶ Клиент B (свёрнутая строка) │
│ ... │
└─────────────────────────────────────────┘
```
### Threshold на графике RPS
На панель RPS автоматически накладываются цветные линии из `client_info`:
| Линия | Значение | Источник |
|---|---|---|
| Синяя | `rps_commercial_limit` | `client_info.rps_commercial_limit` (NULL → 100) |
| Красная | `rps_limit` | `client_info.rps_limit` (0 → линия не рисуется) |
---
## Алерты
### Динамические алерты (per client)
Создаются для каждого клиента у которого задан соответствующий лимит в `client_info`:
| Тип | Условие | Структура запроса |
|---|---|---|
| RPS | `median(count/60) > rps_limit` | OpenSearch count → math /60 → reduce median → threshold |
| 4xx | `(errors/total)*100 > four_hundred` | 2x OpenSearch count → reduce → math % → threshold |
| 5xx | `(errors/total)*100 > five_hundred` | 2x OpenSearch count → reduce → math % → threshold |
UID каждого алерта генерируется детерминированно: `md5(client_title + ":" + type)[:8]`. При повторном запуске алерт обновляется (`PUT`), а не создаётся заново.
### Статические алерты (всегда присутствуют)
Описаны в `alert_rules_template.json` в секции `static_alerts`, не зависят от БД:
| Алерт | Условие | for |
|---|---|---|
| Use in / | диск `/` > 75% | 3m |
| Use in /var/log | диск `/var/log` > 75% | 5m |
| CPU Busy | CPU > 75% | 3m |
| RAM Busy | RAM > 75% | 3m |
| Состояние контейнеров | `docker_container_status == 0` | 2m |
| Упала Angie | `angie.service` не active | 1m |
### Параллельная отправка
Все алерты (динамические + статические) отправляются параллельно через горутины с ограничением **5 одновременных запросов** к Grafana API:
```
[RPS client1] [RPS client2] [4xx client1] [5xx client2] [static: CPU] ← 5 горутин
↓ освободился слот
[static: RAM] ...
```
---
## State-файл
Путь по умолчанию: `/var/lib/grafana_gen/state.json`
Хранит хэши для обнаружения изменений между запусками:
```json
{
"last_run": "2026-02-25T10:00:00Z",
"template_commit": "a1b2c3d4...",
"template_version": "3.1.0",
"clients_hash": "sha256...",
"client_count": 42,
"domain_count": 87,
"sids": ["SID001", "SID002", "..."],
"alerts_hash": "sha256...",
"alert_template_hash": "sha256..."
}
```
**Логика запуска:**
```
изменился clients_hash → пересоздать дашборд
изменился template_commit → пересоздать дашборд
изменился template_version → пересоздать дашборд
изменился alerts_hash → переотправить алерты (без пересоздания дашборда)
изменился alert_template_hash → переотправить алерты (без пересоздания дашборда)
ничего не изменилось → выход без действий (если не указан -force)
```
---
## Конфигурация
Приоритет разрешения параметров (от высшего к низшему):
```
1. Флаг командной строки (-grafana-url=...)
2. Переменная окружения (GRAFANA_URL=...)
3. .env файл (/etc/grafana_gen/grafana_gen.env)
4. Константа в config.go (DefaultGrafanaURL)
```
### Пути к .env файлу (перебираются по порядку)
1. `$GRAFANA_GEN_ENV_FILE` (переменная окружения)
2. `/etc/grafana_gen/grafana_gen.env`
3. `./grafana_gen.env`
4. `./.env`
### Пример .env файла
```env
# База данных
DB_USER=grafana_reader
DB_PASSWORD=secret
# Grafana
GRAFANA_URL=https://grafana.example.com
GRAFANA_API_KEY=glsa-xxxxxxxxxxxxxxxxxxxx
# Git (Gitea)
GIT_TOKEN=your-gitea-token
# Алерты
ALERTS_DATASOURCE_UID=af84zsvlp9blsa
```
---
## Флаги запуска
| Флаг | Переменная окружения | По умолчанию | Описание |
|---|---|---|---|
| `-db-host` | — | `10.100.10.8` | PostgreSQL хост |
| `-db-port` | — | `5432` | PostgreSQL порт |
| `-db-user` | `DB_USER` | — | PostgreSQL пользователь |
| `-db-password` | `DB_PASSWORD` | — | PostgreSQL пароль |
| `-db-name` | — | `waf_info` | PostgreSQL база данных |
| `-grafana-url` | `GRAFANA_URL` | — | URL Grafana |
| `-grafana-api-key` | `GRAFANA_API_KEY` | — | Grafana API ключ |
| `-grafana-folder` | `GRAFANA_FOLDER` | `WAF - Auto Generated` | Папка для дашбордов |
| `-dashboard-title` | `DASHBOARD_TITLE` | `PT AF Nodes` | Название дашборда |
| `-alerts-folder` | `ALERTS_FOLDER` | `WAF - PTAF` | Папка для алертов |
| `-alerts-receiver` | `ALERTS_RECEIVER` | `Telegram PTAF Grafana` | Получатель уведомлений |
| `-alerts-group` | `ALERTS_GROUP` | `PTAF Grafana` | Группа алертов |
| `-alerts-datasource-uid` | `ALERTS_DATASOURCE_UID` | `af84zsvlp9blsa` | UID datasource OpenSearch |
| `-git-token` | `GIT_TOKEN` | — | Gitea токен |
| `-templates-repo` | — | `svc-git.cirex.ru/...` | URL Git-репозитория шаблонов |
| `-templates-branch` | — | `master` | Ветка шаблонов |
| `-templates-path` | — | `/etc/grafana_gen/templates` | Локальный путь шаблонов |
| `-skip-git-pull` | — | `false` | Не обновлять шаблоны из Git |
| `-state-file` | — | `/var/lib/grafana_gen/state.json` | Путь к state-файлу |
| `-dry-run` | — | `false` | Не отправлять в Grafana, только логировать |
| `-force` | — | `false` | Пересоздать всё даже без изменений |
### Примеры запуска
```bash
# Обычный запуск
./grafana_gen
# Тестовый прогон без отправки в Grafana
./grafana_gen -dry-run
# Принудительное пересоздание всего
./grafana_gen -force
# Без обновления шаблонов из Git
./grafana_gen -skip-git-pull
# Явное указание всех параметров
./grafana_gen \
-db-host=10.100.10.8 \
-db-user=reader \
-db-password=secret \
-grafana-url=https://grafana.example.com \
-grafana-api-key=glsa-xxx
```

359
README.md
View file

@ -1,344 +1,71 @@
# grafana_gen — Руководство пользователя
# grafana_gen
Утилита автоматически генерирует дашборды Grafana на основе данных из PostgreSQL. При каждом запуске она читает список клиентов и их ресурсов из БД, сравнивает с предыдущим состоянием и пересоздаёт единый дашборд только при наличии изменений.
Утилита автоматической генерации Grafana-дашбордов и правил алертинга для клиентов WAF/PTAF.
---
## Что делает
## Как это работает
- Читает список клиентов и их SID из PostgreSQL (`sp_info` + `manual_info`)
- Генерирует единый дашборд с панелями на каждого клиента
- Создаёт правила алертинга: RPS, 4xx/5xx ошибки (пороги из БД), CPU, RAM, диск, контейнеры, Angie
- Обновляет шаблоны дашборда и алертов из Git-репозитория при каждом запуске
- Пропускает запуск если ничего не изменилось (отслеживает хэши)
```
PostgreSQL (test_info)
Список клиентов и ресурсов (SID, domain_name, aliases)
Проверка изменений (state.json)
↓ нет изменений → выход
Загрузка шаблона (dashboard_template.json из Git)
Генерация JSON дашборда
Grafana API → создание/обновление дашборда
```
На каждый запуск создаётся **один дашборд** со всеми клиентами. Каждый клиент отображается как свёрнутая строка (collapsed row), внутри которой — панели по каждому ресурсу.
---
## Требования
- Go 1.21+
- PostgreSQL с базой `test_info`
- Grafana с API-доступом (Service Account Token или Legacy API Key)
- Git (для загрузки шаблонов)
- Доступ к Gitea-репозиторию с шаблоном
---
## Установка и запуск
### Сборка
```bash
git clone <репозиторий утилиты>
cd grafana_gen
go build -o grafana_gen .
```
### Первый запуск
## Быстрый старт
```bash
# Минимальный запуск
./grafana_gen \
-grafana-url https://grafana.example.com \
-grafana-api-key glsa_xxxxxxxxxxxx \
-db-user waf_reader \
-db-password secret
```
-db-user=reader \
-db-password=secret \
-grafana-url=https://grafana.example.com \
-grafana-api-key=glsa-xxx
### Через .env файл (рекомендуется)
Создайте файл `/etc/grafana_gen/grafana_gen.env`:
```env
GRAFANA_URL=https://grafana.example.com
GRAFANA_API_KEY=glsa_xxxxxxxxxxxx
DB_USER=waf_reader
DB_PASSWORD=secret
GIT_TOKEN=your-gitea-token
```
Затем просто:
```bash
# Или через .env файл (приоритет ниже переменных окружения)
cp grafana_gen.env.example /etc/grafana_gen/grafana_gen.env
./grafana_gen
```
---
## Конфигурация
### Способы задать параметры (в порядке приоритета)
Параметры разрешаются в порядке приоритета:
**флаг → переменная окружения → .env файл → константа в config.go**
| Приоритет | Способ | Пример |
|-----------|--------|--------|
| 1 | Флаг командной строки | `-grafana-url https://...` |
| 2 | Переменная окружения | `export GRAFANA_URL=https://...` |
| 3 | `.env` файл | `GRAFANA_URL=https://...` |
| 4 | Константа в `config.go` | `DefaultGrafanaURL = "https://..."` |
Минимально необходимые параметры:
### Пути поиска .env файла
| Параметр | Флаг | Переменная окружения |
|---|---|---|
| PostgreSQL пользователь | `-db-user` | `DB_USER` |
| PostgreSQL пароль | `-db-password` | `DB_PASSWORD` |
| URL Grafana | `-grafana-url` | `GRAFANA_URL` |
| Grafana API ключ | `-grafana-api-key` | `GRAFANA_API_KEY` |
Утилита ищет `.env` файл в следующем порядке:
Пример `.env` файла:
1. Путь из переменной `GRAFANA_GEN_ENV_FILE`
2. `/etc/grafana_gen/grafana_gen.env`
3. `./grafana_gen.env` (рядом с бинарником)
4. `./.env`
---
## Флаги командной строки
### База данных
| Флаг | Переменная окружения | По умолчанию | Описание |
|------|---------------------|--------------|----------|
| `-db-host` | — | `10.100.10.8` | Хост PostgreSQL |
| `-db-port` | — | `5432` | Порт PostgreSQL |
| `-db-user` | `DB_USER` | — | Пользователь БД (**обязательно**) |
| `-db-password` | `DB_PASSWORD` | — | Пароль БД (**обязательно**) |
| `-db-name` | — | `test_info` | Имя базы данных |
| `-use-manual` | — | `false` | Использовать таблицу `manual_info` вместо `sp_info` |
### Grafana
| Флаг | Переменная окружения | По умолчанию | Описание |
|------|---------------------|--------------|----------|
| `-grafana-url` | `GRAFANA_URL` | — | URL Grafana (**обязательно**) |
| `-grafana-api-key` | `GRAFANA_API_KEY` | — | API-ключ Grafana (**обязательно**) |
| `-grafana-folder` | `GRAFANA_FOLDER` | `WAF - Auto Generated` | Папка в Grafana для дашборда |
| `-dashboard-title` | `DASHBOARD_TITLE` | `PT AF Nodes` | Название дашборда |
### Git / шаблоны
| Флаг | Переменная окружения | По умолчанию | Описание |
|------|---------------------|--------------|----------|
| `-git-token` | `GIT_TOKEN` | — | Токен доступа к Gitea |
| `-templates-repo` | — | *(см. config.go)* | URL Git-репозитория с шаблонами |
| `-templates-branch` | — | `master` | Ветка репозитория |
| `-templates-path` | — | `/etc/grafana_gen/templates` | Локальный путь для шаблонов |
| `-skip-git-pull` | — | `false` | Не обновлять шаблоны из Git |
### Управление запуском
| Флаг | По умолчанию | Описание |
|------|--------------|----------|
| `-dry-run` | `false` | Сгенерировать JSON, но не отправлять в Grafana |
| `-force` | `false` | Принудительная регенерация даже без изменений |
| `-state-file` | `/var/lib/grafana_gen/state.json` | Путь к файлу состояния |
---
## Приоритет параметров
Пример: если одновременно задан флаг `-grafana-api-key`, переменная `GRAFANA_API_KEY` и значение в `.env` — используется **флаг командной строки** как наиболее приоритетный.
```
Флаг CLI > Переменная окружения > .env файл > константа в config.go
```env
DB_USER=grafana_reader
DB_PASSWORD=secret
GRAFANA_URL=https://grafana.example.com
GRAFANA_API_KEY=glsa-xxxxxxxxxxxxxxxxxxxx
GIT_TOKEN=your-gitea-token
```
Это позволяет безопасно хранить секреты в `.env` и при необходимости переопределять их на лету без изменения файлов.
---
## State-файл
Утилита сохраняет состояние после каждого успешного запуска в JSON-файл (по умолчанию `/var/lib/grafana_gen/state.json`).
При следующем запуске сравниваются:
- Коммит шаблона в Git
- Версия шаблона (`version` в `dashboard_template.json`)
- SHA-256 хэш данных из БД (список клиентов, доменов, SID)
- Количество клиентов и доменов
Если ничего не изменилось — дашборд не пересоздаётся, утилита завершается с кодом 0.
```
=== No Changes Detected ===
No changes in templates or database since last run.
Skipping dashboard generation.
Use -force flag to regenerate anyway.
```
Если изменения есть — выводится подробный отчёт:
```
=== Changes Detected ===
Changes detected:
Template commit changed: abc12345 -> def67890
Database content changed
- New SIDs: [SID_001, SID_002]
```
---
## Режим dry-run
Позволяет проверить что будет сгенерировано без отправки в Grafana:
## Полезные флаги
```bash
./grafana_gen -dry-run
-dry-run # Сгенерировать и показать в логах, не отправлять в Grafana
-force # Пересоздать всё даже если изменений нет
-skip-git-pull # Не обновлять шаблоны из Git
```
В лог выводится превью JSON дашборда и итоговая статистика:
## Шаблоны
```
DRY RUN: Dashboard generated but not sent to Grafana
=== Summary ===
Clients: 12
Skipped: 0
```
Дашборд и алерты строятся по JSON-шаблонам из Git-репозитория:
---
- `dashboard_template.json` — структура и панели дашборда
- `alert_rules_template.json` — правила алертинга
## Запуск через cron
Изменение любого шаблона автоматически триггерит пересоздание при следующем запуске.
Рекомендуемый вариант — запуск каждые 15 минут:
## Подробнее
```cron
*/15 * * * * /usr/local/bin/grafana_gen >> /var/log/grafana_gen.log 2>&1
```
Утилита сама определяет нужно ли обновлять дашборд — частые запуски без изменений завершаются мгновенно.
### Systemd timer (альтернатива)
`/etc/systemd/system/grafana-gen.service`:
```ini
[Unit]
Description=Grafana Dashboard Generator
After=network.target postgresql.service
[Service]
Type=oneshot
EnvironmentFile=/etc/grafana_gen/grafana_gen.env
ExecStart=/usr/local/bin/grafana_gen
StandardOutput=journal
StandardError=journal
```
`/etc/systemd/system/grafana-gen.timer`:
```ini
[Unit]
Description=Run grafana-gen every 15 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
```
```bash
systemctl enable --now grafana-gen.timer
```
---
## Типичные сценарии
### Первое развёртывание
```bash
# 1. Создать .env
cp grafana_gen.env.example /etc/grafana_gen/grafana_gen.env
vim /etc/grafana_gen/grafana_gen.env
# 2. Проверить без отправки
./grafana_gen -dry-run
# 3. Создать дашборд
./grafana_gen
```
### Принудительное обновление после изменения шаблона
```bash
./grafana_gen -force
```
### Переключение на резервную БД
```bash
./grafana_gen -db-host 10.10.10.5
```
### Тестирование с другим шаблоном
```bash
./grafana_gen \
-templates-path /tmp/my-templates \
-skip-git-pull \
-dry-run
```
### Использование таблицы ручного ввода
```bash
./grafana_gen -use-manual
```
---
## Структура БД
Утилита читает данные из двух таблиц базы `test_info`.
### Таблица `sp_info` (основная)
| Колонка | Тип | Описание |
|---------|-----|----------|
| `sid` | text | Идентификатор ресурса |
| `domain_name` | text | Доменное имя |
| `aliases` | jsonb | JSON-массив дополнительных доменов |
### Таблица `apps_settings` (справочник клиентов)
| Колонка | Тип | Описание |
|---------|-----|----------|
| `l7resourceid` | text | SID ресурса (связь с `sp_info.sid`) |
| `client_title` | text | Название клиента |
Если `client_title` не найден — клиент группируется под именем `Unknown`.
Таблица `manual_info` имеет ту же структуру, что и `sp_info`, и используется при флаге `-use-manual` для ручного управления данными без изменения основной таблицы.
---
## Файловая система
```
/etc/grafana_gen/
├── grafana_gen.env # Конфигурация (секреты)
└── templates/ # Клонированный Git-репозиторий с шаблонами
└── dashboard_template.json
/var/lib/grafana_gen/
└── state.json # Состояние последнего запуска
/usr/local/bin/
└── grafana_gen # Бинарник утилиты
```
### Права доступа
```bash
# Директории
install -d -m 755 /etc/grafana_gen
install -d -m 755 /var/lib/grafana_gen
# .env файл — только для владельца процесса
chmod 600 /etc/grafana_gen/grafana_gen.env
```
См. [ARCHITECTURE.md](ARCHITECTURE.md).