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
@@ -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
|
||||||
|
|||||||
Generated
+33
-1
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user