# grafana_gen — архитектура и устройство утилиты Утилита автоматически генерирует Grafana-дашборды и правила алертинга для клиентов WAF на основе данных из PostgreSQL и шаблонов из Git-репозитория. --- ## Обзор потока данных ``` PostgreSQL sp_info + manual_info (UNION) apps_settings client_info (waf_provider IS NOT NULL) │ ▼ fetchClientsData() map[clientTitle]ClientData{WafProvider} │ ├──────────────────────────────────────────────────────────────────┐ │ │ spikClients (waf_provider=spik) spClients (waf_provider=sp) │ │ ├──────────────────────┐ ┌──────────────────────────┤ │ │ │ │ ▼ ▼ ▼ ▼ generateVLDashboard() generateAndSendAlertsVL() generateVLDashboard() generateAndSendAlertsVL() (SPIK дашборд) (группа: PTAF Grafana) (SP дашборд) (группа: SP PTAF Grafana) (receiver: tg+mail+...) (receiver: For_SP) │ │ │ │ ▼ └────────────────┘ │ dashboard_template.json activeUIDs (spik+SP+static) │ │ │ │ ▼ ▼ │ POST /api/dashboards/db deleteObsoleteAlerts() POST /api/dashboards/db state.json ── alerts_hash └── alert_template_hash ``` Генерация дашбордов и алертов управляется независимо через переменные окружения `DASHBOARD_OS_ENABLED`, `DASHBOARD_VL_ENABLED`, `ALERTS_OS_ENABLED`, `ALERTS_VL_ENABLED`. --- ## Структура файлов | Файл | Назначение | |---|---| | `main.go` | Точка входа, оркестрация всего потока, разделение клиентов по `waf_provider` | | `config.go` | Константы, структуры `Config`, `ClientData`, `DomainInfo`, `DashboardTemplate` | | `database.go` | Подключение к PostgreSQL, `fetchClientsData()`, чтение `waf_provider` | | `dashboard.go` | Генерация JSON дашборда (OpenSearch) из шаблона и данных клиентов | | `dashboard_vl.go` | Генерация JSON дашборда для VictoriaLogs | | `panels.go` | Построение панелей (OpenSearch), реестр query-режимов, `applyRPSThreshold()` | | `panels_vl.go` | Построение панелей для VictoriaLogs, LogsQL query builders | | `alerts.go` | Генерация и отправка правил алертинга (OpenSearch), `deleteObsoleteAlerts()` | | `alerts_vl.go` | Генерация и отправка правил алертинга для VictoriaLogs, возвращает список активных UID | | `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`. Читаются только клиенты у которых `waf_provider IS NOT NULL`: ```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 WHERE ci.waf_provider IS NOT NULL ``` Дубликаты по 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 = алерт не создавать) - `waf_provider` — тип провайдера WAF, определяет в какой дашборд и группу алертов попадает клиент: - `spik` — основной VL дашборд + алерты в группе `PTAF Grafana` - `sp` — отдельный SP дашборд + алерты в группе `SP PTAF Grafana` с contact point `For_SP` - `NULL` — клиент игнорируется полностью ### Разделение клиентов по waf_provider После загрузки данных `main.go` разделяет клиентов на две группы: ```go spikClients := map[string]ClientData{} // waf_provider = "spik" spClients := map[string]ClientData{} // waf_provider = "sp" ``` Хэш для обнаружения изменений алертов считается только от `spikClients`. ### Пример добавления нового клиента ```sql -- 1. Ресурс появится автоматически из sp_info (или добавить в manual_info) -- 2. Привязать к клиенту INSERT INTO apps_settings (l7resourceid, client_title) VALUES ('SID12345', 'Название клиента'); -- 3. Задать провайдера и лимиты INSERT INTO client_info (client_title, waf_provider, rps_limit, rps_commercial_limit, four_hundred, five_hundred) VALUES ('Название клиента', 'spik', 1800, 1200, 20, 10); -- или для SP клиента: waf_provider = 'sp' ``` --- ## Шаблоны Шаблоны хранятся в отдельном 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()` для `spikClients`. ### VictoriaLogs дашборды Генерируются через `generateVLDashboard()` при наличии `VL_DATASOURCE_UID`: | Дашборд | Клиенты | UID | Папка | |---|---|---|---| | `PT AF Requests (VictoriaLogs)` | `spikClients` | `pt-af-requests-auto-vl` | `WAF - PTAF` | | `SP PT AF Requests (VictoriaLogs)` | `spClients` | `pt-af-requests-auto-sp` | `WAF - PTAF` | SP дашборд генерируется только если `SP_DASHBOARD_TITLE` задан и есть клиенты с `waf_provider=sp`. **Особенности VL дашборда:** - Все запросы используют `log_type:nginx-access-PTAF` (не `log_type:access`) - `queryType: "statsRange"` для агрегирующих запросов - `interval: "1m"` из поля `vl_interval` шаблона панели - `timeShift: "1m"` для обрезки неполной последней минуты - Math expressions для RPS: `$A / $__interval_ms * 1000` **Структура дашборда:** ``` ┌─────────────────────────────────────────────────┐ │ Grafana-переменные: tenant, SID, node_name, │ │ Status Code, App Name, Error App │ ├─────────────────────────────────────────────────┤ │ ▶ PTAF-Nginx (свёрнутая строка) │ │ Response_status_code SID │ │ Request Count by TENANT per nodes/node │ │ URI top 15 │ │ Response status code by Container │ │ Nginx vs Angie Status Codes │ ├─────────────────────────────────────────────────┤ │ ▶ Angie (свёрнутая строка) │ │ Errors by Apps │ │ Errors by Upstream Path │ │ Errors by Node │ ├─────────────────────────────────────────────────┤ │ ▼ Клиент A │ │ Status Codes | RPS | Total Traffic │ │ [domain1] Status Codes | Response Time | Traffic │ ├─────────────────────────────────────────────────┤ │ ▶ Клиент B ... │ └─────────────────────────────────────────────────┘ ``` --- ## Алерты ### Динамические алерты (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`), а не создаётся заново. **Разделение по waf_provider:** | Клиенты | Группа алертов | Contact point | Папка | |---|---|---|---| | `spikClients` | `PTAF Grafana` | `tg+mail+mattermost PTAF Grafana` | `WAF - PTAF` | | `spClients` | `SP PTAF Grafana` | `For_SP` | `WAF - PTAF` | ### Статические алерты (всегда присутствуют) Описаны в `alert_rules_template.json`, не зависят от БД. Отправляются в группе `[SYS] PTAF Grafana` первыми (до динамических): | Алерт | Условие | for | |---|---|---| | `[SYS] Use in / 75%` | диск `/` > 75% | 3m | | `[SYS] Use in / 90%` | диск `/` > 90% | 1m | | `[SYS] Use in /var/log 75%` | диск `/var/log` > 75% | 5m | | `[SYS] Use in /var/log 90%` | диск `/var/log` > 90% | 1m | | `[SYS] CPU Busy 75%` | CPU > 75% | 3m | | `[SYS] CPU Busy 90%` | CPU > 90% | 1m | | `[SYS] RAM Busy 75%` | RAM > 75% | 3m | | `[SYS] RAM Busy 90%` | RAM > 90% | 1m | | `[SYS] Состояние контейнеров` | контейнер упал | 2m | | `[SYS] Упала Angie` | `angie.service` не active | 1m | Если `ALERTS_OS_ENABLED=false` — статические алерты отправляются через VL путь. ### Удаление устаревших алертов После каждой генерации `deleteObsoleteAlerts()` получает все алерты из папки `WAF - PTAF` и удаляет те, чьи UID не входят в актуальный набор (spik + SP + статические). Это автоматически удаляет алерты клиентов которые были удалены из БД или у которых изменился `waf_provider`. ### Параллельная отправка Все алерты отправляются параллельно через горутины с ограничением **5 одновременных запросов** к Grafana API. ### Подавление DatasourceNoData алертов VL алерты при отсутствии данных переходят в состояние `NoData`. Для подавления уведомлений в Grafana создан Silence с матчером `alertname = DatasourceNoData`. --- ## Валидатор шаблона `template_validator.go` запускается автоматически после загрузки шаблона и проверяет/исправляет: - `vl_interval: "1m"` для панелей: rps, rps_multi, status_codes, response_time, traffic_combined, traffic_combined_total - `spanNulls: 180000` для панели 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) ``` `alerts_hash` считается только от `spikClients` и сохраняется после успешной отправки. --- ## Конфигурация Приоритет разрешения параметров (от высшего к низшему): ``` 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 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 # SP дашборд и алерты SP_DASHBOARD_TITLE=SP PT AF Requests (VictoriaLogs) SP_DASHBOARD_UID=pt-af-requests-auto-sp SP_ALERTS_GROUP=SP PTAF Grafana SP_ALERTS_RECEIVER=For_SP # Управление генерацией 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` | Папка для OS дашборда | | `-dashboard-title` | `DASHBOARD_TITLE` | `PT AF Requests` | Название OS дашборда | | `-alerts-folder` | `ALERTS_FOLDER` | `WAF - PTAF` | Папка для алертов | | `-alerts-receiver` | `ALERTS_RECEIVER` | `tg+mail+mattermost PTAF Grafana` | Contact point для SPIK алертов | | `-alerts-group` | `ALERTS_GROUP` | `PTAF Grafana` | Группа алертов для SPIK | | `-alerts-datasource-uid` | `ALERTS_DATASOURCE_UID` | `af84zsvlp9blsa` | UID datasource OpenSearch для алертов | | `-vl-datasource-uid` | `VL_DATASOURCE_UID` | — | UID VictoriaLogs datasource | | `-vl-dashboard-title` | `VL_DASHBOARD_TITLE` | `PT AF Requests (VictoriaLogs)` | Название SPIK VL дашборда | | `-vl-grafana-folder` | `VL_GRAFANA_FOLDER` | — | Папка для SPIK VL дашборда | | `-vl-dashboard-uid` | `VL_DASHBOARD_UID` | `pt-af-requests-auto-vl` | UID SPIK VL дашборда | | `-sp-dashboard-title` | `SP_DASHBOARD_TITLE` | `SP PT AF Requests (VictoriaLogs)` | Название SP дашборда | | `-sp-dashboard-uid` | `SP_DASHBOARD_UID` | `pt-af-requests-auto-sp` | UID SP дашборда | | `-sp-grafana-folder` | `SP_GRAFANA_FOLDER` | — | Папка для SP дашборда | | — | `SP_ALERTS_GROUP` | `SP PTAF Grafana` | Группа алертов для SP клиентов | | — | `SP_ALERTS_RECEIVER` | `For_SP` | Contact point для SP алертов | | `-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 ```