Add multi-protocol checks and details storage
This commit is contained in:
1 parent
5b03cfb977
commit
7c2fd6163c
5 files changed
+266
-10
No files matched your search
@@ -41,4 +41,12 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resource_id ON incidents(resource_id);
|
||||
`);
|
||||
|
||||
// Lightweight migration: add details column for per-check metadata if missing
|
||||
try {
|
||||
db.prepare("ALTER TABLE checks ADD COLUMN details TEXT").run();
|
||||
// Column added
|
||||
} catch (err) {
|
||||
// Ignore if the column already exists
|
||||
}
|
||||
|
||||
module.exports = db;
|
||||
+206
-8
@@ -1,26 +1,77 @@
|
||||
const axios = require('axios');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const dns = require('dns').promises;
|
||||
const WebSocket = require('ws');
|
||||
const ping = require('ping');
|
||||
const db = require('./database');
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
http: 80,
|
||||
https: 443,
|
||||
tcp: 80,
|
||||
tls: 443,
|
||||
websocket: 80,
|
||||
};
|
||||
|
||||
function parseHostPort(urlString, fallbackPort) {
|
||||
try {
|
||||
const u = new URL(urlString.startsWith('http') || urlString.startsWith('ws') ? urlString : `http://${urlString}`);
|
||||
return { host: u.hostname, port: u.port ? Number(u.port) : fallbackPort };
|
||||
} catch (err) {
|
||||
// Fallback if url is just host:port
|
||||
const [host, port] = urlString.split(':');
|
||||
return { host, port: port ? Number(port) : fallbackPort };
|
||||
}
|
||||
}
|
||||
|
||||
class MonitorService {
|
||||
async checkResource(resource) {
|
||||
const type = (resource.type || 'http').toLowerCase();
|
||||
const startTime = Date.now();
|
||||
let result = {
|
||||
const base = {
|
||||
resource_id: resource.id,
|
||||
status: 'down',
|
||||
response_time: null,
|
||||
status_code: null,
|
||||
error_message: null,
|
||||
details: null,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
case 'health':
|
||||
return this.checkHttp(resource, startTime, base);
|
||||
case 'tcp':
|
||||
return this.checkTcp(resource, startTime, base);
|
||||
case 'tls':
|
||||
return this.checkTls(resource, startTime, base);
|
||||
case 'dns':
|
||||
return this.checkDns(resource, startTime, base);
|
||||
case 'websocket':
|
||||
case 'ws':
|
||||
case 'wss':
|
||||
return this.checkWebSocket(resource, startTime, base);
|
||||
case 'icmp':
|
||||
case 'ping':
|
||||
return this.checkIcmp(resource, startTime, base);
|
||||
default:
|
||||
// Fallback to HTTP if unknown
|
||||
return this.checkHttp(resource, startTime, base);
|
||||
}
|
||||
}
|
||||
|
||||
async checkHttp(resource, startTime, base) {
|
||||
let result = { ...base };
|
||||
try {
|
||||
const response = await axios.get(resource.url, {
|
||||
timeout: resource.timeout,
|
||||
validateStatus: () => true, // Don't throw on any status
|
||||
validateStatus: () => true,
|
||||
maxRedirects: 5,
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const isUp = response.status >= 200 && response.status < 400;
|
||||
|
||||
result = {
|
||||
...result,
|
||||
status: isUp ? 'up' : 'down',
|
||||
@@ -32,21 +83,168 @@ class MonitorService {
|
||||
result.error_message = error.message;
|
||||
result.response_time = Date.now() - startTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async checkTcp(resource, startTime, base) {
|
||||
const { host, port } = parseHostPort(resource.url, DEFAULT_PORTS.tcp);
|
||||
return new Promise(resolve => {
|
||||
const socket = new net.Socket();
|
||||
let finished = false;
|
||||
|
||||
const done = (status, error_message = null) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
socket.destroy();
|
||||
resolve({
|
||||
...base,
|
||||
status,
|
||||
response_time: Date.now() - startTime,
|
||||
status_code: null,
|
||||
error_message,
|
||||
details: JSON.stringify({ host, port }),
|
||||
});
|
||||
};
|
||||
|
||||
socket.setTimeout(resource.timeout || 5000);
|
||||
socket.once('connect', () => done('up'));
|
||||
socket.once('timeout', () => done('down', 'TCP timeout'));
|
||||
socket.once('error', (err) => done('down', err.message));
|
||||
socket.connect(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
async checkTls(resource, startTime, base) {
|
||||
const { host, port } = parseHostPort(resource.url, DEFAULT_PORTS.tls);
|
||||
return new Promise(resolve => {
|
||||
const socket = tls.connect({ host, port, servername: host, timeout: resource.timeout || 5000 }, () => {
|
||||
const cert = socket.getPeerCertificate();
|
||||
const now = Date.now();
|
||||
const expires = new Date(cert.valid_to).getTime();
|
||||
const daysRemaining = Math.round((expires - now) / (1000 * 60 * 60 * 24));
|
||||
const validHost = cert.subjectaltname ? cert.subjectaltname.includes(host) : true;
|
||||
|
||||
const status = daysRemaining > 0 && validHost ? 'up' : 'down';
|
||||
resolve({
|
||||
...base,
|
||||
status,
|
||||
response_time: Date.now() - startTime,
|
||||
error_message: status === 'up' ? null : 'TLS invalid or expired',
|
||||
details: JSON.stringify({
|
||||
issuer: cert.issuer?.CN,
|
||||
subject: cert.subject?.CN,
|
||||
valid_to: cert.valid_to,
|
||||
days_remaining: daysRemaining,
|
||||
host_matches: validHost,
|
||||
}),
|
||||
});
|
||||
socket.end();
|
||||
});
|
||||
|
||||
socket.on('error', err => {
|
||||
resolve({
|
||||
...base,
|
||||
status: 'down',
|
||||
response_time: Date.now() - startTime,
|
||||
error_message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
socket.setTimeout(resource.timeout || 5000, () => {
|
||||
resolve({
|
||||
...base,
|
||||
status: 'down',
|
||||
response_time: Date.now() - startTime,
|
||||
error_message: 'TLS timeout',
|
||||
});
|
||||
socket.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async checkDns(resource, startTime, base) {
|
||||
const { host } = parseHostPort(resource.url, DEFAULT_PORTS.http);
|
||||
try {
|
||||
const records = await dns.lookup(host, { all: true });
|
||||
return {
|
||||
...base,
|
||||
status: 'up',
|
||||
response_time: Date.now() - startTime,
|
||||
details: JSON.stringify({ answers: records }),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
...base,
|
||||
status: 'down',
|
||||
response_time: Date.now() - startTime,
|
||||
error_message: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkWebSocket(resource, startTime, base) {
|
||||
return new Promise(resolve => {
|
||||
let finished = false;
|
||||
|
||||
const done = (status, error_message = null) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
resolve({
|
||||
...base,
|
||||
status,
|
||||
response_time: Date.now() - startTime,
|
||||
error_message,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(resource.url, { handshakeTimeout: resource.timeout || 5000 });
|
||||
ws.on('open', () => {
|
||||
done('up');
|
||||
ws.close();
|
||||
});
|
||||
ws.on('error', (err) => done('down', err.message));
|
||||
ws.on('close', () => done('up'));
|
||||
} catch (err) {
|
||||
done('down', err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async checkIcmp(resource, startTime, base) {
|
||||
const { host } = parseHostPort(resource.url, DEFAULT_PORTS.http);
|
||||
try {
|
||||
const res = await ping.promise.probe(host, { timeout: (resource.timeout || 5000) / 1000 });
|
||||
const status = res.alive ? 'up' : 'down';
|
||||
return {
|
||||
...base,
|
||||
status,
|
||||
response_time: res.time || Date.now() - startTime,
|
||||
error_message: res.alive ? null : res.output,
|
||||
details: JSON.stringify({ packetLoss: res.packetLoss, time: res.time }),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
...base,
|
||||
status: 'down',
|
||||
response_time: Date.now() - startTime,
|
||||
error_message: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
saveCheck(check) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO checks (resource_id, status, response_time, status_code, error_message)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO checks (resource_id, status, response_time, status_code, error_message, details)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(
|
||||
check.resource_id,
|
||||
check.status,
|
||||
check.response_time,
|
||||
check.status_code,
|
||||
check.error_message
|
||||
check.error_message,
|
||||
check.details || null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user