From 505de27f14d8f48f2fe7e3d23a5eea6b94a2f6c9 Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Tue, 24 Feb 2026 12:54:28 +0300 Subject: [PATCH] Work version --- README.md | 568 ++++++++++++++++++++++++++++++++++++++++ dashboard_template.json | 335 ++++++++++++++++++++++++ 2 files changed, 903 insertions(+) create mode 100644 README.md create mode 100644 dashboard_template.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..f90d70a --- /dev/null +++ b/README.md @@ -0,0 +1,568 @@ +# 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"` становится доступен в шаблоне. diff --git a/dashboard_template.json b/dashboard_template.json new file mode 100644 index 0000000..d5533f6 --- /dev/null +++ b/dashboard_template.json @@ -0,0 +1,335 @@ +{ + "version": "3.0.0", + "description": "Data-driven template - panels fully describe their query logic", + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "af84zsvlp9blsa" + }, + "dashboard_meta": { + "uid": "pt-af-requests-auto", + "tags": ["auto-generated", "waf-monitoring"], + "version": 0 + }, + "dashboard": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": {"type": "grafana", "uid": "-- Grafana --"}, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "schemaVersion": 39, + "timezone": "browser", + "time": {"from": "now-6h", "to": "now"}, + "timepicker": {}, + "weekStart": "" + }, + "layout": { + "use_collapsed_rows": true, + "tenant_panels": [ + { + "panel_key": "rps", + "title_format": "RPS %s", + "width": 12, + "x_offset": 0, + "y_offset": 1 + }, + { + "panel_key": "status_codes", + "title_format": "Status Codes %s", + "width": 12, + "x_offset": 12, + "y_offset": 1 + } + ], + "domain_panels": [ + { + "panel_key": "status_codes", + "title_suffix": "{sid} Status Codes", + "width": 12, + "x_offset": 0, + "y_offset": 9 + }, + { + "panel_key": "response_time", + "title_suffix": "{sid} Response Time", + "width": 12, + "x_offset": 12, + "y_offset": 9 + }, + { + "panel_key": "traffic_combined", + "title_suffix": "{sid} Traffic", + "width": 24, + "x_offset": 0, + "y_offset": 17 + } + ] + }, + "panels": { + "row": { + "type": "row", + "collapsed": true, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, + "panels": [] + }, + + "rps": { + "type": "timeseries", + "description": "Requests per second", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "axisBorderShow": false, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "lineInterpolation": "smooth", + "lineWidth": 1, + "showPoints": "never", + "thresholdsStyle": {"mode": "line"} + }, + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": null}, + {"color": "dark-red", "value": 1800} + ] + } + } + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "tooltip": {"mode": "single"} + }, + "query_mode": "rps", + "query_config": { + "base_query": "SID:({sids})", + "metric": "count", + "alias": "{client_title} Total RPS", + "expression": "$A / 60", + "expression_ref_id": "RPS" + } + }, + + "status_codes": { + "type": "timeseries", + "description": "HTTP status codes", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineInterpolation": "smooth", + "lineWidth": 1, + "showPoints": "never" + } + }, + "overrides": [ + {"matcher": {"id": "byName", "options": "2**"}, "properties": [{"id": "color", "value": {"fixedColor": "green", "mode": "fixed"}}]}, + {"matcher": {"id": "byName", "options": "3**"}, "properties": [{"id": "color", "value": {"fixedColor": "blue", "mode": "fixed"}}]}, + {"matcher": {"id": "byName", "options": "4**"}, "properties": [{"id": "color", "value": {"fixedColor": "yellow", "mode": "fixed"}}]}, + {"matcher": {"id": "byName", "options": "5**"}, "properties": [{"id": "color", "value": {"fixedColor": "red", "mode": "fixed"}}]} + ] + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "tooltip": {"mode": "single"} + }, + "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"} + ] + } + }, + + "response_time": { + "type": "timeseries", + "description": "Response time distribution", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 1}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "drawStyle": "line", + "fillOpacity": 30, + "lineWidth": 0, + "stacking": {"mode": "normal"} + }, + "max": 100, + "min": 0, + "unit": "percent" + } + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "tooltip": {"mode": "single"} + }, + "query_mode": "range_percent", + "query_config": { + "base_query": "SID:{sid}", + "field_filter": "upstream_response_time_digital:[0 TO *]", + "metric": "count", + "total_ref_id": "F", + "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"} + ], + "range_field": "upstream_response_time_digital" + } + }, + + "traffic_combined": { + "type": "timeseries", + "description": "Response and Request Traffic", + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 17}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineInterpolation": "smooth", + "lineWidth": 1, + "showPoints": "never", + "spanNulls": 180000 + }, + "unit": "Kbits" + }, + "overrides": [ + { + "matcher": {"id": "byRegexp", "options": "/.*Response.*/"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "green", "mode": "fixed"}} + ] + }, + { + "matcher": {"id": "byRegexp", "options": "/.*Request.*/"}, + "properties": [ + {"id": "color", "value": {"fixedColor": "blue", "mode": "fixed"}} + ] + } + ] + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "tooltip": {"mode": "single"} + }, + "query_mode": "multi_sum_expression", + "query_config": { + "base_query": "SID:{sid}", + "targets": [ + { + "alias": "Response Traffic", + "metric": "sum", + "field": "upstream_response_length_digital", + "ref_id": "A", + "expression": "$A / 60", + "expression_ref_id": "ResponseTraffic" + }, + { + "alias": "Request Traffic", + "metric": "sum", + "field": "upstream_bytes_sent_digital", + "ref_id": "B", + "expression": "$B / 60", + "expression_ref_id": "RequestTraffic" + } + ] + } + }, + + "traffic": { + "type": "timeseries", + "description": "Traffic", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 9}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineInterpolation": "smooth", + "lineWidth": 1, + "showPoints": "never", + "spanNulls": 180000 + }, + "unit": "Kbits" + } + }, + "query_mode": "sum_expression", + "query_config": { + "base_query": "SID:{sid}", + "metric": "sum", + "field": "upstream_response_length_digital", + "ref_id": "A", + "expression": "($A / 60)", + "expression_ref_id": "Traffic" + } + }, + + "status_codes_barchart": { + "type": "barchart", + "description": "Status codes distribution", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 9}, + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"} + } + }, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true} + }, + "query_mode": "bucket_logs", + "query_config": { + "base_query": "log_type:access AND SID:{sid}", + "metric": "count", + "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" + } + ] + } + } + }, + "queries": { + "bucket_agg": { + "field": "@timestamp", + "id": "2", + "settings": {"interval": "1m", "trimEdges": "1"}, + "type": "date_histogram" + } + } +} \ No newline at end of file