Add native traffic inspection to Harbor Connect and Gateway

This commit is contained in:
2026-08-31 05:19:15 +03:00
parent 116686a138
commit 4d066cb879
62 changed files with 10975 additions and 220 deletions
+74 -1
View File
@@ -1,6 +1,12 @@
import type { ServerResponse } from 'node:http';
const COUNTER_PATTERN = /^\d+$/;
const SIGNED_DECIMAL_PATTERN = /^-?\d+$/;
const COLLECTOR_MODES = new Set(['snapshot', 'shadow', 'native']);
const COLLECTOR_WRITERS = new Set(['snapshot', 'native']);
const COLLECTOR_STATES = new Set([
'connecting', 'live', 'degraded', 'stale', 'stopped', 'incompatible', 'disabled',
]);
const labelValue = (value: unknown) => String(value ?? '')
.replaceAll('\\', '\\\\')
@@ -23,6 +29,19 @@ function counter(value: unknown) {
return decimal;
}
function signedGauge(value: unknown) {
const decimal = String(value ?? '');
if (!SIGNED_DECIMAL_PATTERN.test(decimal)) throw new Error(`Invalid Prometheus gauge: ${decimal}`);
return decimal;
}
function safeInteger(value: unknown, { signed = false } = {}) {
if (!Number.isSafeInteger(value) || (!signed && Number(value) < 0)) {
throw new Error(`Invalid Prometheus gauge: ${String(value)}`);
}
return String(value);
}
function timestamp(value: unknown) {
const milliseconds = Date.parse(String(value ?? ''));
return Number.isFinite(milliseconds) ? String(milliseconds / 1000) : null;
@@ -151,10 +170,64 @@ export function renderPrometheusMetrics(value: unknown) {
}
const domainTraffic = record(snapshot.domainTraffic);
const collectorSource = record(domainTraffic.source);
const hasCollectorDiagnostics = ['mode', 'writer', 'native', 'shadow']
.some((field) => Object.hasOwn(collectorSource, field));
if (hasCollectorDiagnostics) {
const source = collectorSource;
const mode = String(source.mode || '');
const writer = String(source.writer || '');
if (!COLLECTOR_MODES.has(mode) || !COLLECTOR_WRITERS.has(writer)) {
throw new Error('Invalid traffic collector labels');
}
lines.push(
'# HELP harbor_traffic_collector_info Current Gateway traffic collector mode and canonical writer.',
'# TYPE harbor_traffic_collector_info gauge',
);
metric(lines, 'harbor_traffic_collector_info', { mode, writer }, '1');
if (source.native !== null) {
const native = record(source.native);
const state = String(native.state || '');
if (!COLLECTOR_STATES.has(state)) throw new Error('Invalid traffic collector state');
lines.push(
'# HELP harbor_traffic_collector_state Current native traffic collector state.',
'# TYPE harbor_traffic_collector_state gauge',
);
metric(lines, 'harbor_traffic_collector_state', { state }, '1');
lines.push(
'# HELP harbor_traffic_collector_unattributed_bytes Native traffic bytes not attributed to a lifecycle connection.',
'# TYPE harbor_traffic_collector_unattributed_bytes gauge',
);
metric(lines, 'harbor_traffic_collector_unattributed_bytes', { direction: 'download' }, counter(native.unattributedDownloadBytes));
metric(lines, 'harbor_traffic_collector_unattributed_bytes', { direction: 'upload' }, counter(native.unattributedUploadBytes));
}
if (source.shadow !== null) {
const shadow = record(source.shadow);
lines.push(
'# HELP harbor_traffic_shadow_active_difference Native active connections minus snapshot active connections.',
'# TYPE harbor_traffic_shadow_active_difference gauge',
`harbor_traffic_shadow_active_difference ${safeInteger(shadow.activeDifference, { signed: true })}`,
'# HELP harbor_traffic_shadow_difference_bytes Native traffic bytes minus snapshot traffic bytes.',
'# TYPE harbor_traffic_shadow_difference_bytes gauge',
);
metric(lines, 'harbor_traffic_shadow_difference_bytes', { direction: 'download' }, signedGauge(shadow.downloadDifferenceBytes));
metric(lines, 'harbor_traffic_shadow_difference_bytes', { direction: 'upload' }, signedGauge(shadow.uploadDifferenceBytes));
lines.push(
'# HELP harbor_traffic_shadow_route_mismatches Route aggregate keys that differ between native and snapshot projections.',
'# TYPE harbor_traffic_shadow_route_mismatches gauge',
`harbor_traffic_shadow_route_mismatches ${safeInteger(shadow.routeMismatches)}`,
'# HELP harbor_traffic_shadow_device_mismatches Device aggregate keys that differ between native and snapshot projections.',
'# TYPE harbor_traffic_shadow_device_mismatches gauge',
`harbor_traffic_shadow_device_mismatches ${safeInteger(shadow.deviceMismatches)}`,
);
}
}
const trackedSeries = Array.isArray(domainTraffic.tracked) ? domainTraffic.tracked.map(record) : [];
if (trackedSeries.length) {
lines.push(
'# HELP harbor_singbox_tracked_bytes_total Bytes observed by the sing-box TCP/UDP tracker; excludes IP and tunnel overhead and may miss short connections.',
'# HELP harbor_singbox_tracked_bytes_total Bytes observed by the configured sing-box traffic collector; excludes IP and tunnel overhead.',
'# TYPE harbor_singbox_tracked_bytes_total counter',
);
for (const series of trackedSeries) {