# 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 ```