const axios = require('axios'); const cache = require('./cache'); const db = require('./db'); const POLL_INTERVAL = 10000; // 10 seconds async function resolveBlackboxLatency(route) { // Blackbox exporter probe URL // We assume ICMP module for now. If target is a URL, maybe use http_2xx let module = 'icmp'; const 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 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_duration_seconds' ]; let foundLatency = null; for (const metricName of targetMetrics) { let bestLine = null; // First pass: look for phase="rtt" which is the most accurate "ping" for (const line of lines) { if (line.startsWith(metricName) && line.includes('phase="rtt"')) { bestLine = line; break; } } // Second pass: if no rtt phase, look for a line without phases (legacy format) or just the first line if (!bestLine) { for (const line of lines) { if (line.startsWith(metricName)) { // Prefer lines without {} if possible, otherwise take the first one if (!line.includes('{')) { bestLine = line; break; } if (!bestLine) bestLine = line; } } } if (bestLine) { // Regex to capture the number, including scientific notation const regex = new RegExp(`^${metricName}(?:\\{[^}]*\\})?\\s+([\\d.eE+-]+)`); const match = bestLine.match(regex); if (match) { const val = parseFloat(match[1]); if (!isNaN(val)) { foundLatency = val * 1000; // convert to ms break; } } } } // 3. Final decision // If it's a success, use found latency. If success=0 or missing, handle carefully. if (isProbeSuccess && foundLatency !== null) { return foundLatency; } // If probe failed or metrics missing, do not show 0, show null (Measurement in progress/Error) return null; } async function resolveLatencyForRoute(route) { try { if (route.source_type === 'blackbox' || route.type === 'blackbox') { const latency = await resolveBlackboxLatency(route); if (route.id !== undefined) { await cache.set(`latency:route:${route.id}`, latency, 60); } return latency; } return null; } catch (err) { if (route.id !== undefined) { await cache.set(`latency:route:${route.id}`, null, 60); } return null; } } 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 { await resolveLatencyForRoute({ ...route, source_type: 'blackbox' }); } 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 = { pollLatency, resolveLatencyForRoute, start };