netty-js
Version:
Netty - A website monitoring and tracking service for uptime, response time, and SSL checks.
182 lines (154 loc) • 5.65 kB
JavaScript
const { EventEmitter } = require('events');
const https = require('https');
const http = require('http');
const tls = require('tls');
const dns = require('dns').promises;
class Monitor extends EventEmitter {
/**
* @param {Object} options
* @param {string|string[]} options.url - URL or array of URLs to monitor
* @param {number} options.interval - check interval in ms (default: 60000)
* @param {number} options.timeout - request timeout in ms (default: 10000)
* @param {number} options.retries - number of retries for failed requests (default: 1)
* @param {number} options.retryDelay - delay between retries in ms (default: 1000)
* @param {number} options.slowThreshold - ms above which a response is considered slow (default: 2000)
*/
constructor(options = {}) {
super();
if (!options.url) throw new Error('url is required');
this.urls = Array.isArray(options.url) ? options.url : [options.url];
this.interval = Math.max(options.interval || 60000, 1000);
this.timeout = options.timeout || 10000;
this.retries = options.retries || 1;
this.retryDelay = options.retryDelay || 1000;
this.slowThreshold = options.slowThreshold || 2000;
this.timers = new Map();
this.history = new Map(); // store history per URL
this.lastResults = new Map();
}
async _getSSLCert(host, port = 443) {
return new Promise((resolve, reject) => {
const socket = tls.connect({ host, port, servername: host, rejectUnauthorized: false }, () => {
const cert = socket.getPeerCertificate(true);
socket.end();
if (!cert || Object.keys(cert).length === 0) return reject(new Error('No certificate found'));
resolve({
subject: cert.subject,
issuer: cert.issuer,
validFrom: cert.valid_from,
validTo: cert.valid_to,
});
});
socket.setTimeout(this.timeout, () => {
socket.destroy();
reject(new Error('SSL check timeout'));
});
socket.on('error', (err) => reject(err));
});
}
async _performRequest(urlString) {
const parsed = new URL(urlString);
const lib = parsed.protocol === 'https:' ? https : http;
const start = Date.now();
return new Promise((resolve) => {
const req = lib.get(urlString, { timeout: this.timeout }, (res) => {
const duration = Date.now() - start;
res.resume(); // consume response to free memory
resolve({ statusCode: res.statusCode, responseTime: duration });
});
req.on('error', (err) => resolve({ error: err }));
req.on('timeout', () => req.destroy(new Error('Request timeout')));
});
}
async _checkSingle(url) {
const hostname = new URL(url).hostname;
// DNS lookup
let dnsInfo;
try {
dnsInfo = await dns.lookup(hostname);
} catch (err) {
const result = { url, up: false, error: 'DNS lookup failed', dnsError: err.message, checkedAt: new Date().toISOString() };
this._emitResult(url, result);
return result;
}
// HTTP/HTTPS request with retries
let attempt = 0;
let result;
while (attempt <= this.retries) {
result = await this._performRequest(url);
if (!result.error) break;
attempt++;
if (attempt <= this.retries) await new Promise(r => setTimeout(r, this.retryDelay));
}
if (result.error) {
const res = { url, up: false, dns: dnsInfo, error: result.error.message, checkedAt: new Date().toISOString() };
this._emitResult(url, res);
return res;
}
// SSL check for HTTPS
let sslInfo = null;
let sslExpired = false;
let sslWarning = false;
if (url.startsWith('https')) {
try {
sslInfo = await this._getSSLCert(hostname);
const now = new Date();
const validTo = new Date(sslInfo.validTo);
sslExpired = validTo < now;
const daysLeft = (validTo - now) / (1000 * 60 * 60 * 24);
sslWarning = daysLeft > 0 && daysLeft < 7;
} catch (err) {
result.sslError = err.message;
}
}
const finalResult = {
url,
up: result.statusCode >= 200 && result.statusCode < 400 && !sslExpired,
statusCode: result.statusCode,
responseTime: result.responseTime,
dns: dnsInfo,
ssl: sslInfo,
sslExpired,
sslWarning,
checkedAt: new Date().toISOString(),
};
if (finalResult.responseTime > this.slowThreshold) this.emit('slow', finalResult);
this._emitResult(url, finalResult);
return finalResult;
}
_emitResult(url, result) {
this.lastResults.set(url, result);
// Keep history
if (!this.history.has(url)) this.history.set(url, []);
const hist = this.history.get(url);
hist.push(result);
if (hist.length > 100) hist.shift();
this.emit('check', result);
if (result.up) this.emit('up', result);
else this.emit('down', result);
if (result.error || result.sslError || result.sslExpired || result.sslWarning) this.emit('error', result);
}
async checkAll() {
const results = await Promise.all(this.urls.map((u) => this._checkSingle(u)));
return results;
}
start() {
this.urls.forEach((url) => {
if (this.timers.has(url)) return;
this._checkSingle(url);
const timer = setInterval(() => this._checkSingle(url), this.interval);
this.timers.set(url, timer);
});
}
stop() {
this.timers.forEach((timer) => clearInterval(timer));
this.timers.clear();
}
getLast(url) {
return this.lastResults.get(url);
}
getHistory(url) {
return this.history.get(url) || [];
}
}
module.exports = Monitor;