Add multi-protocol checks and details storage

This commit is contained in:
nmemmert committed 2025-12-10 11:21:14 -05:00
1 parent 5b03cfb977
commit 7c2fd6163c
5 files changed
+266 -10

No files matched your search

+16
View File
@@ -129,6 +129,22 @@ function Dashboard() {
/> />
</div> </div>
<div className="form-group">
<label>Check Type</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
>
<option value="http">HTTP/HTTPS</option>
<option value="health">Service Health</option>
<option value="tcp">TCP Port</option>
<option value="tls">TLS/SSL</option>
<option value="dns">DNS Lookup</option>
<option value="websocket">WebSocket</option>
<option value="icmp">ICMP Ping</option>
</select>
</div>
<div className="form-group"> <div className="form-group">
<label>Check Interval (ms)</label> <label>Check Interval (ms)</label>
<input <input
+33 -1
View File
@@ -15,7 +15,9 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"nodemailer": "^6.9.7" "nodemailer": "^6.9.7",
"ping": "^0.4.4",
"ws": "^8.17.1"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
@@ -1461,6 +1463,15 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/ping": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz",
"integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/prebuild-install": { "node_modules/prebuild-install": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -2197,6 +2208,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+3 -1
View File
@@ -21,7 +21,9 @@
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"axios": "^1.6.2", "axios": "^1.6.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"ping": "^0.4.4",
"ws": "^8.17.1"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.2", "nodemon": "^3.0.2",
+8
View File
@@ -41,4 +41,12 @@ db.exec(`
CREATE INDEX IF NOT EXISTS idx_incidents_resource_id ON incidents(resource_id); 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; module.exports = db;
+206 -8
View File
@@ -1,26 +1,77 @@
const axios = require('axios'); 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 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 { class MonitorService {
async checkResource(resource) { async checkResource(resource) {
const type = (resource.type || 'http').toLowerCase();
const startTime = Date.now(); const startTime = Date.now();
let result = { const base = {
resource_id: resource.id, resource_id: resource.id,
status: 'down', status: 'down',
response_time: null, response_time: null,
status_code: null, status_code: null,
error_message: 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 { try {
const response = await axios.get(resource.url, { const response = await axios.get(resource.url, {
timeout: resource.timeout, timeout: resource.timeout,
validateStatus: () => true, // Don't throw on any status validateStatus: () => true,
maxRedirects: 5,
}); });
const responseTime = Date.now() - startTime; const responseTime = Date.now() - startTime;
const isUp = response.status >= 200 && response.status < 400; const isUp = response.status >= 200 && response.status < 400;
result = { result = {
...result, ...result,
status: isUp ? 'up' : 'down', status: isUp ? 'up' : 'down',
@@ -32,21 +83,168 @@ class MonitorService {
result.error_message = error.message; result.error_message = error.message;
result.response_time = Date.now() - startTime; result.response_time = Date.now() - startTime;
} }
return result; 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) { saveCheck(check) {
const stmt = db.prepare(` const stmt = db.prepare(`
INSERT INTO checks (resource_id, status, response_time, status_code, error_message) INSERT INTO checks (resource_id, status, response_time, status_code, error_message, details)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
`); `);
stmt.run( stmt.run(
check.resource_id, check.resource_id,
check.status, check.status,
check.response_time, check.response_time,
check.status_code, check.status_code,
check.error_message check.error_message,
check.details || null
); );
} }