Files
PromdataPanel/server/latency-service.js
2026-04-06 01:07:20 +08:00

126 lines
5.1 KiB
JavaScript

const axios = require('axios');
const cache = require('./cache');
const db = require('./db');
const POLL_INTERVAL = 10000; // 10 seconds
async function pollLatency() {
try {
const [routes] = await db.query(`
SELECT r.*, s.url
FROM latency_routes r
JOIN prometheus_sources s ON r.source_id = s.id
WHERE s.type = 'blackbox'
`);
if (routes.length === 0) return;
// Poll each route
await Promise.allSettled(routes.map(async (route) => {
try {
// Blackbox exporter probe URL
// We assume ICMP module for now. If target is a URL, maybe use http_2xx
let module = 'icmp';
let target = route.latency_target;
if (target.startsWith('http://') || target.startsWith('https://')) {
module = 'http_2xx';
}
const probeUrl = `${route.url.replace(/\/+$/, '')}/probe?module=${module}&target=${encodeURIComponent(target)}`;
const startTime = Date.now();
const response = await axios.get(probeUrl, {
timeout: 5000,
responseType: 'text',
validateStatus: false
});
if (typeof response.data !== 'string') {
throw new Error('Response data is not a string');
}
const lines = response.data.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
// 1. Check if the probe was successful
let isProbeSuccess = false;
for (const line of lines) {
if (/^probe_success(\{.*\})?\s+1/.test(line)) {
isProbeSuccess = true;
break;
}
}
// 2. Extract latency from priority metrics
const targetMetrics = [
'probe_icmp_duration_seconds',
'probe_http_duration_seconds',
'probe_dns_lookup_time_seconds',
'probe_duration_seconds'
];
let foundLatency = null;
const encodedTarget = target.toLowerCase();
for (const metricName of targetMetrics) {
for (const line of lines) {
if (!line.startsWith(metricName)) continue;
// Precise matching for the specific target if multiple targets exist in data
// Handles: metric{instance="target"} value OR metric value
const regex = new RegExp(`^${metricName}(?:\\{[^}]*\\})?\\s+([\\d.eE+-]+)`);
const match = line.match(regex);
if (match) {
// If there are labels, verify they relate to our target (if target label exists)
if (line.includes('{')) {
const labelsPart = line.substring(line.indexOf('{') + 1, line.lastIndexOf('}')).toLowerCase();
// Only check if the labels contain our target to be safe,
// though /probe usually only returns one target anyway.
if (labelsPart.includes(encodedTarget) || labelsPart.includes('instance') || labelsPart.includes('target')) {
const val = parseFloat(match[1]);
if (!isNaN(val)) foundLatency = val * 1000;
}
} else {
const val = parseFloat(match[1]);
if (!isNaN(val)) foundLatency = val * 1000;
}
}
if (foundLatency !== null) break;
}
if (foundLatency !== null) break;
}
// 3. Final decision
// If it's a success, use found latency. If success=0 or missing, handle carefully.
if (isProbeSuccess && foundLatency !== null) {
latency = foundLatency;
} else {
// If probe failed or metrics missing, do not show 0, show null (Measurement in progress/Error)
latency = null;
}
// Save to Valkey
await cache.set(`latency:route:${route.id}`, latency, 60);
} catch (err) {
await cache.set(`latency:route:${route.id}`, null, 60);
}
}));
} catch (err) {
console.error('[Latency] Service error:', err.message);
}
}
let intervalId = null;
function start() {
if (intervalId) clearInterval(intervalId);
pollLatency(); // initial run
intervalId = setInterval(pollLatency, POLL_INTERVAL);
console.log('[Latency] Background service started (polling Blackbox Exporter directly)');
}
module.exports = {
start
};