Improve device traffic dashboard and domain attribution metrics
This commit is contained in:
@@ -21,6 +21,7 @@ test('domain traffic accumulates connection deltas by device, service and source
|
||||
let response = { connections: [
|
||||
connection('youtube', 'tproxy/tproxy-in', 'r1.googlevideo.com', 10, 100),
|
||||
connection('chatgpt', 'mixed/mixed-in', 'www.chatgpt.com.', 20, 200),
|
||||
connection('hostless', 'tproxy/tproxy-in', '', 5, 50),
|
||||
connection('diagnostics', 'mixed/diagnostics-vpn-in', 'example.com', 30, 300),
|
||||
connection('unknown-device', 'tproxy/tproxy-in', 'example.net', 40, 400, '192.168.50.99'),
|
||||
] };
|
||||
@@ -34,6 +35,7 @@ test('domain traffic accumulates connection deltas by device, service and source
|
||||
response = { connections: [
|
||||
connection('youtube', 'tproxy/tproxy-in', 'r1.googlevideo.com', 15, 130),
|
||||
connection('chatgpt', 'mixed/mixed-in', 'www.chatgpt.com.', 22, 260),
|
||||
connection('hostless', 'tproxy/tproxy-in', '', 7, 70),
|
||||
] };
|
||||
observedAt = new Date('2026-08-08T10:00:02.000Z');
|
||||
await service.refresh();
|
||||
@@ -56,7 +58,20 @@ test('domain traffic accumulates connection deltas by device, service and source
|
||||
uploadBytes: '15',
|
||||
downloadBytes: '130',
|
||||
},
|
||||
{
|
||||
deviceId: id,
|
||||
domain: '_unknown',
|
||||
service: 'Не распознано',
|
||||
source: 'gateway',
|
||||
uploadBytes: '7',
|
||||
downloadBytes: '70',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(service.snapshot().attributionEvents, {
|
||||
unresolved_host: '1',
|
||||
unknown_device: '1',
|
||||
unsupported_source: '1',
|
||||
});
|
||||
assert.equal(service.snapshot().observedAt, observedAt.toISOString());
|
||||
assert.equal(service.snapshot().source.error, null);
|
||||
|
||||
@@ -65,6 +80,38 @@ test('domain traffic accumulates connection deltas by device, service and source
|
||||
assert.equal(service.snapshot().series[1].downloadBytes, '130');
|
||||
});
|
||||
|
||||
test('a hostless connection can become classified without counting its bytes twice', async () => {
|
||||
let response = { connections: [
|
||||
connection('video', 'tproxy/tproxy-in', '', 10, 100),
|
||||
] };
|
||||
const service = createDomainTrafficService({
|
||||
observe: async () => response,
|
||||
devices: () => [device],
|
||||
});
|
||||
|
||||
await service.refresh();
|
||||
response = { connections: [
|
||||
connection('video', 'tproxy/tproxy-in', 'r2.googlevideo.com', 15, 130),
|
||||
] };
|
||||
await service.refresh();
|
||||
|
||||
assert.deepEqual(
|
||||
Object.fromEntries(service.snapshot().series.map((series) => [series.domain, {
|
||||
uploadBytes: series.uploadBytes,
|
||||
downloadBytes: series.downloadBytes,
|
||||
}])),
|
||||
{
|
||||
_unknown: { uploadBytes: '10', downloadBytes: '100' },
|
||||
'googlevideo.com': { uploadBytes: '5', downloadBytes: '30' },
|
||||
},
|
||||
);
|
||||
assert.deepEqual(service.snapshot().attributionEvents, {
|
||||
unresolved_host: '1',
|
||||
unknown_device: '0',
|
||||
unsupported_source: '0',
|
||||
});
|
||||
});
|
||||
|
||||
test('domain traffic is bounded and keeps the last good snapshot on source failure', async () => {
|
||||
let fail = false;
|
||||
const service = createDomainTrafficService({
|
||||
|
||||
@@ -29,6 +29,11 @@ const snapshot = {
|
||||
domainTraffic: {
|
||||
observedAt,
|
||||
overflowConnections: '2',
|
||||
attributionEvents: {
|
||||
unresolved_host: '3',
|
||||
unknown_device: '4',
|
||||
unsupported_source: '5',
|
||||
},
|
||||
series: [{
|
||||
deviceId: 'dev_0011223344556677',
|
||||
domain: 'chatgpt.com',
|
||||
@@ -54,6 +59,10 @@ test('Prometheus exposition keeps exact counters, stable identity and escaped na
|
||||
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/);
|
||||
assert.match(output, /# TYPE harbor_domain_traffic_attribution_events_total counter/);
|
||||
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unresolved_host"\} 3/);
|
||||
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unknown_device"\} 4/);
|
||||
assert.match(output, /harbor_domain_traffic_attribution_events_total\{outcome="unsupported_source"\} 5/);
|
||||
assert.doesNotMatch(output, /harbor_device_domain_traffic_bytes_total\{[^\n]*name=/);
|
||||
assert.equal(output.endsWith('\n'), true);
|
||||
});
|
||||
|
||||
@@ -37,6 +37,12 @@ test('client exposes one local proxy and routes local exceptions before the sele
|
||||
assert.equal(config.inbounds[0].listen_port, 8082);
|
||||
assert.equal(config.inbounds[1].listen_port, 18080);
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{
|
||||
inbound: ['mixed-in', 'diagnostics-vpn-in'],
|
||||
action: 'sniff',
|
||||
sniffer: ['http', 'tls', 'quic'],
|
||||
timeout: '1s',
|
||||
},
|
||||
{ inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ domain: ['example.com'], outbound: 'direct' },
|
||||
@@ -53,6 +59,12 @@ test('client keeps its local proxy but routes directly when Harbor Gateway is ah
|
||||
});
|
||||
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{
|
||||
inbound: ['mixed-in', 'diagnostics-vpn-in'],
|
||||
action: 'sniff',
|
||||
sniffer: ['http', 'tls', 'quic'],
|
||||
timeout: '1s',
|
||||
},
|
||||
{ inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ inbound: ['mixed-in'], outbound: 'direct' },
|
||||
|
||||
@@ -37,10 +37,15 @@ test('gateway routes .ru domains directly and other traffic through the selected
|
||||
tag: 'diagnostics-vpn-in',
|
||||
listen: '127.0.0.1',
|
||||
listen_port: 18080,
|
||||
sniff: true,
|
||||
set_system_proxy: false,
|
||||
});
|
||||
assert.deepEqual(config.route.rules, [
|
||||
{
|
||||
inbound: ['tproxy-in', 'mixed-in', 'diagnostics-vpn-in'],
|
||||
action: 'sniff',
|
||||
sniffer: ['http', 'tls', 'quic'],
|
||||
timeout: '1s',
|
||||
},
|
||||
{ inbound: ['diagnostics-vpn-in'], outbound: 'test-vpn' },
|
||||
{ domain_suffix: ['ru'], outbound: 'direct' },
|
||||
{ inbound: ['tproxy-in'], outbound: 'test-vpn' },
|
||||
|
||||
@@ -432,7 +432,13 @@ setInterval(() => {}, 60_000);
|
||||
]);
|
||||
assert.equal(routed.state.route.localRulesPendingRestart, false);
|
||||
assert.deepEqual(routed.state.route.activeLocalRules, routed.state.route.localRules);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'sing-box-config.json'))).route.rules.slice(0, 3), [
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'sing-box-config.json'))).route.rules.slice(0, 4), [
|
||||
{
|
||||
inbound: ['mixed-in', 'diagnostics-vpn-in'],
|
||||
action: 'sniff',
|
||||
sniffer: ['http', 'tls', 'quic'],
|
||||
timeout: '1s',
|
||||
},
|
||||
{ inbound: ['diagnostics-vpn-in'], outbound: testServerId },
|
||||
{ domain: ['example.com'], outbound: 'direct' },
|
||||
{ domain_suffix: ['example.org'], outbound: 'direct' },
|
||||
|
||||
Reference in New Issue
Block a user