From 6e701d4fc63a03a1f098218cfc30dc817854004a Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Sat, 8 Aug 2026 02:30:53 +0300 Subject: [PATCH] Refine Grafana traffic dashboard and documentation --- README.md | 4 +- monitoring/grafana/harbor-gateway.json | 524 +++++++++++++++-------- src/shared/versions.js | 4 +- src/web/instructions.js | 2 +- test/web/prometheus-instructions.test.js | 43 +- 5 files changed, 391 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index cd4501f..f052124 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,9 @@ scrape_configs: - targets: [":3456"] ``` -`harbor_traffic_bytes_total` содержит общий накопленный объём по источникам Gateway/Proxy. `harbor_device_traffic_bytes_total` содержит upload/download по стабильному `device_id`; пользовательское название и текущий IP находятся в `harbor_device_info`. `harbor_device_domain_traffic_bytes_total` добавляет наблюдённые домен, сервис, источник и направление для каждого устройства. Dashboard показывает top services за выбранный период и таблицу доменов с фильтром по устройствам. +`harbor_traffic_bytes_total` содержит общий накопленный объём по источникам Gateway/Proxy. `harbor_device_traffic_bytes_total` содержит upload/download по стабильному `device_id`; пользовательское название и текущий IP находятся в `harbor_device_info`. `harbor_device_domain_traffic_bytes_total` добавляет наблюдённые домен, сервис, источник и направление для каждого устройства. + +Dashboard отделяет текущую скорость от значений за выбранный период и накопленных счётчиков. Top-10 устройств за период отсортирован по убыванию и служит переходом к детализации. Один общий фильтр по стабильному `device_id` управляет графиками устройства, сервисами и доменами; в списке он подписан как `name · ip`. Нулевые ranking-строки скрыты, а domain table показывает только сервис, домен и трафик. Автообновление настроено на 30 секунд; freshness предупреждает после 60 секунд и считает данные устаревшими после 120 секунд. Domain counters снимаются с активных соединений sing-box раз в 2 секунды и хранятся в памяти dataplane до его перезапуска; историю и retention хранит Prometheus. YouTube и OpenAI / ChatGPT объединяются по известным связанным доменам в label `service`, остальные значения сохраняют домен как имя сервиса. Новые domain series сверх process limit складываются в `_other`. В метрики не входит физический трафик вне Harbor, устройства с policy Direct, ECH/IP-only соединения, соединения между двумя снимками и байты после последнего снимка перед закрытием или quota провайдера. diff --git a/monitoring/grafana/harbor-gateway.json b/monitoring/grafana/harbor-gateway.json index 92eeafe..c2136f9 100644 --- a/monitoring/grafana/harbor-gateway.json +++ b/monitoring/grafana/harbor-gateway.json @@ -12,7 +12,7 @@ "annotations": { "list": [] }, - "description": "Harbor-accounted Gateway and Proxy traffic. Direct and physical device traffic outside Harbor is not included.", + "description": "Трафик, учтённый Harbor Gateway и Proxy. Физический и Direct-трафик вне Harbor не включён.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, @@ -20,66 +20,31 @@ "links": [], "panels": [ { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "continuous-GrYlRd" - }, - "decimals": 1, - "unit": "bytes" - }, - "overrides": [] - }, + "collapsed": false, "gridPos": { - "h": 6, - "w": 6, + "h": 1, + "w": 24, "x": 0, "y": 0 }, - "id": 1, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "textMode": "auto", - "wideLayout": true - }, - "targets": [ - { - "editorMode": "code", - "expr": "sum(harbor_traffic_bytes_total)", - "instant": true, - "legendFormat": "Учтено Harbor", - "range": false, - "refId": "A" - } - ], - "title": "Всего учтено Harbor", - "type": "stat" + "id": 10, + "panels": [], + "title": "Обзор", + "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Сколько секунд прошло с последнего успешного обновления. До 60 секунд — штатно, 60–119 — задержка, от 120 — данные устарели.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 0, + "noValue": "Нет данных", "thresholds": { "mode": "absolute", "steps": [ @@ -89,7 +54,7 @@ }, { "color": "orange", - "value": 45 + "value": 60 }, { "color": "red", @@ -104,8 +69,8 @@ "gridPos": { "h": 6, "w": 6, - "x": 6, - "y": 0 + "x": 0, + "y": 1 }, "id": 2, "options": { @@ -141,7 +106,7 @@ "refId": "B" } ], - "title": "Свежесть счётчиков", + "title": "Свежесть данных сейчас", "type": "stat" }, { @@ -149,6 +114,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Текущая суммарная скорость всех устройств, сглаженная за последние 5 минут.", "fieldConfig": { "defaults": { "color": { @@ -156,7 +122,7 @@ }, "custom": { "drawStyle": "line", - "fillOpacity": 12, + "fillOpacity": 10, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 4, @@ -168,15 +134,47 @@ } }, "decimals": 1, + "noValue": "Нет данных", "unit": "Bps" }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Скачивание" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Отправка" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] }, "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 0 + "h": 6, + "w": 18, + "x": 6, + "y": 1 }, "id": 3, "options": { @@ -197,13 +195,20 @@ "targets": [ { "editorMode": "code", - "expr": "sum by (source) (rate(harbor_traffic_bytes_total[5m]))", - "legendFormat": "{{source}}", + "expr": "sum(rate(harbor_device_traffic_bytes_total{direction=\"download\"}[5m]))", + "legendFormat": "Скачивание", "range": true, "refId": "A" + }, + { + "editorMode": "code", + "expr": "sum(rate(harbor_device_traffic_bytes_total{direction=\"upload\"}[5m]))", + "legendFormat": "Отправка", + "range": true, + "refId": "B" } ], - "title": "Общая скорость за 5 минут", + "title": "Скорость сейчас, средняя за 5 минут", "type": "timeseries" }, { @@ -211,59 +216,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "decimals": 1, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 9 - }, - "id": 4, - "options": { - "displayMode": "gradient", - "maxVizHeight": 300, - "minVizHeight": 16, - "minVizWidth": 8, - "namePlacement": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "sizing": "auto", - "valueMode": "color" - }, - "targets": [ - { - "editorMode": "code", - "expr": "sum by (source) (harbor_traffic_bytes_total)", - "instant": true, - "legendFormat": "{{source}}", - "range": false, - "refId": "A" - } - ], - "title": "Объём по источникам", - "type": "bargauge" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, + "description": "Устройства с наибольшим учтённым трафиком в текущем диапазоне времени. Нажмите название, чтобы выбрать устройство в детализации ниже.", "fieldConfig": { "defaults": { "color": { @@ -277,15 +230,47 @@ "inspect": false }, "decimals": 1, + "noValue": "Нет данных", "unit": "bytes" }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "device_id" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": false, + "title": "Выбрать устройство", + "url": "${__url_time_range}&var-device_id=${__data.fields[\"device_id\"]}" + } + ] + } + ] + } + ] }, "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 9 + "h": 9, + "w": 24, + "x": 0, + "y": 7 }, "id": 5, "options": { @@ -299,12 +284,18 @@ ], "show": false }, - "showHeader": true + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "За период" + } + ] }, "targets": [ { "editorMode": "code", - "expr": "sum by (device_id) (harbor_device_traffic_bytes_total) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info)", + "expr": "topk(10, (sum by (device_id) (increase(harbor_device_traffic_bytes_total[$__range])) > 0) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info))", "format": "table", "instant": true, "legendFormat": "{{name}} · {{ip}}", @@ -312,22 +303,22 @@ "refId": "A" } ], - "title": "Устройства по объёму", + "title": "Топ-10 устройств за выбранный период", "transformations": [ { "id": "organize", "options": { "excludeByName": { - "Time": true, - "device_id": true + "Time": true }, "indexByName": { "name": 0, "ip": 1, - "Value": 2 + "Value": 2, + "device_id": 3 }, "renameByName": { - "Value": "Учтено", + "Value": "За период", "ip": "IP", "name": "Устройство" } @@ -336,11 +327,25 @@ ], "type": "table" }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 11, + "panels": [], + "title": "Устройство", + "type": "row" + }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Скорость одного выбранного устройства по Gateway/Proxy и upload/download, средняя за последние 5 минут.", "fieldConfig": { "defaults": { "color": { @@ -360,6 +365,7 @@ } }, "decimals": 1, + "noValue": "Нет данных за период", "unit": "Bps" }, "overrides": [] @@ -368,7 +374,7 @@ "h": 9, "w": 24, "x": 0, - "y": 16 + "y": 17 }, "id": 6, "options": { @@ -390,13 +396,13 @@ "targets": [ { "editorMode": "code", - "expr": "sum by (device_id, source, direction) (rate(harbor_device_traffic_bytes_total[5m])) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info{name=~\"$device\"})", - "legendFormat": "{{name}} · {{source}} · {{direction}}", + "expr": "sum by (source, direction) (rate(harbor_device_traffic_bytes_total{device_id=\"$device_id\"}[5m]))", + "legendFormat": "{{source}} · {{direction}}", "range": true, "refId": "A" } ], - "title": "Скорость выбранных устройств", + "title": "${device_id:text}: скорость сейчас", "type": "timeseries" }, { @@ -404,12 +410,14 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Накопленные счётчики выбранного устройства за всё время хранения Harbor. Нулевые серии скрыты.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "decimals": 1, + "noValue": "Трафик не учтён", "unit": "bytes" }, "overrides": [] @@ -418,11 +426,11 @@ "h": 7, "w": 24, "x": 0, - "y": 25 + "y": 26 }, "id": 7, "options": { - "displayMode": "gradient", + "displayMode": "basic", "maxVizHeight": 300, "minVizHeight": 16, "minVizWidth": 8, @@ -435,47 +443,62 @@ "fields": "", "values": false }, - "showUnfilled": true, + "showUnfilled": false, "sizing": "auto", - "valueMode": "color" + "valueMode": "text" }, "targets": [ { "editorMode": "code", - "expr": "sum by (device_id, source, direction) (harbor_device_traffic_bytes_total) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info{name=~\"$device\"})", + "expr": "sum by (source, direction) (harbor_device_traffic_bytes_total{device_id=\"$device_id\"}) > 0", "instant": true, - "legendFormat": "{{name}} · {{source}} · {{direction}}", + "legendFormat": "{{source}} · {{direction}}", "range": false, "refId": "A" } ], - "title": "Объём выбранных устройств", + "title": "${device_id:text}: накоплено", "type": "bargauge" }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 12, + "panels": [], + "title": "Сервисы и домены", + "type": "row" + }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Сервисы выбранного устройства по объёму domain traffic в текущем диапазоне времени. Нулевые серии скрыты.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "decimals": 1, + "noValue": "Нет данных за период", "unit": "bytes" }, "overrides": [] }, "gridPos": { - "h": 9, + "h": 10, "w": 10, "x": 0, - "y": 32 + "y": 34 }, "id": 8, "options": { - "displayMode": "gradient", + "displayMode": "basic", "maxVizHeight": 300, "minVizHeight": 16, "minVizWidth": 8, @@ -488,21 +511,21 @@ "fields": "", "values": false }, - "showUnfilled": true, + "showUnfilled": false, "sizing": "auto", - "valueMode": "color" + "valueMode": "text" }, "targets": [ { "editorMode": "code", - "expr": "topk(15, sum by (service) (increase(harbor_device_domain_traffic_bytes_total[$__range])))", + "expr": "topk(10, sum by (service) (increase(harbor_device_domain_traffic_bytes_total{device_id=\"$device_id\"}[$__range])) > 0)", "instant": true, "legendFormat": "{{service}}", "range": false, "refId": "A" } ], - "title": "Сервисы за выбранный период", + "title": "${device_id:text}: сервисы за период", "type": "bargauge" }, { @@ -510,6 +533,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Домены выбранного устройства в текущем диапазоне времени. Устройство и IP не повторяются в строках.", "fieldConfig": { "defaults": { "color": { @@ -523,15 +547,16 @@ "inspect": false }, "decimals": 1, + "noValue": "Нет данных за период", "unit": "bytes" }, "overrides": [] }, "gridPos": { - "h": 9, + "h": 10, "w": 14, "x": 10, - "y": 32 + "y": 34 }, "id": 9, "options": { @@ -545,46 +570,198 @@ ], "show": false }, - "showHeader": true + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Трафик за период" + } + ] }, "targets": [ { "editorMode": "code", - "expr": "topk(50, sum by (device_id, service, domain) (increase(harbor_device_domain_traffic_bytes_total[$__range])) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info{name=~\"$device\"}))", + "expr": "topk(15, sum by (service, domain) (increase(harbor_device_domain_traffic_bytes_total{device_id=\"$device_id\"}[$__range])) > 0)", "format": "table", "instant": true, - "legendFormat": "{{name}} · {{service}} · {{domain}}", + "legendFormat": "{{service}} · {{domain}}", "range": false, "refId": "A" } ], - "title": "Домены выбранных устройств", + "title": "${device_id:text}: домены за период", "transformations": [ { "id": "organize", "options": { "excludeByName": { - "Time": true, - "device_id": true + "Time": true }, "indexByName": { - "name": 0, - "ip": 1, - "service": 2, - "domain": 3, - "Value": 4 + "service": 0, + "domain": 1, + "Value": 2 }, "renameByName": { "Value": "Трафик за период", "domain": "Домен", - "ip": "IP", - "name": "Устройство", "service": "Сервис" } } } ], "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 13, + "panels": [], + "title": "Техническая детализация", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Накопленный объём всех счётчиков Harbor за всё время хранения.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "noValue": "Нет данных", + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 45 + }, + "id": 1, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "editorMode": "code", + "expr": "sum(harbor_traffic_bytes_total)", + "instant": true, + "legendFormat": "Учтено Harbor", + "range": false, + "refId": "A" + } + ], + "title": "Накоплено: весь Harbor", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Накопленные счётчики за всё время хранения отдельно для Gateway и Proxy.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 1, + "noValue": "Нет данных", + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "gateway" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "proxy" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 16, + "x": 8, + "y": 45 + }, + "id": 4, + "options": { + "displayMode": "basic", + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": false, + "sizing": "auto", + "valueMode": "text" + }, + "targets": [ + { + "editorMode": "code", + "expr": "sum by (source) (harbor_traffic_bytes_total) > 0", + "instant": true, + "legendFormat": "{{source}}", + "range": false, + "refId": "A" + } + ], + "title": "Накоплено: источники", + "type": "bargauge" } ], "refresh": "30s", @@ -597,29 +774,25 @@ "templating": { "list": [ { - "allValue": ".*", - "current": { - "selected": true, - "text": "Все", - "value": "$__all" - }, + "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(harbor_device_info, name)", + "definition": "query_result(label_join(max by (device_id, name, ip) (harbor_device_info), \"display\", \" · \", \"name\", \"ip\"))", + "description": "Одно устройство для графиков, сервисов и доменов ниже. Значение фильтра — стабильный device_id.", "hide": 0, - "includeAll": true, + "includeAll": false, "label": "Устройство", - "multi": true, - "name": "device", + "multi": false, + "name": "device_id", "options": [], "query": { - "query": "label_values(harbor_device_info, name)", + "query": "query_result(label_join(max by (device_id, name, ip) (harbor_device_info), \"display\", \" · \", \"name\", \"ip\"))", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, "refresh": 1, - "regex": "", + "regex": "/display=\"(?[^\"]+)\"|device_id=\"(?[^\"]+)\"/g", "skipUrlSync": false, "sort": 1, "type": "query" @@ -630,10 +803,19 @@ "from": "now-24h", "to": "now" }, - "timepicker": {}, + "timepicker": { + "refresh_intervals": [ + "30s", + "1m", + "5m", + "15m", + "30m", + "1h" + ] + }, "timezone": "browser", - "title": "Harbor Gateway Traffic", + "title": "Harbor Gateway: трафик", "uid": "harbor-gateway-traffic", - "version": 2, + "version": 3, "weekStart": "monday" } diff --git a/src/shared/versions.js b/src/shared/versions.js index 4aed4e5..1aee71f 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.20.0', - gatewayClient: '0.21.0', + macClient: '0.20.1', + gatewayClient: '0.21.1', gatewayBackend: '0.21.0', }); diff --git a/src/web/instructions.js b/src/web/instructions.js index 87db376..2f2ae6c 100644 --- a/src/web/instructions.js +++ b/src/web/instructions.js @@ -92,7 +92,7 @@ export function instructionBlocks({ isGateway, host, port, controlHost }) { paragraphs: [ `Prometheus забирает накопленные Harbor counters с http://${controlHost}/metrics. Ручка читает готовый snapshot и не запускает новый сбор трафика.`, 'Harbor обновляет traffic snapshot раз в 15 секунд, поэтому начальный scrape interval и refresh dashboard в 30 секунд не создают лишних одинаковых выборок.', - 'Dashboard показывает общий объём и скорость, top services за выбранный период и домены выбранных устройств. В статистику входит только трафик, учтённый Harbor.', + 'Dashboard показывает свежесть и текущую скорость, Top-10 устройств за период и детализацию одного выбранного устройства по стабильному ID. Сервисы и домены используют тот же фильтр; накопленные счётчики вынесены в техническую секцию.', ], steps: [ 'Добавьте блок ниже в prometheus.yml и перезагрузите Prometheus.', diff --git a/test/web/prometheus-instructions.test.js b/test/web/prometheus-instructions.test.js index 7614240..cc02eae 100644 --- a/test/web/prometheus-instructions.test.js +++ b/test/web/prometheus-instructions.test.js @@ -36,21 +36,42 @@ test('metrics route reads the current snapshot before static fallback', () => { assert.doesNotMatch(server.slice(metricsRoute, staticFallback), /deviceInventory\.refresh\(/); }); -test('Grafana dashboard covers global and named per-device traffic', () => { +test('Grafana dashboard is organized around period ranking and one stable device selection', () => { const expressions = dashboard.panels.flatMap((panel) => panel.targets || []).map(({ expr }) => expr).filter(Boolean); const titles = dashboard.panels.map(({ title }) => title); + const deviceVariable = dashboard.templating.list[0]; + const freshness = dashboard.panels.find(({ id }) => id === 2); + const topDevices = dashboard.panels.find(({ id }) => id === 5); + const domains = dashboard.panels.find(({ id }) => id === 9); - assert.equal(dashboard.title, 'Harbor Gateway Traffic'); - assert.ok(titles.includes('Всего учтено Harbor')); - assert.ok(titles.includes('Общая скорость за 5 минут')); - assert.ok(titles.includes('Устройства по объёму')); - assert.ok(titles.includes('Скорость выбранных устройств')); - assert.ok(titles.includes('Сервисы за выбранный период')); - assert.ok(titles.includes('Домены выбранных устройств')); + assert.equal(dashboard.title, 'Harbor Gateway: трафик'); + assert.equal(dashboard.refresh, '30s'); + assert.deepEqual(titles.filter((title) => ['Обзор', 'Устройство', 'Сервисы и домены', 'Техническая детализация'].includes(title)), [ + 'Обзор', + 'Устройство', + 'Сервисы и домены', + 'Техническая детализация', + ]); + assert.ok(titles.includes('Скорость сейчас, средняя за 5 минут')); + assert.ok(titles.includes('Топ-10 устройств за выбранный период')); + assert.ok(titles.includes('${device_id:text}: накоплено')); + assert.ok(titles.includes('${device_id:text}: сервисы за период')); + assert.ok(titles.includes('${device_id:text}: домены за период')); assert.ok(expressions.some((expression) => expression.includes('harbor_traffic_bytes_total'))); assert.ok(expressions.some((expression) => expression.includes('harbor_device_traffic_bytes_total'))); - assert.ok(expressions.some((expression) => expression.includes('harbor_device_domain_traffic_bytes_total[$__range]'))); + assert.ok(expressions.some((expression) => expression.includes('harbor_device_domain_traffic_bytes_total{device_id="$device_id"}[$__range]'))); assert.ok(expressions.some((expression) => expression.includes('harbor_domain_traffic_last_observed_timestamp_seconds'))); - assert.ok(expressions.some((expression) => expression.includes('group_left (name, ip)'))); - assert.equal(dashboard.templating.list[0].query.query, 'label_values(harbor_device_info, name)'); + assert.match(topDevices.targets[0].expr, /^topk\(10,[\s\S]*increase\(harbor_device_traffic_bytes_total\[\$__range\]\)[\s\S]*> 0[\s\S]*group_left \(name, ip\)/); + assert.equal(topDevices.options.sortBy[0].desc, true); + assert.match(topDevices.fieldConfig.overrides[1].properties[0].value[0].url, /var-device_id=\$\{__data\.fields/); + assert.deepEqual(freshness.fieldConfig.defaults.thresholds.steps.map(({ value }) => value), [null, 60, 120]); + assert.equal(freshness.fieldConfig.defaults.noValue, 'Нет данных'); + assert.equal(deviceVariable.name, 'device_id'); + assert.equal(deviceVariable.multi, false); + assert.equal(deviceVariable.includeAll, false); + assert.match(deviceVariable.query.query, /query_result\(label_join\(/); + assert.match(deviceVariable.regex, //); + assert.match(deviceVariable.regex, //); + assert.deepEqual(domains.transformations[0].options.indexByName, { service: 0, domain: 1, Value: 2 }); + assert.match(domains.targets[0].expr, /^topk\(15,[\s\S]*> 0\)$/); });