UNPKG

netty-js

Version:

Netty - A website monitoring and tracking service for uptime, response time, and SSL checks.

182 lines (154 loc) 5.65 kB
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;