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_';
|
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 — список сервисов для пинга из настроек (для дашборда и редактора)
|
// GET /api/ping-services-list — список сервисов для пинга из настроек (для дашборда и редактора)
|
||||||
async function getPingServicesList(req, res) {
|
async function getPingServicesList(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -420,56 +502,8 @@ async function getPingServices(req, res) {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viaRouter) {
|
const byId = await refreshPingServicesCache();
|
||||||
const { runPingViaRouter } = require('./mikrotikConfigRoutes');
|
return res.json(byId);
|
||||||
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);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ping-services error', e);
|
console.error('ping-services error', e);
|
||||||
const fallback = {};
|
const fallback = {};
|
||||||
@@ -695,5 +729,6 @@ module.exports = {
|
|||||||
postUiSettings,
|
postUiSettings,
|
||||||
getPingServicesList,
|
getPingServicesList,
|
||||||
getPingServices,
|
getPingServices,
|
||||||
|
refreshPingServicesCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
const { sendError, sendOk } = require('../middleware/errorHandler');
|
const { sendError, sendOk } = require('../middleware/errorHandler');
|
||||||
const { readS3TextObject, writeS3JsonObject } = require('../services/s3Service');
|
const { readS3TextObject, writeS3JsonObject } = require('../services/s3Service');
|
||||||
const networkMapScheduler = require('../services/networkMapScheduler');
|
const networkMapScheduler = require('../services/networkMapScheduler');
|
||||||
|
const pingServicesScheduler = require('../services/pingServicesScheduler');
|
||||||
|
|
||||||
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
||||||
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
||||||
@@ -17,6 +18,11 @@ const SCHEDULER_KEYS = [
|
|||||||
'networkMapSchedulerLogMaxLines',
|
'networkMapSchedulerLogMaxLines',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PING_SERVICES_SCHEDULER_KEYS = [
|
||||||
|
'pingServicesSchedulerEnabled',
|
||||||
|
'pingServicesSchedulerIntervalMinutes',
|
||||||
|
];
|
||||||
|
|
||||||
async function readUiSettings() {
|
async function readUiSettings() {
|
||||||
try {
|
try {
|
||||||
const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
|
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 = {
|
module.exports = {
|
||||||
getNetworkMapSchedulerSettings,
|
getNetworkMapSchedulerSettings,
|
||||||
patchNetworkMapSchedulerSettings,
|
patchNetworkMapSchedulerSettings,
|
||||||
getNetworkMapSchedulerLogs,
|
getNetworkMapSchedulerLogs,
|
||||||
runNetworkMapSchedulerNow,
|
runNetworkMapSchedulerNow,
|
||||||
getNetworkMapCache,
|
getNetworkMapCache,
|
||||||
|
getPingServicesSchedulerSettings,
|
||||||
|
patchPingServicesSchedulerSettings,
|
||||||
|
runPingServicesSchedulerNow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
|
|||||||
const trafficRoutes = require('./routes/trafficRoutes');
|
const trafficRoutes = require('./routes/trafficRoutes');
|
||||||
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
||||||
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
|
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
|
||||||
|
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
|
||||||
const schedulerRoutes = require('./routes/schedulerRoutes');
|
const schedulerRoutes = require('./routes/schedulerRoutes');
|
||||||
|
|
||||||
const app = express();
|
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.get('/api/scheduler/network-map/logs', schedulerRoutes.getNetworkMapSchedulerLogs);
|
||||||
app.post('/api/scheduler/network-map/run-now', writeLimiter, schedulerRoutes.runNetworkMapSchedulerNow);
|
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 ===
|
// === MIKROTIK VALIDATION ===
|
||||||
app.post('/api/mikrotik/validate', async (req, res) => {
|
app.post('/api/mikrotik/validate', async (req, res) => {
|
||||||
const { config } = req.body;
|
const { config } = req.body;
|
||||||
@@ -530,5 +536,10 @@ app.listen(port, () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error({ component: 'network-map-scheduler', err: e && e.message }, 'Failed to start network map scheduler');
|
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