315 lines
8.9 KiB
JavaScript
315 lines
8.9 KiB
JavaScript
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();
|
|
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,
|
|
maxRedirects: 5,
|
|
});
|
|
const responseTime = Date.now() - startTime;
|
|
const isUp = response.status >= 200 && response.status < 400;
|
|
result = {
|
|
...result,
|
|
status: isUp ? 'up' : 'down',
|
|
response_time: responseTime,
|
|
status_code: response.status,
|
|
error_message: isUp ? null : `HTTP ${response.status}`,
|
|
};
|
|
} catch (error) {
|
|
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, details)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`);
|
|
stmt.run(
|
|
check.resource_id,
|
|
check.status,
|
|
check.response_time,
|
|
check.status_code,
|
|
check.error_message,
|
|
check.details || null
|
|
);
|
|
}
|
|
|
|
getLastCheck(resourceId) {
|
|
return db.prepare(`
|
|
SELECT * FROM checks
|
|
WHERE resource_id = ?
|
|
ORDER BY checked_at DESC
|
|
LIMIT 1
|
|
`).get(resourceId);
|
|
}
|
|
|
|
handleIncident(resourceId, isDown) {
|
|
const activeIncident = db.prepare(`
|
|
SELECT * FROM incidents
|
|
WHERE resource_id = ? AND resolved_at IS NULL
|
|
`).get(resourceId);
|
|
|
|
if (isDown && !activeIncident) {
|
|
// Start new incident
|
|
db.prepare(`
|
|
INSERT INTO incidents (resource_id) VALUES (?)
|
|
`).run(resourceId);
|
|
return { type: 'started', incident: true };
|
|
} else if (!isDown && activeIncident) {
|
|
// Resolve incident
|
|
db.prepare(`
|
|
UPDATE incidents
|
|
SET resolved_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`).run(activeIncident.id);
|
|
return { type: 'resolved', incident: activeIncident };
|
|
}
|
|
|
|
return { type: 'none' };
|
|
}
|
|
|
|
getResourceStats(resourceId, hours = 24) {
|
|
const checks = db.prepare(`
|
|
SELECT * FROM checks
|
|
WHERE resource_id = ?
|
|
AND checked_at >= datetime('now', '-' || ? || ' hours')
|
|
ORDER BY checked_at ASC
|
|
`).all(resourceId, hours);
|
|
|
|
const upChecks = checks.filter(c => c.status === 'up');
|
|
const totalChecks = checks.length;
|
|
const uptime = totalChecks > 0 ? (upChecks.length / totalChecks) * 100 : 0;
|
|
|
|
const responseTimes = checks
|
|
.filter(c => c.response_time !== null)
|
|
.map(c => c.response_time);
|
|
|
|
const avgResponseTime = responseTimes.length > 0
|
|
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
|
: 0;
|
|
|
|
return {
|
|
uptime: uptime.toFixed(2),
|
|
avgResponseTime: avgResponseTime.toFixed(0),
|
|
totalChecks,
|
|
checks: checks.slice(-50), // Last 50 checks
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = new MonitorService();
|