Add Prometheus and Grafana monitoring instructions
This commit is contained in:
+7
-1
@@ -48,6 +48,7 @@ import {
|
||||
} from './services/deviceInventoryService.js';
|
||||
import { buildGatewayVersionInfo, buildVersionInfo } from './version.js';
|
||||
import { createConnectivityDiagnosticsService } from './services/connectivityDiagnosticsService.js';
|
||||
import { sendPrometheusMetrics } from './prometheusMetrics.js';
|
||||
|
||||
const MAX_BODY_BYTES = 1_000_000;
|
||||
const SUBSCRIPTION_REFRESH_INTERVAL_MS = 15 * 60 * 1000;
|
||||
@@ -860,7 +861,12 @@ function serveStatic(req, res) {
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
return req.url?.startsWith('/api/')
|
||||
const requestUrl = new URL(req.url, `http://localhost:${settings.port}`);
|
||||
if (requestUrl.pathname === '/metrics') {
|
||||
if (!deviceInventory || req.method !== 'GET') throw new HarborError('ENDPOINT_NOT_FOUND');
|
||||
return sendPrometheusMetrics(res, deviceInventory.snapshot());
|
||||
}
|
||||
return requestUrl.pathname.startsWith('/api/')
|
||||
? await handleApi(req, res)
|
||||
: serveStatic(req, res);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
const COUNTER_PATTERN = /^\d+$/;
|
||||
|
||||
const labelValue = (value) => String(value ?? '')
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('"', '\\"');
|
||||
|
||||
const labels = (values) => Object.entries(values)
|
||||
.map(([key, value]) => `${key}="${labelValue(value)}"`)
|
||||
.join(',');
|
||||
|
||||
function counter(value) {
|
||||
const decimal = String(value ?? '');
|
||||
if (!COUNTER_PATTERN.test(decimal)) throw new Error(`Invalid Prometheus counter: ${decimal}`);
|
||||
return decimal;
|
||||
}
|
||||
|
||||
function timestamp(value) {
|
||||
const milliseconds = Date.parse(value);
|
||||
return Number.isFinite(milliseconds) ? String(milliseconds / 1000) : null;
|
||||
}
|
||||
|
||||
function metric(lines, name, metricLabels, value) {
|
||||
lines.push(`${name}{${labels(metricLabels)}} ${value}`);
|
||||
}
|
||||
|
||||
export function renderPrometheusMetrics(snapshot) {
|
||||
const lines = [
|
||||
'# HELP harbor_traffic_bytes_total Total traffic accounted by Harbor.',
|
||||
'# TYPE harbor_traffic_bytes_total counter',
|
||||
];
|
||||
metric(lines, 'harbor_traffic_bytes_total', { source: 'gateway' }, counter(snapshot?.traffic?.gatewayBytes));
|
||||
metric(lines, 'harbor_traffic_bytes_total', { source: 'proxy' }, counter(snapshot?.traffic?.proxyBytes));
|
||||
|
||||
const globalFreshness = [
|
||||
['gateway', snapshot?.traffic?.gatewayObservedAt],
|
||||
['proxy', snapshot?.traffic?.proxyObservedAt],
|
||||
].map(([source, observedAt]) => [source, timestamp(observedAt)]).filter(([, observedAt]) => observedAt);
|
||||
if (globalFreshness.length) {
|
||||
lines.push(
|
||||
'# HELP harbor_traffic_last_observed_timestamp_seconds Unix timestamp of the last successful Harbor traffic observation.',
|
||||
'# TYPE harbor_traffic_last_observed_timestamp_seconds gauge',
|
||||
);
|
||||
for (const [source, observedAt] of globalFreshness) {
|
||||
metric(lines, 'harbor_traffic_last_observed_timestamp_seconds', { source }, observedAt);
|
||||
}
|
||||
}
|
||||
|
||||
const devices = Array.isArray(snapshot?.devices) ? snapshot.devices : [];
|
||||
if (devices.length) {
|
||||
lines.push(
|
||||
'# HELP harbor_device_info Current Harbor device identity metadata.',
|
||||
'# TYPE harbor_device_info gauge',
|
||||
);
|
||||
for (const device of devices) {
|
||||
metric(lines, 'harbor_device_info', {
|
||||
device_id: device.id,
|
||||
name: device.alias || device.hostname || device.ip || device.id,
|
||||
ip: device.ip || '',
|
||||
}, '1');
|
||||
}
|
||||
}
|
||||
|
||||
const deviceTraffic = devices.flatMap((device) => [
|
||||
timestamp(device.trafficObservedAt)
|
||||
? [device, 'gateway', timestamp(device.trafficObservedAt), device.uploadBytes, device.downloadBytes]
|
||||
: null,
|
||||
timestamp(device.proxyTrafficObservedAt)
|
||||
? [device, 'proxy', timestamp(device.proxyTrafficObservedAt), device.proxyUploadBytes, device.proxyDownloadBytes]
|
||||
: null,
|
||||
].filter(Boolean));
|
||||
if (deviceTraffic.length) {
|
||||
lines.push(
|
||||
'# HELP harbor_device_traffic_bytes_total Total traffic accounted by Harbor for a device.',
|
||||
'# TYPE harbor_device_traffic_bytes_total counter',
|
||||
);
|
||||
for (const [device, source, , uploadBytes, downloadBytes] of deviceTraffic) {
|
||||
metric(lines, 'harbor_device_traffic_bytes_total', {
|
||||
device_id: device.id,
|
||||
source,
|
||||
direction: 'download',
|
||||
}, counter(downloadBytes));
|
||||
metric(lines, 'harbor_device_traffic_bytes_total', {
|
||||
device_id: device.id,
|
||||
source,
|
||||
direction: 'upload',
|
||||
}, counter(uploadBytes));
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'# HELP harbor_device_traffic_last_observed_timestamp_seconds Unix timestamp of the last successful device traffic observation.',
|
||||
'# TYPE harbor_device_traffic_last_observed_timestamp_seconds gauge',
|
||||
);
|
||||
for (const [device, source, observedAt] of deviceTraffic) {
|
||||
metric(lines, 'harbor_device_traffic_last_observed_timestamp_seconds', {
|
||||
device_id: device.id,
|
||||
source,
|
||||
}, observedAt);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
export function sendPrometheusMetrics(res, snapshot) {
|
||||
const body = renderPrometheusMetrics(snapshot);
|
||||
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' });
|
||||
res.end(body);
|
||||
}
|
||||
Reference in New Issue
Block a user