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