feat(PingServicesScheduler): implement ping services scheduler endpoints and cache refresh logic for improved service monitoring
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m29s

This commit is contained in:
2026-02-22 20:40:57 +07:00
parent 4c7af43be7
commit 431314b973
4 changed files with 249 additions and 50 deletions
+85 -50
View File
@@ -368,6 +368,88 @@ async function loadUiSettingsSync() {
const PING_SERVICES_CACHE_KEY_PREFIX = 'ping-services/cache_';
/**
* Обновить кеш пинг-сервисов: выполнить пинги по списку из настроек и записать результат в S3.
* Используется планировщиком и при промахе кеша в getPingServices.
* @returns {Promise<Record<string, { id: string, name: string, host: string, ms: number|null }>>} byId
*/
async function refreshPingServicesCache() {
const uiSettings = await loadUiSettingsSync();
const servicesList = getPingServicesListFromSettings(uiSettings);
const fallbackPayload = {};
servicesList.forEach((s) => { fallbackPayload[s.id] = { id: s.id, name: s.name, host: s.host, ms: null }; });
const viaRouter = String(uiSettings.pingServicesSource || 'web').toLowerCase() === 'router';
const cacheSeconds = Math.max(0, parseInt(uiSettings.pingServicesCacheSeconds, 10) || 0);
let serverId = null;
let gatewayIp = null;
if (viaRouter) {
const { readServersFromS3 } = require('./serversRoutes');
const servers = await readServersFromS3();
const routerServers = servers.filter(
(s) => s && (String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home')
);
serverId = (uiSettings.pingServicesServerId && String(uiSettings.pingServicesServerId).trim()) || (routerServers[0] && (routerServers[0].id || routerServers[0].dns || routerServers[0].ip));
gatewayIp = (uiSettings.pingServicesGatewayIp && String(uiSettings.pingServicesGatewayIp).trim()) || null;
}
const cacheKey = cacheSeconds > 0
? PING_SERVICES_CACHE_KEY_PREFIX + (viaRouter ? 'router' : 'web') + '_' + String(serverId || '').replace(/[^a-zA-Z0-9.-]/g, '_') + '_' + String(gatewayIp || '').replace(/[^a-zA-Z0-9.]/g, '_')
: null;
if (viaRouter) {
const { runPingViaRouter } = require('./mikrotikConfigRoutes');
const { readServersFromS3 } = require('./serversRoutes');
const servers = await readServersFromS3();
const routerServers = servers.filter(
(s) => s && (String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home')
);
let gatewayIpResolved = gatewayIp;
if (!gatewayIpResolved && serverId && routerServers.length > 0) {
const server = routerServers.find((s) => (s.id || s.dns || s.ip) === serverId) || routerServers[0];
const gateways = Array.isArray(server.gateways) ? server.gateways : [];
const primary = gateways.find((g) => g && g.primary) || gateways[0];
gatewayIpResolved = primary && (primary.ip || primary.remoteIp) ? (primary.ip || primary.remoteIp) : null;
}
if (!serverId) {
console.warn('[ping-services] router mode: no serverId (no jumphost/home in settings or in servers list)');
return fallbackPayload;
}
const results = await Promise.all(
servicesList.map(async (svc) => {
try {
const result = await runPingViaRouter(serverId, gatewayIpResolved || null, svc.host, 3);
const ms = typeof result.avgMs === 'number' ? Math.round(result.avgMs) : null;
return { id: svc.id, name: svc.name, host: svc.host, ms };
} catch (err) {
console.warn('[ping-services] runPingViaRouter failed for', svc.host, err?.message || err);
return { id: svc.id, name: svc.name, host: svc.host, ms: null };
}
})
);
const byId = {};
results.forEach((r) => { byId[r.id] = r; });
if (cacheKey) {
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
}
return byId;
}
const results = await Promise.all(
servicesList.map(async (svc) => {
const ms = await measureTcpRtt(svc.host, svc.port, 6000);
return { id: svc.id, name: svc.name, host: svc.host, ms };
})
);
const byId = {};
results.forEach((r) => { byId[r.id] = r; });
if (cacheKey) {
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
}
return byId;
}
// GET /api/ping-services-list — список сервисов для пинга из настроек (для дашборда и редактора)
async function getPingServicesList(req, res) {
try {
@@ -420,56 +502,8 @@ async function getPingServices(req, res) {
} catch (_) {}
}
if (viaRouter) {
const { runPingViaRouter } = require('./mikrotikConfigRoutes');
const { readServersFromS3 } = require('./serversRoutes');
const servers = await readServersFromS3();
const routerServers = servers.filter(
(s) => s && (String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home')
);
let gatewayIpResolved = gatewayIp;
if (!gatewayIpResolved && serverId && routerServers.length > 0) {
const server = routerServers.find((s) => (s.id || s.dns || s.ip) === serverId) || routerServers[0];
const gateways = Array.isArray(server.gateways) ? server.gateways : [];
const primary = gateways.find((g) => g && g.primary) || gateways[0];
gatewayIpResolved = primary && (primary.ip || primary.remoteIp) ? (primary.ip || primary.remoteIp) : null;
}
if (!serverId) {
console.warn('[ping-services] router mode: no serverId (no jumphost/home in settings or in servers list)');
return res.json(fallbackPayload);
}
const results = await Promise.all(
servicesList.map(async (svc) => {
try {
const result = await runPingViaRouter(serverId, gatewayIpResolved || null, svc.host, 3);
const ms = typeof result.avgMs === 'number' ? Math.round(result.avgMs) : null;
return { id: svc.id, name: svc.name, host: svc.host, ms };
} catch (err) {
console.warn('[ping-services] runPingViaRouter failed for', svc.host, err?.message || err);
return { id: svc.id, name: svc.name, host: svc.host, ms: null };
}
})
);
const byId = {};
results.forEach((r) => { byId[r.id] = r; });
if (cacheKey) {
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
}
return res.json(byId);
}
const results = await Promise.all(
servicesList.map(async (svc) => {
const ms = await measureTcpRtt(svc.host, svc.port, 6000);
return { id: svc.id, name: svc.name, host: svc.host, ms };
})
);
const byId = {};
results.forEach((r) => { byId[r.id] = r; });
if (cacheKey) {
writeS3JsonObject(cacheKey, { byId, cachedAt: Date.now() }).catch((err) => console.warn('[ping-services] cache write failed:', err?.message));
}
res.json(byId);
const byId = await refreshPingServicesCache();
return res.json(byId);
} catch (e) {
console.error('ping-services error', e);
const fallback = {};
@@ -695,5 +729,6 @@ module.exports = {
postUiSettings,
getPingServicesList,
getPingServices,
refreshPingServicesCache,
};
+57
View File
@@ -5,6 +5,7 @@
const { sendError, sendOk } = require('../middleware/errorHandler');
const { readS3TextObject, writeS3JsonObject } = require('../services/s3Service');
const networkMapScheduler = require('../services/networkMapScheduler');
const pingServicesScheduler = require('../services/pingServicesScheduler');
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
@@ -17,6 +18,11 @@ const SCHEDULER_KEYS = [
'networkMapSchedulerLogMaxLines',
];
const PING_SERVICES_SCHEDULER_KEYS = [
'pingServicesSchedulerEnabled',
'pingServicesSchedulerIntervalMinutes',
];
async function readUiSettings() {
try {
const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
@@ -124,10 +130,61 @@ async function getNetworkMapCache(req, res) {
}
}
// === Ping services scheduler ===
/** GET /api/scheduler/ping-services/settings */
async function getPingServicesSchedulerSettings(req, res) {
try {
const ui = await readUiSettings();
const settings = pingServicesScheduler.getSettingsFromUiSettings(ui);
const nextRunAt = pingServicesScheduler.getNextRunAt(settings);
return res.json({ ...settings, nextRunAt: nextRunAt || undefined });
} catch (e) {
console.error('[scheduler] getPingServicesSettings', e);
return sendError(res, 500, 'Не удалось прочитать настройки', 'E_SCHEDULER');
}
}
/** PATCH /api/scheduler/ping-services/settings */
async function patchPingServicesSchedulerSettings(req, res) {
try {
const body = req.body || {};
const current = await readUiSettings();
for (const key of PING_SERVICES_SCHEDULER_KEYS) {
if (Object.prototype.hasOwnProperty.call(body, key)) {
current[key] = body[key];
}
}
await writeS3JsonObject(UI_SETTINGS_KEY, current);
const settings = pingServicesScheduler.getSettingsFromUiSettings(current);
return res.json(settings);
} catch (e) {
console.error('[scheduler] patchPingServicesSettings', e);
return sendError(res, 500, 'Не удалось сохранить настройки', 'E_SCHEDULER');
}
}
/** POST /api/scheduler/ping-services/run-now */
async function runPingServicesSchedulerNow(req, res) {
try {
const { refreshPingServicesCache } = require('./miscRoutes');
refreshPingServicesCache().catch((err) => {
console.error('[scheduler] ping-services run-now failed', err);
});
return sendOk(res, { message: 'Обновление кеша пинг-сервисов запущено в фоне' });
} catch (e) {
console.error('[scheduler] ping-services run-now', e);
return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER');
}
}
module.exports = {
getNetworkMapSchedulerSettings,
patchNetworkMapSchedulerSettings,
getNetworkMapSchedulerLogs,
runNetworkMapSchedulerNow,
getNetworkMapCache,
getPingServicesSchedulerSettings,
patchPingServicesSchedulerSettings,
runPingServicesSchedulerNow,
};
+11
View File
@@ -32,6 +32,7 @@ const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
const trafficRoutes = require('./routes/trafficRoutes');
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
const schedulerRoutes = require('./routes/schedulerRoutes');
const app = express();
@@ -493,6 +494,11 @@ app.patch('/api/scheduler/network-map/settings', writeLimiter, schedulerRoutes.p
app.get('/api/scheduler/network-map/logs', schedulerRoutes.getNetworkMapSchedulerLogs);
app.post('/api/scheduler/network-map/run-now', writeLimiter, schedulerRoutes.runNetworkMapSchedulerNow);
// === SCHEDULER (пинг сервисов: обновление кеша по расписанию, по умолчанию каждые 2 мин) ===
app.get('/api/scheduler/ping-services/settings', schedulerRoutes.getPingServicesSchedulerSettings);
app.patch('/api/scheduler/ping-services/settings', writeLimiter, schedulerRoutes.patchPingServicesSchedulerSettings);
app.post('/api/scheduler/ping-services/run-now', writeLimiter, schedulerRoutes.runPingServicesSchedulerNow);
// === MIKROTIK VALIDATION ===
app.post('/api/mikrotik/validate', async (req, res) => {
const { config } = req.body;
@@ -530,5 +536,10 @@ app.listen(port, () => {
} catch (e) {
logger.error({ component: 'network-map-scheduler', err: e && e.message }, 'Failed to start network map scheduler');
}
try {
initPingServicesScheduler(logger);
} catch (e) {
logger.error({ component: 'ping-services-scheduler', err: e && e.message }, 'Failed to start ping services scheduler');
}
});
+96
View File
@@ -0,0 +1,96 @@
/**
* Планировщик пинга сервисов из списка настроек.
* Периодически вызывает refreshPingServicesCache и обновляет кеш для GET /api/ping-services.
*/
const { readS3TextObject } = require('./s3Service');
const { refreshPingServicesCache } = require('../routes/miscRoutes');
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
const DEFAULT_INTERVAL_MINUTES = 2;
let schedulerTimer = null;
let lastScheduledRunAt = null;
function getSettingsFromUiSettings(uiSettings) {
return {
enabled: uiSettings.pingServicesSchedulerEnabled !== false,
intervalMinutes: Math.max(1, Math.min(1440, parseInt(uiSettings.pingServicesSchedulerIntervalMinutes, 10) || DEFAULT_INTERVAL_MINUTES)),
};
}
/**
* Время следующего запуска по расписанию (timestamp или null если выключено).
*/
function getNextRunAt(settings) {
if (!settings || !settings.enabled) return null;
const intervalMs = (settings.intervalMinutes || DEFAULT_INTERVAL_MINUTES) * 60 * 1000;
const from = lastScheduledRunAt || Date.now();
return from + intervalMs;
}
/**
* Запустить планировщик пинг-сервисов.
* @param {object} logger - pino logger
*/
function initPingServicesScheduler(logger) {
const log = logger || console;
async function tick() {
lastScheduledRunAt = Date.now();
let uiSettings;
try {
const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
uiSettings = JSON.parse(raw?.body || '{}') || {};
} catch (_) {
return;
}
const settings = getSettingsFromUiSettings(uiSettings);
if (!settings.enabled) return;
log.info({ component: 'ping-services-scheduler' }, 'Running ping services refresh');
refreshPingServicesCache().catch((err) => {
log.error({ component: 'ping-services-scheduler', err: err?.message }, 'Ping services refresh failed');
});
}
async function schedule() {
let uiSettings;
try {
const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
uiSettings = JSON.parse(raw?.body || '{}') || {};
} catch (_) {
return;
}
const settings = getSettingsFromUiSettings(uiSettings);
if (!settings.enabled) {
log.info({ component: 'ping-services-scheduler' }, 'Ping services scheduler disabled in settings');
return;
}
const intervalMs = settings.intervalMinutes * 60 * 1000;
log.info(
{ component: 'ping-services-scheduler', intervalMinutes: settings.intervalMinutes },
'Ping services scheduler started (first run in 15 s, then every %d min)',
settings.intervalMinutes
);
schedulerTimer = setInterval(tick, intervalMs);
setTimeout(() => tick(), 15 * 1000);
}
schedule();
}
function stopPingServicesScheduler() {
if (schedulerTimer) {
clearInterval(schedulerTimer);
schedulerTimer = null;
}
}
module.exports = {
initPingServicesScheduler,
stopPingServicesScheduler,
getSettingsFromUiSettings,
getNextRunAt,
};