grafana_gen/ARCHITECTURE.md

29 KiB
Executable file
Raw Permalink Blame History

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 point For_SP
    • NULL — клиент игнорируется полностью
  • 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.jsoncontact_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_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

{
  "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 файла

# База данных
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