Add domain traffic metrics and Grafana dashboard
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-08 01:57:00 +03:00
parent 10888ac012
commit ca53b671ee
20 changed files with 612 additions and 17 deletions
+103
View File
@@ -0,0 +1,103 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
classifyDomain,
createDomainTrafficService,
} from '../../src/server/services/domainTrafficService.js';
import { deviceId } from '../../src/server/services/deviceInventoryService.js';
const mac = '00:11:22:33:44:55';
const id = deviceId(mac);
const device = { ip: '192.168.50.7', mac };
const connection = (connectionId, type, host, upload, download, sourceIP = device.ip) => ({
id: connectionId,
metadata: { type, host, sourceIP },
upload,
download,
});
test('domain traffic accumulates connection deltas by device, service and source', async () => {
let observedAt = new Date('2026-08-08T10:00:00.000Z');
let response = { connections: [
connection('youtube', 'tproxy/tproxy-in', 'r1.googlevideo.com', 10, 100),
connection('chatgpt', 'mixed/mixed-in', 'www.chatgpt.com.', 20, 200),
connection('diagnostics', 'mixed/diagnostics-vpn-in', 'example.com', 30, 300),
connection('unknown-device', 'tproxy/tproxy-in', 'example.net', 40, 400, '192.168.50.99'),
] };
const service = createDomainTrafficService({
observe: async () => response,
devices: () => [device],
now: () => observedAt,
});
await service.refresh();
response = { connections: [
connection('youtube', 'tproxy/tproxy-in', 'r1.googlevideo.com', 15, 130),
connection('chatgpt', 'mixed/mixed-in', 'www.chatgpt.com.', 22, 260),
] };
observedAt = new Date('2026-08-08T10:00:02.000Z');
await service.refresh();
await service.refresh();
assert.deepEqual(service.snapshot().series, [
{
deviceId: id,
domain: 'chatgpt.com',
service: 'OpenAI / ChatGPT',
source: 'proxy',
uploadBytes: '22',
downloadBytes: '260',
},
{
deviceId: id,
domain: 'googlevideo.com',
service: 'YouTube',
source: 'gateway',
uploadBytes: '15',
downloadBytes: '130',
},
]);
assert.equal(service.snapshot().observedAt, observedAt.toISOString());
assert.equal(service.snapshot().source.error, null);
response = { connections: [] };
await service.refresh();
assert.equal(service.snapshot().series[1].downloadBytes, '130');
});
test('domain traffic is bounded and keeps the last good snapshot on source failure', async () => {
let fail = false;
const service = createDomainTrafficService({
observe: async () => {
if (fail) throw new Error('Clash API unavailable');
return { connections: [
connection('first', 'tproxy/tproxy-in', 'one.example', 1, 10),
connection('second', 'tproxy/tproxy-in', 'two.example', 2, 20),
] };
},
devices: () => [device],
maxSeries: 3,
});
await service.refresh();
await service.refresh();
assert.deepEqual(
Object.fromEntries(service.snapshot().series.map(({ domain, downloadBytes }) => [domain, downloadBytes])),
{ 'one.example': '10', _other: '20' },
);
assert.equal(service.snapshot().overflowConnections, '1');
assert.equal(service.snapshot().series.find(({ domain }) => domain === '_other').deviceId, '_other');
assert.ok(service.snapshot().series.length <= 3);
fail = true;
await assert.rejects(service.refresh(), /Clash API unavailable/);
assert.equal(service.snapshot().series.find(({ domain }) => domain === 'one.example').downloadBytes, '10');
assert.equal(service.snapshot().source.error, 'Clash API unavailable');
});
test('domain classification normalizes known services and rejects IP or malformed labels', () => {
assert.deepEqual(classifyDomain('WWW.YouTube.com.'), { domain: 'youtube.com', service: 'YouTube' });
assert.deepEqual(classifyDomain('api.example.org'), { domain: 'api.example.org', service: 'api.example.org' });
assert.equal(classifyDomain('192.0.2.1'), null);
assert.equal(classifyDomain('broken_label.example'), null);
});