grafana_gen/ARCHITECTURE.md
2026-04-16 15:21:43 +03:00

503 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# grafana_gen — архитектура и устройство утилиты
Утилита автоматически генерирует Grafana-дашборды и правила алертинга для клиентов WAF на основе данных из PostgreSQL и шаблонов из Git-репозитория.
---
## Обзор потока данных
```
PostgreSQL
sp_info + manual_info (UNION)
apps_settings
client_info
fetchClientsData()
map[clientTitle]ClientData
├──────────────────────────────────────────────────────┐
│ │
▼ ▼
generateSingleDashboard() generateAndSendAlerts() (OS)
generateVLDashboard() generateAndSendAlertsVL() (VL)
│ │
▼ ▼
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
```
Генерация дашбордов и алертов для OS и VL управляется независимо через переменные окружения `DASHBOARD_OS_ENABLED`, `DASHBOARD_VL_ENABLED`, `ALERTS_OS_ENABLED`, `ALERTS_VL_ENABLED`.
---
## Структура файлов
| Файл | Назначение |
|---|---|
| `main.go` | Точка входа, оркестрация всего потока |
| `config.go` | Константы, структуры `Config`, `ClientData`, `DomainInfo`, `DashboardTemplate` |
| `database.go` | Подключение к PostgreSQL, `fetchClientsData()` |
| `dashboard.go` | Генерация JSON дашборда (OpenSearch) из шаблона и данных клиентов |
| `dashboard_vl.go` | Генерация JSON дашборда для VictoriaLogs |
| `panels.go` | Построение панелей (OpenSearch), реестр query-режимов, `applyRPSThreshold()` |
| `panels_vl.go` | Построение панелей для VictoriaLogs, LogsQL query builders |
| `alerts.go` | Генерация и отправка правил алертинга (OpenSearch), параллельный upsert |
| `alerts_vl.go` | Генерация и отправка правил алертинга для VictoriaLogs |
| `template_validator.go` | Валидация и автоисправление критичных настроек шаблона |
| `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": "rps", "vl_interval": "1m", ... },
"rps_multi": { "query_mode": "rps_multi", "vl_interval": "1m", ... },
"status_codes": { "query_mode": "multi_target", "vl_interval": "1m", ... },
"response_time": { "query_mode": "range_percent", "vl_interval": "1m", ... },
"traffic_combined": { "query_mode": "multi_sum_expression", "vl_interval": "1m", ... },
"overview_status_codes": { "query_mode": "bucket_logs", ... },
"overview_tenant_per_nodes": { "query_mode": "bucket_logs", ... },
"overview_tenant_per_node": { "query_mode": "bucket_logs", ... },
"overview_uri_top15": { "query_mode": "bucket_logs", ... },
"overview_errors_by_container": { "query_mode": "bucket_logs", ... },
"overview_nginx_vs_angie": { "query_mode": "multi_target", ... },
"error_by_host": { "query_mode": "bucket_logs", ... },
"error_by_path": { "query_mode": "bucket_logs", ... },
"error_by_node": { "query_mode": "bucket_logs", ... }
},
"layout": {
"tenant_panels": [...],
"domain_panels": [...],
"overview_panels": [...],
"overview_row_title": "PTAF-Nginx",
"error_panels": [...],
"error_row_title": "Angie"
},
"templating": { "list": [...] }
}
```
**Query-режимы панелей** (`query_mode`):
| Режим | Описание |
|---|---|
| `rps` | RPS для одного домена (OS: count/60, VL: count/$__interval_ms*1000) |
| `rps_multi` | RPS с разбивкой по доменам клиента |
| `rps_var` | RPS с Grafana-переменной SID |
| `multi_target` | Несколько targets на одной панели (status codes, nginx vs angie) |
| `range_percent` | Процентное распределение по диапазонам (response time) |
| `sum_expression` | Сумма поля + math expression (traffic) |
| `multi_sum_expression` | Несколько sum + math expressions |
| `bucket_logs` | Группировка по полю с terms aggregation (обзорные панели) |
| `static_targets` | Статические targets без подстановки SID |
**VL-специфичные поля панелей:**
| Поле | Описание |
|---|---|
| `vl_interval` | Минимальный интервал агрегации для VL (`"1m"`, `"2m"`) |
| `vl_base_query` | LogsQL запрос вместо Lucene `base_query` для VL |
| `unknown_label` | Метка для записей с пустым полем группировки (напр. `"(unknown)"`) |
**Layout панелей:**
Каждый элемент `tenant_panels` и `domain_panels` описывает позицию панели:
```json
{
"panel_key": "status_codes",
"title_format": "Status Codes {client}",
"width": 8,
"x_offset": 0,
"condition": { "type": "min_domains", "value": 2 }
}
```
Текущая расстановка — три столбца по 8: **Status Codes | RPS | Traffic**.
**Обзорные панели** (`overview_panels`) отображаются в свёрнутой строке «PTAF-Nginx» и используют Grafana-переменные (`${SID}`, `${tenant}`, `${node_name}`, `${response_status_code}`, `${app_name}`, `${error_server}`).
**Error панели** (`error_panels`) отображаются в свёрнутой строке «Angie» и показывают данные из `log_type:angie-error-PTAF`.
### alert_rules_template.json
Описывает шаблон для алертов.
**Структура:**
```json
{
"version": "1.3.0",
"defaults": {
"receiver": "tg+mail+mattermost PTAF Grafana",
"folder": "WAF - PTAF",
"group": "PTAF Grafana"
},
"rps_alert": {
"relative_time_range_from": 1800,
"annotation_summary": "Превышен порог в {rps_limit} RPS в {client_title}"
},
"contact_point": {
"receiver_name": "tg+mail+mattermost PTAF Grafana",
"message_template": "..."
},
"static_alerts": [ ... ]
}
```
---
## Генерация дашборда
### OpenSearch дашборд
Создаётся один дашборд со всеми клиентами через `generateSingleDashboard()`.
### VictoriaLogs дашборд
Создаётся через `generateVLDashboard()` при наличии `VL_DATASOURCE_UID`. Использует те же панели и layout что и OS дашборд, но targets строятся через `queryModeRegistryVL` с LogsQL запросами.
**Особенности VL дашборда:**
- Все запросы используют `log_type:nginx-access-PTAF` (не `log_type:access`)
- `queryType: "statsRange"` для агрегирующих запросов
- `interval: "1m"` из поля `vl_interval` шаблона панели
- `timeShift: "1m"` для обрезки неполной последней минуты
- Math expressions для RPS: `$A / $__interval_ms * 1000`
- Math expressions для response time: `$ref / $total * 100`
- Transformation `renameByRegex` для очистки суффиксов в легенде
**Структура дашборда:**
```
┌─────────────────────────────────────────────────┐
│ Grafana-переменные: tenant, SID, node_name, │
│ Status Code, App Name, Error App │
├─────────────────────────────────────────────────┤
│ ▶ PTAF-Nginx (свёрнутая строка) │
│ Response_status_code SID │
│ Request Count by TENANT per nodes │
│ Request Count by TENANT per node │
│ URI top 15 │
│ Response status code by Container │
│ Nginx vs Angie Status Codes │
├─────────────────────────────────────────────────┤
│ ▶ Angie (свёрнутая строка) │
│ Errors by Apps │
│ Errors by Upstream Path (app: ${error_server})│
│ Errors by Node │
├─────────────────────────────────────────────────┤
│ ▼ Клиент A (развёрнутая строка) │
│ Status Codes | RPS | Total Traffic │
│ [domain1] Status Codes | Response Time | Traffic │
│ [domain2] ... │
├─────────────────────────────────────────────────┤
│ ▶ Клиент 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`.
**OpenSearch алерты** (`alerts.go`):
| Тип | Условие | Структура запроса |
|---|---|---|
| RPS | `median(count/60) > rps_limit` | OS count → math /60 → reduce median → threshold |
| 4xx | `(errors/total)*100 > four_hundred` | 2x OS count → reduce → math % → threshold |
| 5xx | `(errors/total)*100 > five_hundred` | 2x OS count → reduce → math % → threshold |
**VictoriaLogs алерты** (`alerts_vl.go`):
| Тип | Условие | Структура запроса |
|---|---|---|
| RPS | `median(count/60) > rps_limit` | VL statsRange (1m) → math /60 → reduce median → threshold |
| 4xx | `(errors/total)*100 > four_hundred` | 2x VL statsRange (1m) → reduce sum → math % → threshold |
| 5xx | `(errors/total)*100 > five_hundred` | 2x VL statsRange (1m) → reduce sum → math % → threshold |
VL алерты имеют `noDataState: "NoData"` — при отсутствии данных алерт не срабатывает.
UID каждого алерта генерируется детерминированно: `md5(client_title + "_vl:" + 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] ...
```
### Подавление DatasourceNoData алертов
VL алерты при отсутствии данных переходят в состояние `NoData` (не `Alerting`). Для подавления уведомлений об этом состоянии в Grafana создан Silence с матчером `alertname = DatasourceNoData`.
---
## Валидатор шаблона
`template_validator.go` запускается автоматически после загрузки шаблона и проверяет/исправляет:
- `vl_interval: "1m"` для панелей: rps, rps_multi, status_codes, response_time, traffic_combined, traffic_combined_total
- `spanNulls: true` для панели status_codes
- `vl_base_query` с `TENANT:in(${tenant:csv})` для `overview_tenant_per_nodes`
- `vl_base_query` с `node_name:in(${node_name:csv})` для `overview_tenant_per_node`
- Layout tenant_panels в три столбца по 8 (status_codes + rps/rps_multi + traffic_combined_total)
- Layout domain_panels в три столбца по 8 (status_codes + response_time + traffic_combined)
---
## 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)
```
Хэш алертов сохраняется после успешного выполнения независимо от того какие системы алертов включены (OS/VL).
---
## Конфигурация
Приоритет разрешения параметров (от высшего к низшему):
```
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
# Алерты (OpenSearch)
ALERTS_DATASOURCE_UID=af84zsvlp9blsa
# VictoriaLogs
VL_DATASOURCE_UID=efewdonokxybkf
VL_DASHBOARD_TITLE=PT AF Requests (VictoriaLogs)
VL_GRAFANA_FOLDER=WAF - PTAF
# Управление генерацией
DASHBOARD_OS_ENABLED=false
DASHBOARD_VL_ENABLED=true
ALERTS_OS_ENABLED=false
ALERTS_VL_ENABLED=true
```
---
## Флаги запуска
| Флаг | Переменная окружения | По умолчанию | Описание |
|---|---|---|---|
| `-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` | `tg+mail+mattermost PTAF Grafana` | Получатель уведомлений |
| `-alerts-group` | `ALERTS_GROUP` | `PTAF Grafana` | Группа алертов |
| `-alerts-datasource-uid` | `ALERTS_DATASOURCE_UID` | `af84zsvlp9blsa` | UID datasource OpenSearch для алертов |
| `-vl-datasource-uid` | `VL_DATASOURCE_UID` | — | UID VictoriaLogs datasource (пустая = не генерировать VL) |
| `-vl-dashboard-title` | `VL_DASHBOARD_TITLE` | `PT AF Requests (VictoriaLogs)` | Название VL дашборда |
| `-vl-grafana-folder` | `VL_GRAFANA_FOLDER` | — | Папка для VL дашборда (пустая = как у OS) |
| `-vl-dashboard-uid` | `VL_DASHBOARD_UID` | `pt-af-requests-auto-vl` | UID VL дашборда в Grafana |
| `-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` | Пересоздать всё даже без изменений |
| — | `DASHBOARD_OS_ENABLED` | `true` | Генерировать OS дашборд |
| — | `DASHBOARD_VL_ENABLED` | `true` | Генерировать VL дашборд |
| — | `ALERTS_OS_ENABLED` | `true` | Генерировать OS алерты |
| — | `ALERTS_VL_ENABLED` | `true` | Генерировать VL алерты |
### Примеры запуска
```bash
# Обычный запуск
./grafana_gen
# Тестовый прогон без отправки в Grafana
./grafana_gen -dry-run
# Принудительное пересоздание всего
./grafana_gen -force
# Без обновления шаблонов из Git
./grafana_gen -skip-git-pull
# Только VL дашборд и VL алерты
DASHBOARD_OS_ENABLED=false ALERTS_OS_ENABLED=false ./grafana_gen
# Явное указание всех параметров
./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 \
-vl-datasource-uid=efewdonokxybkf
```