568 lines
20 KiB
Markdown
Executable file
568 lines
20 KiB
Markdown
Executable file
# 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
|
||
|
||
Суффикс, который добавляется к имени домена: `"<domain> <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"` становится доступен в шаблоне.
|