52 lines
3.6 KiB
JavaScript
52 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
const fs = require('fs'), path = require('path'), http = require('http');
|
|
const { secret } = require('../runtime/config');
|
|
const { Monitor } = require('./monitor');
|
|
const root = path.resolve(process.env.SENTINEL_DATA || path.join(process.env.DATA || 'data', 'sentinel'));
|
|
fs.mkdirSync(root, { recursive: true });
|
|
const token = secret('SCRAPER_TOKEN');
|
|
const host = process.env.SENTINEL_HOST || '127.0.0.1', port = +(process.env.SENTINEL_PORT || 8092);
|
|
if (!['127.0.0.1', 'localhost', '::1'].includes(host) && !token) throw new Error('SCRAPER_TOKEN is required for non-loopback Sentinel');
|
|
const reader = process.env.READER_URL || 'http://127.0.0.1:8080';
|
|
const scraper = process.env.SCRAPER_URL || 'http://127.0.0.1:8091';
|
|
const file = path.join(root, 'state.json');
|
|
let saved = {};
|
|
if (fs.existsSync(file)) saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
const monitor = new Monitor({ incidents: saved.incidents || [] });
|
|
const request = async (base, endpoint, body) => {
|
|
const response = await fetch(base + endpoint, { headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), 'content-type': 'application/json' },
|
|
signal: AbortSignal.timeout(8000), ...(body ? { method: 'POST', body: JSON.stringify(body) } : {}) });
|
|
if (!response.ok) throw new Error(`Service HTTP ${response.status}`);
|
|
return response.json();
|
|
};
|
|
let stopped = false, timer, lastTick = Date.now();
|
|
async function tick() {
|
|
try {
|
|
const results = await Promise.allSettled([request(reader, '/health'), request(scraper, '/api/state')]);
|
|
const state = await monitor.observe(results[0].status === 'fulfilled' ? results[0].value : null,
|
|
results[1].status === 'fulfilled' ? results[1].value : null, input => request(scraper, '/api/recovery', input));
|
|
fs.writeFileSync(file + '.tmp', JSON.stringify(state, null, 2) + '\n'); fs.renameSync(file + '.tmp', file);
|
|
lastTick = Date.now();
|
|
for (const incident of state.incidents.filter(i => i.state === 'open' && !i.logged)) {
|
|
console.log(JSON.stringify({ ...incident, targetService: incident.service, service: 'sentinel', level: 'warning' })); incident.logged = true;
|
|
}
|
|
} catch (error) { console.error('[sentinel]', error.message); }
|
|
if (!stopped) timer = setTimeout(tick, 30000);
|
|
}
|
|
const server = http.createServer((req, res) => {
|
|
const endpoint = new URL(req.url, 'http://localhost').pathname;
|
|
let code = 200, value;
|
|
if (endpoint === '/health') { code = Date.now() - lastTick < 120000 ? 200 : 503; value = { ok: code === 200, service: 'sentinel', checkedAt: monitor.state.checkedAt }; }
|
|
else if (token && req.headers.authorization !== `Bearer ${token}`) { code = 401; value = { error: 'Unauthorized' }; }
|
|
else if (endpoint === '/api/state' && req.method === 'GET') value = monitor.state;
|
|
else if (endpoint === '/metrics' && req.method === 'GET') {
|
|
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4' });
|
|
return res.end(`mangacircus_reader_up ${Number(!!monitor.state.services.reader)}\nmangacircus_scraper_up ${Number(!!monitor.state.services.scraper)}\nmangacircus_open_incidents ${monitor.incidents.filter(i => i.state === 'open').length}\n`);
|
|
} else { code = 404; value = { error: 'Not found' }; }
|
|
res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value));
|
|
});
|
|
server.listen(port, host, () => console.log(JSON.stringify({ service: 'sentinel', event: 'started', port })));
|
|
tick();
|
|
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => { stopped = true; clearTimeout(timer); server.close(() => process.exit(0)); });
|