# dashboard_template.json — Руководство по созданию шаблонов Шаблон полностью управляет структурой и содержимым генерируемого дашборда Grafana. Go-код утилиты является исполнителем инструкций из шаблона — добавление новых панелей не требует изменения кода. --- ## Структура файла ```json { "version": "3.0.0", "description": "Описание шаблона", "datasource": { ... }, "dashboard_meta": { ... }, "dashboard": { ... }, "layout": { ... }, "panels": { ... }, "queries": { ... } } ``` | Секция | Назначение | |--------|-----------| | `version` | Версия шаблона. Изменение вызывает регенерацию дашборда | | `description` | Произвольное описание, не используется в генерации | | `datasource` | Тип и UID источника данных Grafana | | `dashboard_meta` | Метаданные дашборда: UID, теги, версия | | `dashboard` | Настройки дашборда Grafana (аннотации, тайм-зона, время по умолчанию) | | `layout` | Расположение панелей: какие панели и в каком порядке показывать | | `panels` | Определения панелей: внешний вид + логика запросов | | `queries` | Переиспользуемые конфигурации запросов | --- ## Секция datasource Задаёт источник данных по умолчанию для всех панелей. ```json "datasource": { "type": "grafana-opensearch-datasource", "uid": "af84zsvlp9blsa" } ``` | Поле | Описание | |------|----------| | `type` | Тип плагина datasource в Grafana | | `uid` | UID источника данных (найти в Grafana → Connections → Data sources) | --- ## Секция dashboard_meta Метаданные создаваемого дашборда. Позволяет управлять UID и тегами без изменения Go-кода. ```json "dashboard_meta": { "uid": "pt-af-requests-auto", "tags": ["auto-generated", "waf-monitoring"], "version": 0 } ``` | Поле | Описание | |------|----------| | `uid` | Статичный UID дашборда. Именно по нему Grafana определяет, обновить существующий дашборд или создать новый. Менять с осторожностью — при смене UID будет создан новый дашборд | | `tags` | Список тегов. Используются для поиска и фильтрации в Grafana | | `version` | Начальная версия. Обычно `0` — Grafana сама увеличивает при каждом обновлении | --- ## Секция dashboard Стандартные настройки дашборда Grafana. Копируются в создаваемый дашборд как есть. ```json "dashboard": { "editable": true, "timezone": "browser", "time": {"from": "now-6h", "to": "now"}, "graphTooltip": 0, "schemaVersion": 39 } ``` Полный список доступных полей соответствует Grafana Dashboard JSON Model. Наиболее часто меняемые: | Поле | Значения | Описание | |------|----------|----------| | `timezone` | `"browser"`, `"utc"`, `"Europe/Moscow"` | Часовой пояс | | `time.from` | `"now-6h"`, `"now-24h"`, `"now-7d"` | Начало временного диапазона по умолчанию | | `time.to` | `"now"` | Конец диапазона | | `editable` | `true` / `false` | Разрешить редактирование дашборда в UI | | `graphTooltip` | `0` — отдельный, `1` — общий, `2` — зафиксированный | Режим тултипа | --- ## Секция layout Определяет какие панели генерировать для каждого клиента и каждого домена, и где их размещать. ```json "layout": { "use_collapsed_rows": true, "tenant_panels": [ ... ], "domain_panels": [ ... ] } ``` | Поле | Описание | |------|----------| | `use_collapsed_rows` | `true` — каждый клиент в свёрнутой строке (все панели внутри row). `false` — панели размещаются последовательно на дашборде | | `tenant_panels` | Панели на уровне клиента (агрегируют данные по всем доменам клиента) | | `domain_panels` | Панели на уровне домена (отдельно для каждого ресурса клиента) | ### Структура элемента layout ```json { "panel_key": "rps", "title_format": "RPS %s", "width": 12, "x_offset": 0, "y_offset": 1, "condition": null } ``` | Поле | Обязательно | Описание | |------|-------------|----------| | `panel_key` | ✅ | Ключ панели из секции `panels` | | `width` | ✅ | Ширина панели (1–24, сетка Grafana — 24 колонки) | | `x_offset` | ✅ | Позиция по горизонтали (0–23) | | `y_offset` | — | Позиция по вертикали внутри collapsed row | | `title_format` | — | Формат заголовка с плейсхолдерами | | `title_suffix` | — | Суффикс к имени домена в заголовке | | `condition` | — | Условие отображения панели | ### Позиционирование Grafana использует сетку шириной 24 единицы. Типичные варианты размещения: ``` Две панели по полэкрана: panel_1: width=12, x_offset=0 panel_2: width=12, x_offset=12 Одна широкая панель: panel: width=24, x_offset=0 Три панели: panel_1: width=8, x_offset=0 panel_2: width=8, x_offset=8 panel_3: width=8, x_offset=16 ``` --- ## Секция panels Библиотека панелей. Каждая панель описывает внешний вид (Grafana JSON) и логику запросов (`query_mode` + `query_config`). ```json "panels": { "row": { ... }, "rps": { ... }, "status_codes": { ... }, "my_new_panel": { ... } } ``` Ключ (например `"rps"`) используется в `layout` как `panel_key`. ### Структура панели ```json "my_panel": { "type": "timeseries", "description": "Описание панели", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, "fieldConfig": { ... }, "options": { ... }, "query_mode": "multi_target", "query_config": { ... } } ``` Поля делятся на две категории: **Grafana-поля** (передаются в API как есть): `type`, `description`, `gridPos`, `fieldConfig`, `options`, `transformations`, `overrides` и любые другие стандартные поля Grafana. **Служебные поля шаблона** (используются при генерации, не попадают в итоговый JSON): `query_mode`, `query_config`. > **Совет**: проще всего создать нужную панель вручную в Grafana, экспортировать дашборд через Share → Export, скопировать JSON панели и добавить к нему `query_mode` и `query_config`. ### Специальная панель row Обязательная панель для создания строк-заголовков клиентов: ```json "row": { "type": "row", "collapsed": true, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "panels": [] } ``` Не требует `query_mode`. Заголовок (`title`) устанавливается автоматически из имени клиента. --- ## Секция queries Переиспользуемые конфигурации bucket aggregation для OpenSearch/Elasticsearch. ```json "queries": { "bucket_agg": { "field": "@timestamp", "id": "2", "settings": {"interval": "1m", "trimEdges": "1"}, "type": "date_histogram" } } ``` `bucket_agg` используется автоматически во всех построителях targets как агрегация по времени. Изменение `interval` здесь влияет на разрешение всех графиков. --- ## query_mode — типы запросов `query_mode` определяет как будут построены targets панели. Выбирается в зависимости от того, что нужно отобразить. --- ### `rps` — Запросы в секунду Один count-запрос + math expression `$A / 60` для перевода из запросов/минуту в запросы/секунду. Предназначен для **tenant-уровня** (агрегация по всем SID клиента). ```json "query_mode": "rps", "query_config": { "base_query": "SID:({sids})", "alias": "{client_title} Total RPS", "expression": "$A / 60", "expression_ref_id": "RPS" } ``` | Поле | Описание | |------|----------| | `base_query` | Lucene-запрос. Плейсхолдер `{sids}` заменяется на список SID через ` OR ` | | `alias` | Имя серии на графике. Поддерживает `{client_title}` | | `expression` | Math expression Grafana. `$A` ссылается на data query | | `expression_ref_id` | refId результирующей серии | --- ### `multi_target` — Несколько фильтров Несколько самостоятельных запросов с разными условиями. Используется для статус-кодов, разбивки по любому признаку. ```json "query_mode": "multi_target", "query_config": { "base_query": "SID:{sid}", "metric": "count", "targets": [ {"alias": "2**", "query_suffix": "AND response_status_code:[200 TO 299]", "ref_id": "A"}, {"alias": "3**", "query_suffix": "AND response_status_code:[300 TO 399]", "ref_id": "B"}, {"alias": "4**", "query_suffix": "AND response_status_code:[400 TO 499]", "ref_id": "C"}, {"alias": "5**", "query_suffix": "AND response_status_code:[500 TO 599]", "ref_id": "D"} ] } ``` | Поле | Описание | |------|----------| | `base_query` | Базовый Lucene-запрос. `{sid}` заменяется на SID ресурса | | `metric` | Тип агрегации: `"count"` | | `targets[].alias` | Имя серии | | `targets[].query_suffix` | Дополнение к base_query через `AND` | | `targets[].ref_id` | Уникальный идентификатор запроса (A, B, C...) | --- ### `range_percent` — Распределение по диапазонам в % Скрытый total-запрос + запросы по диапазонам + math expressions `($X / $Total) * 100`. Используется для распределения времени ответа, размеров ответов и т.п. ```json "query_mode": "range_percent", "query_config": { "base_query": "SID:{sid}", "field_filter": "upstream_response_time_digital:[0 TO *]", "total_ref_id": "F", "range_field": "upstream_response_time_digital", "ranges": [ {"label": "0-0.3s", "from": "0", "to": "0.3", "ref_id": "A"}, {"label": "0.3-1s", "from": "0.3", "to": "1", "ref_id": "B"}, {"label": "1-3s", "from": "1", "to": "3", "ref_id": "C"}, {"label": "3-10s", "from": "3", "to": "10", "ref_id": "D"}, {"label": "10s+", "from": "10", "to": "*", "ref_id": "E"} ] } ``` | Поле | Описание | |------|----------| | `field_filter` | Фильтр для total-запроса (исключает записи без нужного поля) | | `total_ref_id` | refId total-запроса, на который делится каждый диапазон | | `range_field` | Поле OpenSearch по которому строятся диапазоны | | `ranges[].label` | Подпись на графике | | `ranges[].from` | Начало диапазона (Lucene range syntax) | | `ranges[].to` | Конец диапазона. `"*"` означает неограниченно | | `ranges[].ref_id` | refId data-запроса | Диапазоны можно добавлять/убирать не меняя Go-код. --- ### `multi_sum_expression` — Несколько метрик суммирования Несколько sum-запросов, каждый с math expression. Используется для отображения нескольких потоков трафика на одном графике. ```json "query_mode": "multi_sum_expression", "query_config": { "base_query": "SID:{sid}", "targets": [ { "alias": "Response Traffic", "field": "upstream_response_length_digital", "ref_id": "A", "expression": "$A / 60", "expression_ref_id": "ResponseTraffic" }, { "alias": "Request Traffic", "field": "upstream_bytes_sent_digital", "ref_id": "B", "expression": "$B / 60", "expression_ref_id": "RequestTraffic" } ] } ``` | Поле | Описание | |------|----------| | `targets[].alias` | Имя серии исходного запроса | | `targets[].field` | Поле OpenSearch для sum-агрегации | | `targets[].ref_id` | refId data-запроса | | `targets[].expression` | Math expression. `$A` ссылается на `ref_id` | | `targets[].expression_ref_id` | refId результирующей серии (отображается в легенде) | --- ### `sum_expression` — Одна метрика суммирования Упрощённый вариант `multi_sum_expression` для одной метрики. ```json "query_mode": "sum_expression", "query_config": { "base_query": "SID:{sid}", "field": "upstream_response_length_digital", "ref_id": "A", "expression": "$A / 60", "expression_ref_id": "Traffic" } ``` --- ### `bucket_logs` — Произвольные bucket aggregations Полный контроль над агрегациями OpenSearch. Используется для barchart, таблиц, любых нестандартных агрегаций. ```json "query_mode": "bucket_logs", "query_config": { "base_query": "log_type:access AND SID:{sid}", "bucket_aggs": [ { "field": "response_status_code", "id": "3", "settings": {"min_doc_count": "1", "order": "desc", "orderBy": "_count", "size": "10"}, "type": "terms" }, { "field": "@timestamp", "id": "2", "settings": {"interval": "1m", "trimEdges": "1"}, "type": "date_histogram" } ] } ``` | Поле | Описание | |------|----------| | `base_query` | Lucene-запрос. `{sid}` заменяется на SID ресурса | | `bucket_aggs` | Массив bucket aggregations в формате OpenSearch Grafana плагина. Порядок важен — внешняя агрегация первая | --- ## Плейсхолдеры в заголовках Заголовки панелей задаются через `title_format` или `title_suffix` в секции `layout`. ### title_format Полный контроль над заголовком. Поддерживает плейсхолдеры: | Плейсхолдер | Значение | |-------------|----------| | `{client}` | Название клиента | | `{domain}` | Доменное имя ресурса | | `{sid}` | SID ресурса | | `%s` | Название клиента (fmt.Sprintf, для обратной совместимости) | Примеры: ```json "title_format": "RPS {client}" "title_format": "RPS %s" "title_format": "{client} / {domain}" ``` ### title_suffix Суффикс, который добавляется к имени домена: `" "`. Тоже поддерживает плейсхолдеры: ```json "title_suffix": "{sid} Status Codes" // Результат: "example.com sid123 Status Codes" "title_suffix": "Traffic" // Результат: "example.com Traffic" ``` ### Приоритет Если в `layout` задан `title_format` — используется он. Если только `title_suffix` — используется суффикс. Если не задано ничего — используется `title` из определения панели в секции `panels`. --- ## Условия отображения панелей Поле `condition` в элементе layout позволяет показывать панель только при выполнении условия. ```json { "panel_key": "rps", "width": 12, "x_offset": 0, "condition": { "type": "min_domains", "value": 2 } } ``` ### Доступные типы условий #### `min_domains` — минимальное количество доменов Показывает панель только если у клиента не менее N доменов. ```json "condition": {"type": "min_domains", "value": 3} ``` #### `max_domains` — максимальное количество доменов Показывает панель только если у клиента не более N доменов. ```json "condition": {"type": "max_domains", "value": 1} ``` #### `client_name_matches` — фильтр по имени клиента Показывает панель только для клиентов, в имени которых содержится указанная строка. ```json "condition": {"type": "client_name_matches", "value": "Enterprise"} ``` Если `condition` не задан или равен `null` — панель отображается всегда. --- ## Как добавить новую панель ### Шаг 1. Определить панель в секции `panels` ```json "panels": { "blocked_requests": { "type": "timeseries", "description": "Заблокированные запросы WAF", "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, "fieldConfig": { "defaults": { "color": {"mode": "palette-classic"}, "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1 } } }, "options": { "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true} }, "query_mode": "multi_target", "query_config": { "base_query": "SID:{sid}", "metric": "count", "targets": [ {"alias": "Blocked", "query_suffix": "AND action:block", "ref_id": "A"}, {"alias": "Passed", "query_suffix": "AND action:pass", "ref_id": "B"} ] } } } ``` ### Шаг 2. Добавить в layout ```json "domain_panels": [ { "panel_key": "blocked_requests", "title_suffix": "{sid} WAF Actions", "width": 12, "x_offset": 0, "y_offset": 25 } ] ``` Этого достаточно. Пересборка бинарника не нужна. ### Если нужен принципиально новый тип запроса Если существующие `query_mode` не покрывают нужный случай — добавьте в `panels.go`: ```go // 1. Функция-строитель targets func buildMyModeTargets(cfg map[string]interface{}, sidQuery, ...) []interface{} { // логика построения targets } // 2. Регистрация в реестре (одна строка) var queryModeRegistry = map[string]targetBuilder{ // ...существующие... "my_mode": buildMyModeTargets, } ``` После этого `"query_mode": "my_mode"` становится доступен в шаблоне.