Clarify VPN and Direct traffic metrics in Grafana dashboard
This commit is contained in:
@@ -302,17 +302,17 @@ scrape_configs:
|
|||||||
- targets: ["<gateway>:3456"]
|
- targets: ["<gateway>: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` добавляет наблюдённые домен, сервис, источник и направление для каждого устройства. `harbor_domain_traffic_attribution_events_total{outcome}` помогает отличить нераспознанный hostname, неизвестное устройство и неподдерживаемый inbound без динамических high-cardinality labels.
|
`harbor_traffic_bytes_total` содержит общий накопленный объём по источникам Gateway/Proxy. `harbor_device_traffic_bytes_total` содержит upload/download по стабильному `device_id`; пользовательское название и текущий IP находятся в `harbor_device_info`. Gauge `harbor_device_applied_policy{device_id}` показывает последнюю применённую policy: `0` для Direct и `1` для VPN. История начинается с первого Prometheus scrape после обновления и не восстанавливается задним числом. `harbor_device_domain_traffic_bytes_total` добавляет наблюдённые домен, сервис, источник и направление для каждого устройства. `harbor_domain_traffic_attribution_events_total{outcome}` помогает отличить нераспознанный hostname, неизвестное устройство и неподдерживаемый inbound без динамических high-cardinality labels.
|
||||||
|
|
||||||
Фактический выход экспортируется отдельно. `harbor_singbox_tracked_bytes_total{source,outbound,direction}` показывает наблюдённые sing-box байты с `outbound="vpn|direct|unknown"`; вариант с префиксом `harbor_device_...` добавляет `device_id`. `harbor_direct_ipv4_packet_bytes_total{direction}` считает IPv4-пакеты, которые Gateway направил напрямую вместо sing-box, включая policy Direct и работу при остановленном VPN runtime; вариант `harbor_device_...` содержит атрибутированную детализацию. `source="gateway|proxy"` по-прежнему означает место входа, а `outbound` — выбранный sing-box выход.
|
Фактический выход экспортируется отдельно. `harbor_singbox_tracked_bytes_total{source,outbound,direction}` показывает наблюдённые sing-box байты с `outbound="vpn|direct|unknown"`; вариант с префиксом `harbor_device_...` добавляет `device_id`. `harbor_direct_ipv4_packet_bytes_total{direction}` считает IPv4-пакеты, которые Gateway направил напрямую вместо sing-box, включая policy Direct и работу при остановленном VPN runtime; вариант `harbor_device_...` содержит атрибутированную детализацию. `source="gateway|proxy"` по-прежнему означает место входа, а `outbound` — выбранный sing-box выход.
|
||||||
|
|
||||||
Dashboard начинает со скорости скачивания и отправки в конце выбранного периода, общего трафика и VPN / Direct внутри sing-box за этот период. Для стандартного диапазона, который заканчивается сейчас, карточки скорости показывают текущее значение. Единый фильтр `Устройства` по умолчанию охватывает все устройства, но позволяет выбрать одно; он управляет скоростью, общим трафиком, маршрутами, сервисами, доменами и технической детализацией. Таблица «Все устройства за период» намеренно остаётся общей: она показывает все устройства с ненулевым трафиком, сортируется в обе стороны и выбирает устройство в том же фильтре. Блок «Куда уходит трафик» показывает основные назначения и Top-15 доменов без пагинации. Свёрнутая техническая детализация отдельно показывает точки входа Gateway / Proxy и Direct IPv4 мимо sing-box. Автообновление настроено на 30 секунд; индикатор показывает возраст самого старого из контуров общего, domain / sing-box и Direct IPv4 трафика, предупреждает после 60 секунд и считает данные устаревшими после 120 секунд.
|
Dashboard начинает со скорости скачивания и отправки в конце выбранного периода, общего трафика и фактического VPN / Direct за этот период. Под обзором полоса `Применённый режим` показывает applied policy устройства, а график `Фактический VPN / Direct` независимо показывает маршрут наблюдённых байтов. Поэтому компьютер с policy Direct, браузер которого использует Harbor Proxy, остаётся Direct на полосе режима, но его proxy-соединения учитываются в VPN. Единый фильтр `Устройства` управляет режимом, скоростью, общим трафиком, сервисами, доменами и технической детализацией. Таблица «Все устройства за период» намеренно остаётся общей и выбирает устройство в том же фильтре. Блок «Куда уходит трафик» показывает основные назначения и Top-15 доменов без пагинации. Свёрнутая техническая детализация показывает `source × outbound`, включая `proxy · vpn`, и раздельные Direct-пути через sing-box и Linux мимо sing-box. Автообновление настроено на 30 секунд; индикатор предупреждает после 60 секунд и считает данные устаревшими после 120 секунд.
|
||||||
|
|
||||||
В `snapshot` и `shadow` domain и sing-box outbound counters снимаются с активных соединений раз в 2 секунды. В `native` dataplane получает полный lifecycle, включая короткие соединения и финальный хвост; данные всё равно хранятся в памяти только до перезапуска, а историю и retention хранит Prometheus. Перед routing sing-box до 1 секунды распознаёт HTTP Host, TLS SNI и QUIC Server Name. YouTube и OpenAI / ChatGPT объединяются по известным связанным доменам в label `service`, остальные значения сохраняют домен как имя сервиса. Если устройство и Harbor source известны, но hostname недоступен (например, ECH или IP-only), трафик попадает в `domain="_unknown",service="Не распознано"` и не теряется. Новые domain series сверх process limit складываются в `_other`.
|
В `snapshot` и `shadow` domain и sing-box outbound counters снимаются с активных соединений раз в 2 секунды. В `native` dataplane получает полный lifecycle, включая короткие соединения и финальный хвост; данные всё равно хранятся в памяти только до перезапуска, а историю и retention хранит Prometheus. Перед routing sing-box до 1 секунды распознаёт HTTP Host, TLS SNI и QUIC Server Name. YouTube и OpenAI / ChatGPT объединяются по известным связанным доменам в label `service`, остальные значения сохраняют домен как имя сервиса. Если устройство и Harbor source известны, но hostname недоступен (например, ECH или IP-only), трафик попадает в `domain="_unknown",service="Не распознано"` и не теряется. Новые domain series сверх process limit складываются в `_other`.
|
||||||
|
|
||||||
Состояние collector и сравнение `shadow` экспортируются отдельными bounded gauges `harbor_traffic_collector_*` и `harbor_traffic_shadow_*`. Они не содержат UUID, IP, домены или пользовательские имена и не заменяют canonical traffic counters.
|
Состояние collector и сравнение `shadow` экспортируются отдельными bounded gauges `harbor_traffic_collector_*` и `harbor_traffic_shadow_*`. Они не содержат UUID, IP, домены или пользовательские имена и не заменяют canonical traffic counters.
|
||||||
|
|
||||||
Direct IPv4 считает L3 packet bytes с IP-заголовками и retransmit, а sing-box tracker — логические TCP/UDP bytes без tunnel overhead. Эти семейства нельзя складывать в один «точный общий трафик». Snapshot polling может пропустить короткие соединения и финальный хвост; native lifecycle закрывает этот разрыв только для трафика, вошедшего в sing-box. IPv6, трафик вне Gateway, назначения из `BYPASS_CIDRS` и quota провайдера не входят в новый route split.
|
Direct IPv4 считает L3 packet bytes с IP-заголовками и retransmit, а sing-box tracker считает логические TCP/UDP bytes без tunnel overhead. Основные Grafana panels складывают их только как приблизительную пользовательскую оценку непересекающихся Direct-путей; техническая секция сохраняет значения раздельными. Эту сумму нельзя считать точным provider или wire total. Snapshot polling может пропустить короткие соединения и финальный хвост; native lifecycle закрывает этот разрыв только для трафика, вошедшего в sing-box. IPv6, трафик вне Gateway, назначения из `BYPASS_CIDRS` и quota провайдера не входят в route split.
|
||||||
|
|
||||||
Готовый dashboard: [`monitoring/grafana/harbor-gateway.json`](monitoring/grafana/harbor-gateway.json). При импорте Grafana попросит выбрать Prometheus data source. Та же конфигурация и dashboard доступны для копирования в Gateway drawer «Как использовать» → «Prometheus и Grafana».
|
Готовый dashboard: [`monitoring/grafana/harbor-gateway.json`](monitoring/grafana/harbor-gateway.json). При импорте Grafana попросит выбрать Prometheus data source. Та же конфигурация и dashboard доступны для копирования в Gateway drawer «Как использовать» → «Prometheus и Grafana».
|
||||||
|
|
||||||
|
|||||||
@@ -196,14 +196,14 @@
|
|||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "${DS_PROMETHEUS}"
|
"uid": "${DS_PROMETHEUS}"
|
||||||
},
|
},
|
||||||
"description": "Наблюдённые sing-box байты для выбранных устройств по фактически выбранному выходу. Direct IPv4 мимо sing-box учитывается отдельно в технической детализации.",
|
"description": "Фактический выход выбранных устройств за период. VPN включает трафик, вошедший через Gateway и явно настроенный Proxy. Direct приблизительно объединяет логические байты sing-box и IPv4 L3 packet bytes Linux; точная разбивка остаётся в технической детализации.",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-classic"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"decimals": 1,
|
"decimals": 1,
|
||||||
"noValue": "Нет трафика",
|
"noValue": "Нет данных",
|
||||||
"unit": "bytes"
|
"unit": "bytes"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(increase(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"direct\"}[$__range])) > 0",
|
"expr": "((sum(increase(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"direct\"}[$__range])) or vector(0)) + (sum(increase(harbor_device_direct_ipv4_packet_bytes_total{device_id=~\"$device_id\"}[$__range])) or vector(0))) > 0",
|
||||||
"instant": true,
|
"instant": true,
|
||||||
"legendFormat": "Direct",
|
"legendFormat": "Direct",
|
||||||
"range": false,
|
"range": false,
|
||||||
@@ -382,7 +382,73 @@
|
|||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "${DS_PROMETHEUS}"
|
"uid": "${DS_PROMETHEUS}"
|
||||||
},
|
},
|
||||||
"description": "Скорость трафика выбранных устройств. Окно усреднения адаптируется к масштабу времени; скачивание показано вверх, отправка — вниз. Это наблюдаемый Harbor трафик, а не скорость интернет-канала.",
|
"description": "Последняя применённая policy устройства по данным Prometheus: Direct или VPN. Это не маршрут каждого байта: устройство в Direct может одновременно использовать VPN через явно настроенный Harbor Proxy.",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 0,
|
||||||
|
"text": "Direct"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"color": "green",
|
||||||
|
"index": 1,
|
||||||
|
"text": "VPN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"noValue": "Нет данных"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 4,
|
||||||
|
"w": 16,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 17,
|
||||||
|
"options": {
|
||||||
|
"alignValue": "left",
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"mergeValues": true,
|
||||||
|
"rowHeight": 0.9,
|
||||||
|
"showValue": "always",
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "max by (device_id) (harbor_device_applied_policy{device_id=~\"$device_id\"}) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info)",
|
||||||
|
"legendFormat": "{{name}} · {{ip}}",
|
||||||
|
"range": true,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Применённый режим",
|
||||||
|
"type": "state-timeline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${DS_PROMETHEUS}"
|
||||||
|
},
|
||||||
|
"description": "Фактическая скорость по выходу выбранных устройств. VPN суммирует Gateway и Proxy. Direct приблизительно объединяет непересекающиеся sing-box и Linux IPv4 пути с разной byte semantics. Скачивание показано вверх, отправка вниз.",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
@@ -414,14 +480,14 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"matcher": {
|
"matcher": {
|
||||||
"id": "byName",
|
"id": "byRegexp",
|
||||||
"options": "Скачивание"
|
"options": "^VPN"
|
||||||
},
|
},
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "color",
|
"id": "color",
|
||||||
"value": {
|
"value": {
|
||||||
"fixedColor": "blue",
|
"fixedColor": "green",
|
||||||
"mode": "fixed"
|
"mode": "fixed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,8 +495,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": {
|
"matcher": {
|
||||||
"id": "byName",
|
"id": "byRegexp",
|
||||||
"options": "Отправка"
|
"options": "^Direct"
|
||||||
},
|
},
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
@@ -442,6 +508,21 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byRegexp",
|
||||||
|
"options": "^Не определено"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "gray",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matcher": {
|
"matcher": {
|
||||||
"id": "byFrameRefID",
|
"id": "byFrameRefID",
|
||||||
@@ -453,6 +534,30 @@
|
|||||||
"value": "negative-Y"
|
"value": "negative-Y"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byFrameRefID",
|
||||||
|
"options": "D"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.transform",
|
||||||
|
"value": "negative-Y"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byFrameRefID",
|
||||||
|
"options": "F"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.transform",
|
||||||
|
"value": "negative-Y"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -460,7 +565,7 @@
|
|||||||
"h": 12,
|
"h": 12,
|
||||||
"w": 16,
|
"w": 16,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 5
|
"y": 9
|
||||||
},
|
},
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"options": {
|
"options": {
|
||||||
@@ -482,20 +587,48 @@
|
|||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[$__rate_interval])) > 0",
|
"expr": "sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"vpn\", direction=\"download\"}[$__rate_interval])) > 0",
|
||||||
"legendFormat": "Скачивание",
|
"legendFormat": "VPN · скачивание",
|
||||||
"range": true,
|
"range": true,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[$__rate_interval])) > 0",
|
"expr": "sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"vpn\", direction=\"upload\"}[$__rate_interval])) > 0",
|
||||||
"legendFormat": "Отправка",
|
"legendFormat": "VPN · отправка",
|
||||||
"range": true,
|
"range": true,
|
||||||
"refId": "B"
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "((sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"direct\", direction=\"download\"}[$__rate_interval])) or vector(0)) + (sum(rate(harbor_device_direct_ipv4_packet_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[$__rate_interval])) or vector(0))) > 0",
|
||||||
|
"legendFormat": "Direct · скачивание",
|
||||||
|
"range": true,
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "((sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"direct\", direction=\"upload\"}[$__rate_interval])) or vector(0)) + (sum(rate(harbor_device_direct_ipv4_packet_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[$__rate_interval])) or vector(0))) > 0",
|
||||||
|
"legendFormat": "Direct · отправка",
|
||||||
|
"range": true,
|
||||||
|
"refId": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"unknown\", direction=\"download\"}[$__rate_interval])) > 0",
|
||||||
|
"legendFormat": "Не определено · скачивание",
|
||||||
|
"range": true,
|
||||||
|
"refId": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "sum(rate(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"unknown\", direction=\"upload\"}[$__rate_interval])) > 0",
|
||||||
|
"legendFormat": "Не определено · отправка",
|
||||||
|
"range": true,
|
||||||
|
"refId": "F"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Скорость трафика",
|
"title": "Фактический VPN / Direct",
|
||||||
"type": "timeseries"
|
"type": "timeseries"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -582,7 +715,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 12,
|
"h": 16,
|
||||||
"w": 8,
|
"w": 8,
|
||||||
"x": 16,
|
"x": 16,
|
||||||
"y": 5
|
"y": 5
|
||||||
@@ -648,7 +781,7 @@
|
|||||||
"h": 1,
|
"h": 1,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 17
|
"y": 21
|
||||||
},
|
},
|
||||||
"id": 12,
|
"id": 12,
|
||||||
"panels": [],
|
"panels": [],
|
||||||
@@ -676,7 +809,7 @@
|
|||||||
"h": 15,
|
"h": 15,
|
||||||
"w": 9,
|
"w": 9,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 18
|
"y": 22
|
||||||
},
|
},
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"options": {
|
"options": {
|
||||||
@@ -775,7 +908,7 @@
|
|||||||
"h": 15,
|
"h": 15,
|
||||||
"w": 15,
|
"w": 15,
|
||||||
"x": 9,
|
"x": 9,
|
||||||
"y": 18
|
"y": 22
|
||||||
},
|
},
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"options": {
|
"options": {
|
||||||
@@ -837,7 +970,7 @@
|
|||||||
"h": 1,
|
"h": 1,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 33
|
"y": 37
|
||||||
},
|
},
|
||||||
"id": 13,
|
"id": 13,
|
||||||
"panels": [
|
"panels": [
|
||||||
@@ -846,7 +979,7 @@
|
|||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "${DS_PROMETHEUS}"
|
"uid": "${DS_PROMETHEUS}"
|
||||||
},
|
},
|
||||||
"description": "Где выбранный трафик вошёл в Harbor: через Gateway или Proxy. Это техническая точка входа, а не выбор VPN / Direct.",
|
"description": "Как sing-box увидел трафик выбранных устройств: точка входа gateway или proxy и фактически выбранный outbound. Например, компьютер с policy Direct и браузерным Proxy появляется как proxy · vpn.",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
@@ -856,44 +989,13 @@
|
|||||||
"noValue": "Нет данных",
|
"noValue": "Нет данных",
|
||||||
"unit": "bytes"
|
"unit": "bytes"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"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": {
|
"gridPos": {
|
||||||
"h": 7,
|
"h": 7,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 34
|
"y": 38
|
||||||
},
|
},
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"options": {
|
"options": {
|
||||||
@@ -917,14 +1019,14 @@
|
|||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum by (source) (increase(harbor_device_traffic_bytes_total{device_id=~\"$device_id\"}[$__range])) > 0",
|
"expr": "sum by (source, outbound) (increase(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\"}[$__range])) > 0",
|
||||||
"instant": true,
|
"instant": true,
|
||||||
"legendFormat": "{{source}}",
|
"legendFormat": "{{source}} · {{outbound}}",
|
||||||
"range": false,
|
"range": false,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Точки входа Gateway / Proxy",
|
"title": "Вход × выход sing-box",
|
||||||
"type": "bargauge"
|
"type": "bargauge"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -932,7 +1034,7 @@
|
|||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "${DS_PROMETHEUS}"
|
"uid": "${DS_PROMETHEUS}"
|
||||||
},
|
},
|
||||||
"description": "IPv4-пакеты выбранных устройств, которые Gateway направил напрямую вместо sing-box: policy Direct или работа при остановленном VPN runtime. Это L3 packet bytes с заголовками и retransmit; их нельзя складывать с sing-box tracker bytes.",
|
"description": "Два непересекающихся Direct-пути выбранных устройств. Через sing-box показаны логические TCP/UDP bytes; Linux мимо sing-box показывает IPv4 L3 packet bytes с заголовками и retransmit. Их сумма в основных панелях является только приблизительной оценкой.",
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
@@ -942,13 +1044,44 @@
|
|||||||
"noValue": "Нет данных",
|
"noValue": "Нет данных",
|
||||||
"unit": "bytes"
|
"unit": "bytes"
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byFrameRefID",
|
||||||
|
"options": "A"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "orange",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byFrameRefID",
|
||||||
|
"options": "B"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"fixedColor": "purple",
|
||||||
|
"mode": "fixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 7,
|
"h": 7,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 34
|
"y": 38
|
||||||
},
|
},
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"options": {
|
"options": {
|
||||||
@@ -972,14 +1105,22 @@
|
|||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum by (direction) (increase(harbor_device_direct_ipv4_packet_bytes_total{device_id=~\"$device_id\"}[$__range])) > 0",
|
"expr": "sum(increase(harbor_device_singbox_tracked_bytes_total{device_id=~\"$device_id\", outbound=\"direct\"}[$__range])) > 0",
|
||||||
"instant": true,
|
"instant": true,
|
||||||
"legendFormat": "{{direction}}",
|
"legendFormat": "Через sing-box",
|
||||||
"range": false,
|
"range": false,
|
||||||
"refId": "A"
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "sum(increase(harbor_device_direct_ipv4_packet_bytes_total{device_id=~\"$device_id\"}[$__range])) > 0",
|
||||||
|
"instant": true,
|
||||||
|
"legendFormat": "Linux мимо sing-box",
|
||||||
|
"range": false,
|
||||||
|
"refId": "B"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Direct IPv4 мимо sing-box",
|
"title": "Direct раздельно",
|
||||||
"type": "bargauge"
|
"type": "bargauge"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1008,7 +1149,7 @@
|
|||||||
"uid": "${DS_PROMETHEUS}"
|
"uid": "${DS_PROMETHEUS}"
|
||||||
},
|
},
|
||||||
"definition": "query_result(label_join(max by (device_id, name, ip) (harbor_device_info), \"display\", \" · \", \"name\", \"ip\"))",
|
"definition": "query_result(label_join(max by (device_id, name, ip) (harbor_device_info), \"display\", \" · \", \"name\", \"ip\"))",
|
||||||
"description": "Выберите все устройства или одно устройство. Фильтр управляет скоростью, общим трафиком, маршрутами, сервисами, доменами и технической детализацией.",
|
"description": "Выберите все устройства или одно устройство. Фильтр управляет применённым режимом, фактическим VPN/Direct, общим трафиком, сервисами, доменами и технической детализацией.",
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"includeAll": true,
|
"includeAll": true,
|
||||||
"label": "Устройства",
|
"label": "Устройства",
|
||||||
@@ -1044,6 +1185,6 @@
|
|||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Harbor Gateway: трафик",
|
"title": "Harbor Gateway: трафик",
|
||||||
"uid": "harbor-gateway-traffic",
|
"uid": "harbor-gateway-traffic",
|
||||||
"version": 9,
|
"version": 10,
|
||||||
"weekStart": "monday"
|
"weekStart": "monday"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const COUNTER_PATTERN = /^\d+$/;
|
|||||||
const SIGNED_DECIMAL_PATTERN = /^-?\d+$/;
|
const SIGNED_DECIMAL_PATTERN = /^-?\d+$/;
|
||||||
const COLLECTOR_MODES = new Set(['snapshot', 'shadow', 'native']);
|
const COLLECTOR_MODES = new Set(['snapshot', 'shadow', 'native']);
|
||||||
const COLLECTOR_WRITERS = new Set(['snapshot', 'native']);
|
const COLLECTOR_WRITERS = new Set(['snapshot', 'native']);
|
||||||
|
const APPLIED_POLICY_VALUES: Readonly<Record<string, string>> = Object.freeze({ direct: '0', vpn: '1' });
|
||||||
const COLLECTOR_STATES = new Set([
|
const COLLECTOR_STATES = new Set([
|
||||||
'connecting', 'live', 'degraded', 'stale', 'stopped', 'incompatible', 'disabled',
|
'connecting', 'live', 'degraded', 'stale', 'stopped', 'incompatible', 'disabled',
|
||||||
]);
|
]);
|
||||||
@@ -93,6 +94,20 @@ export function renderPrometheusMetrics(value: unknown) {
|
|||||||
ip: device.ip || '',
|
ip: device.ip || '',
|
||||||
}, '1');
|
}, '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'# HELP harbor_device_applied_policy Current applied device policy: 0 Direct, 1 VPN.',
|
||||||
|
'# TYPE harbor_device_applied_policy gauge',
|
||||||
|
);
|
||||||
|
for (const device of devices) {
|
||||||
|
const appliedPolicy = String(device.appliedPolicy || '');
|
||||||
|
if (!Object.hasOwn(APPLIED_POLICY_VALUES, appliedPolicy)) {
|
||||||
|
throw new Error('Invalid applied device policy');
|
||||||
|
}
|
||||||
|
metric(lines, 'harbor_device_applied_policy', {
|
||||||
|
device_id: device.id,
|
||||||
|
}, APPLIED_POLICY_VALUES[appliedPolicy]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceTraffic = devices.flatMap((device) => [
|
const deviceTraffic = devices.flatMap((device) => [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.36.0',
|
macClient: '0.36.1',
|
||||||
gatewayClient: '0.38.0',
|
gatewayClient: '0.38.1',
|
||||||
gatewayBackend: '0.38.0',
|
gatewayBackend: '0.38.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ParsedVersion {
|
export interface ParsedVersion {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function instructionBlocks({ isGateway, host, port, controlHost }: {
|
|||||||
paragraphs: [
|
paragraphs: [
|
||||||
`Prometheus забирает накопленные Harbor counters с http://${controlHost}/metrics. Ручка читает готовый snapshot и не запускает новый сбор трафика.`,
|
`Prometheus забирает накопленные Harbor counters с http://${controlHost}/metrics. Ручка читает готовый snapshot и не запускает новый сбор трафика.`,
|
||||||
'Harbor обновляет traffic snapshot раз в 15 секунд, поэтому начальный scrape interval и refresh dashboard в 30 секунд не создают лишних одинаковых выборок.',
|
'Harbor обновляет traffic snapshot раз в 15 секунд, поэтому начальный scrape interval и refresh dashboard в 30 секунд не создают лишних одинаковых выборок.',
|
||||||
'Единый фильтр «Устройства» управляет скоростью, накопленным трафиком, сервисами и доменами для всех или одного устройства. Отдельный график показывает текущую скорость каждого активного устройства; нулевые series скрыты.',
|
'Единый фильтр «Устройства» управляет applied policy, фактическим VPN/Direct, общим трафиком, сервисами и доменами. Policy Direct не запрещает VPN через явно настроенный Harbor Proxy: режим останется Direct, а proxy-байты появятся в VPN.',
|
||||||
],
|
],
|
||||||
steps: [
|
steps: [
|
||||||
'Добавьте блок ниже в prometheus.yml и перезагрузите Prometheus.',
|
'Добавьте блок ниже в prometheus.yml и перезагрузите Prometheus.',
|
||||||
@@ -110,7 +110,7 @@ export function instructionBlocks({ isGateway, host, port, controlHost }: {
|
|||||||
{ id: 'prometheus-config', label: 'prometheus.yml', text: prometheusScrapeConfig(controlHost) },
|
{ id: 'prometheus-config', label: 'prometheus.yml', text: prometheusScrapeConfig(controlHost) },
|
||||||
{ id: 'grafana-dashboard', label: 'Grafana dashboard', text: grafanaDashboardJson },
|
{ id: 'grafana-dashboard', label: 'Grafana dashboard', text: grafanaDashboardJson },
|
||||||
],
|
],
|
||||||
note: 'Domain counters снимаются с активных соединений sing-box раз в 2 секунды. Историю хранит Prometheus; соединения между снимками могут быть пропущены, неизвестный домен записывается как _unknown, а policy Direct виден только в отдельной Direct IPv4 metric.',
|
note: 'Историю хранит Prometheus. Основной Direct приблизительно объединяет sing-box и Linux IPv4 bytes с разной семантикой; точные значения остаются раздельными в технической секции. До первого scrape история applied policy отсутствует.',
|
||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,22 @@ test('domain traffic accumulates connection deltas by device, service and source
|
|||||||
downloadBytes: '70',
|
downloadBytes: '70',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
assert.deepEqual(service.snapshot().routes, [
|
||||||
|
{
|
||||||
|
deviceId: id,
|
||||||
|
source: 'gateway',
|
||||||
|
outbound: 'vpn',
|
||||||
|
uploadBytes: '22',
|
||||||
|
downloadBytes: '200',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: id,
|
||||||
|
source: 'proxy',
|
||||||
|
outbound: 'vpn',
|
||||||
|
uploadBytes: '22',
|
||||||
|
downloadBytes: '260',
|
||||||
|
},
|
||||||
|
]);
|
||||||
assert.deepEqual(service.snapshot().attributionEvents, {
|
assert.deepEqual(service.snapshot().attributionEvents, {
|
||||||
unresolved_host: '1',
|
unresolved_host: '1',
|
||||||
unknown_device: '1',
|
unknown_device: '1',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const snapshot = {
|
|||||||
proxyUploadBytes: '0',
|
proxyUploadBytes: '0',
|
||||||
proxyDownloadBytes: '0',
|
proxyDownloadBytes: '0',
|
||||||
proxyTrafficObservedAt: null,
|
proxyTrafficObservedAt: null,
|
||||||
|
appliedPolicy: 'direct',
|
||||||
}],
|
}],
|
||||||
directTraffic: {
|
directTraffic: {
|
||||||
observedAt,
|
observedAt,
|
||||||
@@ -80,6 +81,12 @@ const snapshot = {
|
|||||||
outbound: 'vpn',
|
outbound: 'vpn',
|
||||||
uploadBytes: '55',
|
uploadBytes: '55',
|
||||||
downloadBytes: '666',
|
downloadBytes: '666',
|
||||||
|
}, {
|
||||||
|
deviceId: 'dev_0011223344556677',
|
||||||
|
source: 'proxy',
|
||||||
|
outbound: 'vpn',
|
||||||
|
uploadBytes: '12',
|
||||||
|
downloadBytes: '345',
|
||||||
}],
|
}],
|
||||||
series: [{
|
series: [{
|
||||||
deviceId: 'dev_0011223344556677',
|
deviceId: 'dev_0011223344556677',
|
||||||
@@ -98,6 +105,8 @@ test('Prometheus exposition keeps exact counters, stable identity and escaped na
|
|||||||
assert.match(output, /# TYPE harbor_traffic_bytes_total counter/);
|
assert.match(output, /# TYPE harbor_traffic_bytes_total counter/);
|
||||||
assert.match(output, /harbor_traffic_bytes_total\{source="gateway"\} 9007199254740993/);
|
assert.match(output, /harbor_traffic_bytes_total\{source="gateway"\} 9007199254740993/);
|
||||||
assert.match(output, /harbor_device_info\{device_id="dev_0011223344556677",name="ТВ \\"Зал\\"\\\\основной\\nэкран",ip="192\.168\.50\.7"\} 1/);
|
assert.match(output, /harbor_device_info\{device_id="dev_0011223344556677",name="ТВ \\"Зал\\"\\\\основной\\nэкран",ip="192\.168\.50\.7"\} 1/);
|
||||||
|
assert.match(output, /harbor_device_applied_policy\{device_id="dev_0011223344556677"\} 0/);
|
||||||
|
assert.doesNotMatch(output, /harbor_device_applied_policy\{[^\n]*(?:name|ip|mac|desired|status)=/);
|
||||||
assert.match(output, /harbor_device_traffic_bytes_total\{device_id="dev_0011223344556677",source="gateway",direction="upload"\} 9007199254740993/);
|
assert.match(output, /harbor_device_traffic_bytes_total\{device_id="dev_0011223344556677",source="gateway",direction="upload"\} 9007199254740993/);
|
||||||
assert.doesNotMatch(output, /harbor_device_traffic_bytes_total\{[^\n]*name=/);
|
assert.doesNotMatch(output, /harbor_device_traffic_bytes_total\{[^\n]*name=/);
|
||||||
assert.doesNotMatch(output, /harbor_device_traffic_bytes_total\{[^\n]*source="proxy"/);
|
assert.doesNotMatch(output, /harbor_device_traffic_bytes_total\{[^\n]*source="proxy"/);
|
||||||
@@ -112,6 +121,7 @@ test('Prometheus exposition keeps exact counters, stable identity and escaped na
|
|||||||
assert.match(output, /harbor_singbox_tracked_bytes_total\{source="gateway",outbound="vpn",direction="download"\} 777/);
|
assert.match(output, /harbor_singbox_tracked_bytes_total\{source="gateway",outbound="vpn",direction="download"\} 777/);
|
||||||
assert.match(output, /# TYPE harbor_device_singbox_tracked_bytes_total counter/);
|
assert.match(output, /# TYPE harbor_device_singbox_tracked_bytes_total counter/);
|
||||||
assert.match(output, /harbor_device_singbox_tracked_bytes_total\{device_id="dev_0011223344556677",source="gateway",outbound="vpn",direction="upload"\} 55/);
|
assert.match(output, /harbor_device_singbox_tracked_bytes_total\{device_id="dev_0011223344556677",source="gateway",outbound="vpn",direction="upload"\} 55/);
|
||||||
|
assert.match(output, /harbor_device_singbox_tracked_bytes_total\{device_id="dev_0011223344556677",source="proxy",outbound="vpn",direction="download"\} 345/);
|
||||||
assert.match(output, /harbor_device_domain_traffic_bytes_total\{device_id="dev_0011223344556677",domain="chatgpt\.com",service="OpenAI \/ ChatGPT",source="proxy",direction="download"\} 345/);
|
assert.match(output, /harbor_device_domain_traffic_bytes_total\{device_id="dev_0011223344556677",domain="chatgpt\.com",service="OpenAI \/ ChatGPT",source="proxy",direction="download"\} 345/);
|
||||||
assert.match(output, /harbor_domain_traffic_last_observed_timestamp_seconds 1786183200/);
|
assert.match(output, /harbor_domain_traffic_last_observed_timestamp_seconds 1786183200/);
|
||||||
assert.match(output, /harbor_domain_traffic_overflow_connections_total 2/);
|
assert.match(output, /harbor_domain_traffic_overflow_connections_total 2/);
|
||||||
@@ -181,6 +191,19 @@ test('invalid canonical counters fail the scrape instead of publishing corrupt v
|
|||||||
const invalidShadow = structuredClone(snapshot);
|
const invalidShadow = structuredClone(snapshot);
|
||||||
invalidShadow.domainTraffic.source.shadow.uploadDifferenceBytes = '1.5';
|
invalidShadow.domainTraffic.source.shadow.uploadDifferenceBytes = '1.5';
|
||||||
assert.throws(() => renderPrometheusMetrics(invalidShadow), /Invalid Prometheus gauge/);
|
assert.throws(() => renderPrometheusMetrics(invalidShadow), /Invalid Prometheus gauge/);
|
||||||
|
const invalidPolicy = structuredClone(snapshot);
|
||||||
|
invalidPolicy.devices[0].appliedPolicy = 'future';
|
||||||
|
assert.throws(() => renderPrometheusMetrics(invalidPolicy), /Invalid applied device policy/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Prometheus applied policy gauge reports VPN without changing series identity', () => {
|
||||||
|
const vpn = structuredClone(snapshot);
|
||||||
|
vpn.devices[0].appliedPolicy = 'vpn';
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
renderPrometheusMetrics(vpn),
|
||||||
|
/harbor_device_applied_policy\{device_id="dev_0011223344556677"\} 1/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function routeResponse() {
|
function routeResponse() {
|
||||||
|
|||||||
@@ -43,15 +43,16 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
|||||||
const services = byId.get(8);
|
const services = byId.get(8);
|
||||||
const domains = byId.get(9);
|
const domains = byId.get(9);
|
||||||
const totalTraffic = byId.get(1);
|
const totalTraffic = byId.get(1);
|
||||||
const trafficBySource = byId.get(4);
|
const sourceOutbounds = byId.get(4);
|
||||||
const singboxOutbounds = byId.get(15);
|
const singboxOutbounds = byId.get(15);
|
||||||
const directPackets = byId.get(16);
|
const directPaths = byId.get(16);
|
||||||
|
const appliedPolicy = byId.get(17);
|
||||||
const technical = byId.get(13);
|
const technical = byId.get(13);
|
||||||
|
|
||||||
assert.equal(dashboard.title, 'Harbor Gateway: трафик');
|
assert.equal(dashboard.title, 'Harbor Gateway: трафик');
|
||||||
assert.equal(dashboard.refresh, '30s');
|
assert.equal(dashboard.refresh, '30s');
|
||||||
assert.equal(dashboard.version, 9);
|
assert.equal(dashboard.version, 10);
|
||||||
assert.deepEqual(dashboard.panels.map(({ id }) => id), [10, 6, 7, 1, 15, 2, 3, 5, 12, 8, 9, 13]);
|
assert.deepEqual(dashboard.panels.map(({ id }) => id), [10, 6, 7, 1, 15, 2, 17, 3, 5, 12, 8, 9, 13]);
|
||||||
assert.deepEqual(dashboard.panels.filter(({ type }) => type === 'row').map(({ title }) => title), [
|
assert.deepEqual(dashboard.panels.filter(({ type }) => type === 'row').map(({ title }) => title), [
|
||||||
'Обзор',
|
'Обзор',
|
||||||
'Куда уходит трафик',
|
'Куда уходит трафик',
|
||||||
@@ -62,11 +63,12 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
|||||||
assert.equal(byId.has(11), false);
|
assert.equal(byId.has(11), false);
|
||||||
assert.equal(byId.has(14), false);
|
assert.equal(byId.has(14), false);
|
||||||
|
|
||||||
assert.deepEqual(speed.gridPos, { h: 12, w: 16, x: 0, y: 5 });
|
assert.deepEqual(appliedPolicy.gridPos, { h: 4, w: 16, x: 0, y: 5 });
|
||||||
assert.deepEqual(devices.gridPos, { h: 12, w: 8, x: 16, y: 5 });
|
assert.deepEqual(speed.gridPos, { h: 12, w: 16, x: 0, y: 9 });
|
||||||
assert.deepEqual(services.gridPos, { h: 15, w: 9, x: 0, y: 18 });
|
assert.deepEqual(devices.gridPos, { h: 16, w: 8, x: 16, y: 5 });
|
||||||
assert.deepEqual(domains.gridPos, { h: 15, w: 15, x: 9, y: 18 });
|
assert.deepEqual(services.gridPos, { h: 15, w: 9, x: 0, y: 22 });
|
||||||
assert.deepEqual(technical.gridPos, { h: 1, w: 24, x: 0, y: 33 });
|
assert.deepEqual(domains.gridPos, { h: 15, w: 15, x: 9, y: 22 });
|
||||||
|
assert.deepEqual(technical.gridPos, { h: 1, w: 24, x: 0, y: 37 });
|
||||||
assert.equal(devices.options.footer.enablePagination, false);
|
assert.equal(devices.options.footer.enablePagination, false);
|
||||||
assert.equal(domains.options.footer.enablePagination, false);
|
assert.equal(domains.options.footer.enablePagination, false);
|
||||||
|
|
||||||
@@ -88,16 +90,35 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
|||||||
log: 2,
|
log: 2,
|
||||||
type: 'symlog',
|
type: 'symlog',
|
||||||
});
|
});
|
||||||
assert.deepEqual(speed.targets.map(({ refId }) => refId), ['A', 'B']);
|
assert.equal(speed.title, 'Фактический VPN / Direct');
|
||||||
|
assert.deepEqual(speed.targets.map(({ refId }) => refId), ['A', 'B', 'C', 'D', 'E', 'F']);
|
||||||
assert.ok(speed.targets.every(({ expr }) => expr.includes('device_id=~"$device_id"') && expr.includes('[$__rate_interval]') && !expr.includes('[5m]')));
|
assert.ok(speed.targets.every(({ expr }) => expr.includes('device_id=~"$device_id"') && expr.includes('[$__rate_interval]') && !expr.includes('[5m]')));
|
||||||
assert.ok(speed.fieldConfig.overrides.some((override) => (
|
assert.ok(speed.targets.filter(({ refId }) => ['A', 'B', 'E', 'F'].includes(refId))
|
||||||
override.matcher.id === 'byFrameRefID'
|
.every(({ expr }) => expr.includes('harbor_device_singbox_tracked_bytes_total')));
|
||||||
&& override.matcher.options === 'B'
|
assert.ok(speed.targets.every(({ expr }) => !expr.includes('source=')));
|
||||||
&& override.properties.some(({ id, value }) => id === 'custom.transform' && value === 'negative-Y')
|
assert.ok(speed.targets.filter(({ refId }) => ['C', 'D'].includes(refId)).every(({ expr }) => (
|
||||||
|
expr.includes('harbor_device_singbox_tracked_bytes_total')
|
||||||
|
&& expr.includes('harbor_device_direct_ipv4_packet_bytes_total')
|
||||||
|
&& expr.includes('or vector(0)')
|
||||||
)));
|
)));
|
||||||
|
assert.deepEqual(speed.fieldConfig.overrides.filter((override) => (
|
||||||
|
override.matcher.id === 'byFrameRefID'
|
||||||
|
&& override.properties.some(({ id, value }) => id === 'custom.transform' && value === 'negative-Y')
|
||||||
|
)).map(({ matcher }) => matcher.options), ['B', 'D', 'F']);
|
||||||
assert.equal(speed.options.legend.placement, 'right');
|
assert.equal(speed.options.legend.placement, 'right');
|
||||||
assert.deepEqual(speed.options.legend.calcs, ['lastNotNull', 'mean', 'max']);
|
assert.deepEqual(speed.options.legend.calcs, ['lastNotNull', 'mean', 'max']);
|
||||||
|
|
||||||
|
assert.equal(appliedPolicy.type, 'state-timeline');
|
||||||
|
assert.equal(appliedPolicy.title, 'Применённый режим');
|
||||||
|
assert.match(appliedPolicy.description, /Direct[\s\S]*VPN через явно настроенный Harbor Proxy/);
|
||||||
|
assert.match(appliedPolicy.targets[0].expr, /harbor_device_applied_policy\{device_id=~"\$device_id"\}/);
|
||||||
|
assert.match(appliedPolicy.targets[0].expr, /group_left \(name, ip\)[\s\S]*harbor_device_info/);
|
||||||
|
assert.deepEqual(appliedPolicy.fieldConfig.defaults.mappings[0].options, {
|
||||||
|
0: { color: 'orange', index: 0, text: 'Direct' },
|
||||||
|
1: { color: 'green', index: 1, text: 'VPN' },
|
||||||
|
});
|
||||||
|
assert.equal(appliedPolicy.fieldConfig.defaults.noValue, 'Нет данных');
|
||||||
|
|
||||||
assert.deepEqual(singboxOutbounds.targets.map(({ legendFormat }) => legendFormat), ['VPN', 'Direct', 'Не определено']);
|
assert.deepEqual(singboxOutbounds.targets.map(({ legendFormat }) => legendFormat), ['VPN', 'Direct', 'Не определено']);
|
||||||
assert.ok(singboxOutbounds.targets.every(({ expr }) => (
|
assert.ok(singboxOutbounds.targets.every(({ expr }) => (
|
||||||
expr.includes('harbor_device_singbox_tracked_bytes_total')
|
expr.includes('harbor_device_singbox_tracked_bytes_total')
|
||||||
@@ -105,11 +126,20 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
|||||||
&& expr.includes('[$__range]')
|
&& expr.includes('[$__range]')
|
||||||
)));
|
)));
|
||||||
assert.match(singboxOutbounds.targets[0].expr, /outbound="vpn"/);
|
assert.match(singboxOutbounds.targets[0].expr, /outbound="vpn"/);
|
||||||
assert.match(singboxOutbounds.targets[1].expr, /outbound="direct"/);
|
assert.doesNotMatch(singboxOutbounds.targets[0].expr, /source=/);
|
||||||
|
assert.match(singboxOutbounds.targets[1].expr, /harbor_device_singbox_tracked_bytes_total[\s\S]*outbound="direct"/);
|
||||||
|
assert.match(singboxOutbounds.targets[1].expr, /harbor_device_direct_ipv4_packet_bytes_total/);
|
||||||
|
assert.match(singboxOutbounds.targets[1].expr, /or vector\(0\)/);
|
||||||
assert.match(singboxOutbounds.targets[2].expr, /outbound="unknown"/);
|
assert.match(singboxOutbounds.targets[2].expr, /outbound="unknown"/);
|
||||||
assert.match(directPackets.targets[0].expr, /harbor_device_direct_ipv4_packet_bytes_total\{device_id=~"\$device_id"\}\[\$__range\]/);
|
assert.equal(singboxOutbounds.fieldConfig.defaults.noValue, 'Нет данных');
|
||||||
assert.match(directPackets.description, /нельзя складывать с sing-box tracker bytes/);
|
assert.equal(sourceOutbounds.title, 'Вход × выход sing-box');
|
||||||
assert.match(trafficBySource.targets[0].expr, /harbor_device_traffic_bytes_total\{device_id=~"\$device_id"\}\[\$__range\]/);
|
assert.match(sourceOutbounds.targets[0].expr, /sum by \(source, outbound\)[\s\S]*harbor_device_singbox_tracked_bytes_total/);
|
||||||
|
assert.equal(sourceOutbounds.targets[0].legendFormat, '{{source}} · {{outbound}}');
|
||||||
|
assert.match(sourceOutbounds.description, /proxy · vpn/);
|
||||||
|
assert.deepEqual(directPaths.targets.map(({ legendFormat }) => legendFormat), ['Через sing-box', 'Linux мимо sing-box']);
|
||||||
|
assert.match(directPaths.targets[0].expr, /harbor_device_singbox_tracked_bytes_total[\s\S]*outbound="direct"/);
|
||||||
|
assert.match(directPaths.targets[1].expr, /harbor_device_direct_ipv4_packet_bytes_total/);
|
||||||
|
assert.match(directPaths.description, /приблизительной оценкой/);
|
||||||
|
|
||||||
assert.match(devices.targets[0].expr, /^\(sum by \(device_id\)[\s\S]*increase\(harbor_device_traffic_bytes_total\[\$__range\]\)[\s\S]*> 0[\s\S]*group_left \(name, ip\)/);
|
assert.match(devices.targets[0].expr, /^\(sum by \(device_id\)[\s\S]*increase\(harbor_device_traffic_bytes_total\[\$__range\]\)[\s\S]*> 0[\s\S]*group_left \(name, ip\)/);
|
||||||
assert.doesNotMatch(devices.targets[0].expr, /topk/);
|
assert.doesNotMatch(devices.targets[0].expr, /topk/);
|
||||||
|
|||||||
Reference in New Issue
Block a user