Clarify VPN and Direct traffic metrics in Grafana dashboard
This commit is contained in:
@@ -136,6 +136,22 @@ test('domain traffic accumulates connection deltas by device, service and source
|
||||
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, {
|
||||
unresolved_host: '1',
|
||||
unknown_device: '1',
|
||||
|
||||
@@ -27,6 +27,7 @@ const snapshot = {
|
||||
proxyUploadBytes: '0',
|
||||
proxyDownloadBytes: '0',
|
||||
proxyTrafficObservedAt: null,
|
||||
appliedPolicy: 'direct',
|
||||
}],
|
||||
directTraffic: {
|
||||
observedAt,
|
||||
@@ -80,6 +81,12 @@ const snapshot = {
|
||||
outbound: 'vpn',
|
||||
uploadBytes: '55',
|
||||
downloadBytes: '666',
|
||||
}, {
|
||||
deviceId: 'dev_0011223344556677',
|
||||
source: 'proxy',
|
||||
outbound: 'vpn',
|
||||
uploadBytes: '12',
|
||||
downloadBytes: '345',
|
||||
}],
|
||||
series: [{
|
||||
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, /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_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.doesNotMatch(output, /harbor_device_traffic_bytes_total\{[^\n]*name=/);
|
||||
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, /# 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="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_domain_traffic_last_observed_timestamp_seconds 1786183200/);
|
||||
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);
|
||||
invalidShadow.domainTraffic.source.shadow.uploadDifferenceBytes = '1.5';
|
||||
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() {
|
||||
|
||||
@@ -43,15 +43,16 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
||||
const services = byId.get(8);
|
||||
const domains = byId.get(9);
|
||||
const totalTraffic = byId.get(1);
|
||||
const trafficBySource = byId.get(4);
|
||||
const sourceOutbounds = byId.get(4);
|
||||
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);
|
||||
|
||||
assert.equal(dashboard.title, 'Harbor Gateway: трафик');
|
||||
assert.equal(dashboard.refresh, '30s');
|
||||
assert.equal(dashboard.version, 9);
|
||||
assert.deepEqual(dashboard.panels.map(({ id }) => id), [10, 6, 7, 1, 15, 2, 3, 5, 12, 8, 9, 13]);
|
||||
assert.equal(dashboard.version, 10);
|
||||
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), [
|
||||
'Обзор',
|
||||
'Куда уходит трафик',
|
||||
@@ -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(14), false);
|
||||
|
||||
assert.deepEqual(speed.gridPos, { h: 12, w: 16, x: 0, y: 5 });
|
||||
assert.deepEqual(devices.gridPos, { h: 12, w: 8, x: 16, y: 5 });
|
||||
assert.deepEqual(services.gridPos, { h: 15, w: 9, x: 0, y: 18 });
|
||||
assert.deepEqual(domains.gridPos, { h: 15, w: 15, x: 9, y: 18 });
|
||||
assert.deepEqual(technical.gridPos, { h: 1, w: 24, x: 0, y: 33 });
|
||||
assert.deepEqual(appliedPolicy.gridPos, { h: 4, w: 16, x: 0, y: 5 });
|
||||
assert.deepEqual(speed.gridPos, { h: 12, w: 16, x: 0, y: 9 });
|
||||
assert.deepEqual(devices.gridPos, { h: 16, w: 8, x: 16, y: 5 });
|
||||
assert.deepEqual(services.gridPos, { h: 15, w: 9, x: 0, y: 22 });
|
||||
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(domains.options.footer.enablePagination, false);
|
||||
|
||||
@@ -88,16 +90,35 @@ test('Grafana dashboard uses a compact user-first layout and one device scope',
|
||||
log: 2,
|
||||
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.fieldConfig.overrides.some((override) => (
|
||||
override.matcher.id === 'byFrameRefID'
|
||||
&& override.matcher.options === 'B'
|
||||
&& override.properties.some(({ id, value }) => id === 'custom.transform' && value === 'negative-Y')
|
||||
assert.ok(speed.targets.filter(({ refId }) => ['A', 'B', 'E', 'F'].includes(refId))
|
||||
.every(({ expr }) => expr.includes('harbor_device_singbox_tracked_bytes_total')));
|
||||
assert.ok(speed.targets.every(({ expr }) => !expr.includes('source=')));
|
||||
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.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.ok(singboxOutbounds.targets.every(({ expr }) => (
|
||||
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]')
|
||||
)));
|
||||
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(directPackets.targets[0].expr, /harbor_device_direct_ipv4_packet_bytes_total\{device_id=~"\$device_id"\}\[\$__range\]/);
|
||||
assert.match(directPackets.description, /нельзя складывать с sing-box tracker bytes/);
|
||||
assert.match(trafficBySource.targets[0].expr, /harbor_device_traffic_bytes_total\{device_id=~"\$device_id"\}\[\$__range\]/);
|
||||
assert.equal(singboxOutbounds.fieldConfig.defaults.noValue, 'Нет данных');
|
||||
assert.equal(sourceOutbounds.title, 'Вход × выход sing-box');
|
||||
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.doesNotMatch(devices.targets[0].expr, /topk/);
|
||||
|
||||
Reference in New Issue
Block a user