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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m29s
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user