29 KiB
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:
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 → клиент, пороги ошибок per-SID:
l7resourceid— SID ресурсаclient_title— название клиента (имя тенанта)four_hundred— порог 4xx ошибок в % (default 20)five_hundred— порог 5xx ошибок в % (default 10)ptaf_fallback_code_alert_count— порог WAF block алерта в штуках (default 1)
client_info — настройки мониторинга клиента:
client_title— название клиентаrps_limit— порог RPS для алерта (NULL = алерт не создавать)rps_commercial_limit— коммерческий лимит RPS для синей линии на графике (NULL = 100)waf_provider— тип провайдера WAF, определяет в какой дашборд и группу алертов попадает клиент:spik— основной VL дашборд + алерты в группеPTAF Grafana {client_title}sp— отдельный SP дашборд + алерты в группеSP PTAF Grafana {client_title}с contact pointFor_SPNULL— клиент игнорируется полностью
ptaf_fallback_code— HTTP код который PTAF возвращает при блокировке (напр.418,403).passили NULL = WAF block алерт не создавать
Разделение клиентов по waf_provider
После загрузки данных main.go разделяет клиентов на две группы:
spikClients := map[string]ClientData{} // waf_provider = "spik"
spClients := map[string]ClientData{} // waf_provider = "sp"
Хэш для обнаружения изменений алертов считается только от spikClients.
Пример добавления нового клиента
-- 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), панели.
Ключевые секции:
{
"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 описывает позицию панели:
{
"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
Описывает шаблон для алертов.
Структура:
{
"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 response | (errors/total)*100 > limit_4xx |
2x OS count по response_status_code → reduce → math % → threshold |
| 4xx upstream | (errors/total)*100 > limit_4xx |
2x OS count по upstream_status_code → reduce → math % → threshold |
| 5xx response | (errors/total)*100 > limit_5xx |
2x OS count по response_status_code → reduce → math % → threshold |
| 5xx upstream | (errors/total)*100 > limit_5xx |
2x OS count по upstream_status_code → reduce → math % → threshold |
| WAF block | count > ptaf_fallback_code_alert_count |
OS count по коду из ptaf_fallback_code → reduce last → threshold |
Алерты 4xx/5xx и WAF block создаются по одному на каждый SID.
VictoriaLogs алерты (alerts_vl.go):
| Тип | Условие | Структура запроса |
|---|---|---|
| RPS | median(count/60) > rps_limit |
VL statsRange (1m) → math /60 → reduce median → threshold |
| 4xx response | (errors/total)*100 > limit_4xx |
2x VL statsRange по response_status_code → reduce sum → math % → threshold |
| 4xx upstream | (errors/total)*100 > limit_4xx |
2x VL statsRange по upstream_status_code → reduce sum → math % → threshold |
| 5xx response | (errors/total)*100 > limit_5xx |
2x VL statsRange по response_status_code → reduce sum → math % → threshold |
| 5xx upstream | (errors/total)*100 > limit_5xx |
2x VL statsRange по upstream_status_code → reduce sum → math % → threshold |
| WAF block | count > ptaf_fallback_code_alert_count |
VL statsRange по коду из ptaf_fallback_code → reduce last → threshold |
VL алерты имеют noDataState: "NoData" — при отсутствии данных алерт не срабатывает.
Алерты 4xx/5xx и WAF block создаются по одному на каждый SID (домен) клиента. RPS алерт — один на всего клиента (все SID суммируются).
UID каждого алерта генерируется детерминированно от client_title + SID + type. При повторном запуске алерт обновляется (PUT), а не создаётся заново.
Разделение по waf_provider:
| Клиенты | Группа алертов | Contact point | Папка |
|---|---|---|---|
spikClients |
PTAF Grafana {client_title} |
tg+mail+mattermost PTAF Grafana |
WAF - PTAF |
spClients |
SP PTAF Grafana {client_title} |
For_SP |
WAF - PTAF |
Группировка по тенанту позволяет видеть алерты в Grafana в разделе WAF - PTAF → PTAF Grafana nloto.
Статические алерты (всегда присутствуют)
Описаны в 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.
Параллельная отправка
Все алерты отправляются параллельно через горутины с ограничением 20 одновременных запросов к Grafana API.
Обновление шаблона contact point
После каждой отправки алертов upsertContactPoint() автоматически обновляет message во всех Telegram интеграциях contact point. Если ALERTS_OS_ENABLED=false — обновление происходит через VL путь. Шаблон берётся из alert_rules_template.json → contact_point.message_template.
Формат уведомлений:
Firing:
PTAF alerts:
🔴 nloto / nationallottery.ru (SID:10307) — 4xx response ошибок 33.45% за 3m.
Порог сработки >20% для response_status_code (ответ от PTAF, не от origin)
Время: 2026-04-27 10:00:00
Resolved:
PTAF alerts:
✅ nloto / nationallottery.ru (SID:10307) — 4xx response ошибок трафика.
Текущее значение 12.30% для response_status_code (ответ от PTAF, не от origin)
Время: 2026-04-27 10:15:00
WAF block (🟡 — severity=warning):
PTAF alerts:
🟡 gorodpay / stage-dev.com (SID:12092) — 418 WAF ошибок 3 шт. за 1m.
Порог сработки >1 штуки для response_status_code (ответ от PTAF, не от origin)
Время: 2026-04-27 14:50:00
---
Проверить логи docker, процитировать ошибку и эскалировать на аналитика.
Подавление DatasourceNoData алертов
VL алерты при отсутствии данных переходят в состояние NoData. Для подавления уведомлений в Grafana создан Silence с матчером alertname = DatasourceNoData.
Валидатор шаблона
template_validator.go запускается автоматически после загрузки шаблона и проверяет/исправляет:
vl_interval: "1m"для панелей: rps, rps_multi, status_codes, response_time, traffic_combined, traffic_combined_totalspanNulls: 180000для панели status_codesvl_base_queryсTENANT:in(${tenant:csv})дляoverview_tenant_per_nodesvl_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
{
"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 файлу (перебираются по порядку)
$GRAFANA_GEN_ENV_FILE/etc/grafana_gen/grafana_gen.env./grafana_gen.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 алерты |
Примеры запуска
# Обычный запуск
./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