"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LogMonitor = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const zlib = __importStar(require("zlib")); const http = __importStar(require("http")); const https = __importStar(require("https")); // ---- Monitor Engine ---- class LogMonitor { config; scanTimer = null; results = []; constructor(config = {}) { this.config = { enabled: config.enabled !== false, scanIntervalMs: config.scanIntervalMs || 60000, rules: config.rules || [], storageDir: config.storageDir || path.join(process.cwd(), 'monitor-data'), }; if (!fs.existsSync(this.config.storageDir)) { fs.mkdirSync(this.config.storageDir, { recursive: true }); } this.loadState(); if (this.config.enabled && this.config.rules.length > 0) { this.startScanning(); } } loadState() { const stateFile = path.join(this.config.storageDir, '_monitor_state.json'); if (fs.existsSync(stateFile)) { try { const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); this.results = data.results || []; // Restore lastTriggered from state if (data.rules) { for (const saved of data.rules) { const rule = this.config.rules.find(r => r.id === saved.id); if (rule) { rule.lastTriggered = saved.lastTriggered; rule.matchCount = saved.matchCount || 0; } } } } catch { } } } saveState() { const stateFile = path.join(this.config.storageDir, '_monitor_state.json'); fs.writeFileSync(stateFile, JSON.stringify({ results: this.results.slice(-100), rules: this.config.rules.map(r => ({ id: r.id, lastTriggered: r.lastTriggered, matchCount: r.matchCount })), }, null, 2)); } startScanning() { if (this.scanTimer) clearInterval(this.scanTimer); console.log(` Monitor scanning every ${this.config.scanIntervalMs / 1000}s with ${this.config.rules.length} rule(s)`); this.scanTimer = setInterval(() => this.runScan(), this.config.scanIntervalMs); // Run immediately setTimeout(() => this.runScan(), 2000); } stopScanning() { if (this.scanTimer) { clearInterval(this.scanTimer); this.scanTimer = null; } } async runScan() { for (const rule of this.config.rules) { if (!rule.enabled) continue; // Check schedule if (rule.schedule && rule.schedule.mode !== 'interval') { if (!this.shouldRunBySchedule(rule)) continue; } // Check cooldown if (rule.lastTriggered) { const elapsed = Date.now() - new Date(rule.lastTriggered).getTime(); if (elapsed < rule.cooldownMs) continue; } try { const matches = this.scanRule(rule); if (matches.length > 0) { const result = { ruleId: rule.id, ruleName: rule.name, matchedLines: matches, totalMatches: matches.length, sources: rule.sources, timestamp: new Date().toISOString(), }; rule.lastTriggered = result.timestamp; rule.matchCount += matches.length; this.results.push(result); // Send notifications await this.notify(rule, result); this.saveState(); } } catch (err) { console.error(` Monitor rule "${rule.name}" error:`, err.message); } } } shouldRunBySchedule(rule) { const schedule = rule.schedule; if (!schedule) return true; const now = new Date(); const lastRun = rule.lastTriggered ? new Date(rule.lastTriggered) : null; switch (schedule.mode) { case 'daily': { // Run once per day at specified time (HH:MM) const [h, m] = (schedule.value || '07:00').split(':').map(Number); const targetToday = new Date(now); targetToday.setHours(h, m, 0, 0); // Already ran today? if (lastRun && lastRun.toDateString() === now.toDateString()) return false; // Is it past the target time? return now >= targetToday; } case 'hourly': { // Run every N hours const hours = parseInt(schedule.value || '2', 10); if (!lastRun) return true; return (now.getTime() - lastRun.getTime()) >= hours * 3600000; } case 'cron': { // Simple cron: just check if enough time passed based on scan interval // Full cron parsing would need a library, so we approximate return true; } default: return true; } } scanRule(rule) { const allMatches = []; const regexes = rule.patterns.map(p => new RegExp(p, 'i')); for (const source of rule.sources) { const files = this.resolveFiles(source); for (const file of files) { if (!this.isInTimeRange(file, rule.timeRange)) continue; try { const content = this.readFile(file); const lines = content.split('\n'); for (const line of lines) { if (line.trim() === '') continue; const matched = rule.patternLogic === 'all' ? regexes.every(r => r.test(line)) : regexes.some(r => r.test(line)); if (matched) allMatches.push(line); } } catch { } } } return allMatches; } resolveFiles(pattern) { if (pattern.includes('*')) { const dir = path.dirname(pattern); const filePattern = path.basename(pattern); try { const files = fs.readdirSync(dir); const regex = new RegExp('^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); return files.filter(f => regex.test(f)).map(f => path.join(dir, f)); } catch { return []; } } return fs.existsSync(pattern) ? [pattern] : []; } isInTimeRange(file, range) { if (range.type === 'all') return true; try { const stat = fs.statSync(file); const fileTime = stat.mtime.getTime(); const now = Date.now(); switch (range.type) { case 'today': { const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); return fileTime >= startOfDay.getTime(); } case 'week': return fileTime >= now - 7 * 24 * 60 * 60 * 1000; case 'month': return fileTime >= now - 30 * 24 * 60 * 60 * 1000; case 'hours': return fileTime >= now - (range.hours || 24) * 60 * 60 * 1000; default: return true; } } catch { return false; } } readFile(filePath) { const ext = path.extname(filePath).toLowerCase(); if (ext === '.gz' || ext === '.gzip') { return zlib.gunzipSync(fs.readFileSync(filePath)).toString('utf-8'); } return fs.readFileSync(filePath, 'utf-8'); } // ---- Notifications ---- async notify(rule, result) { const summary = this.buildSummary(rule, result); const attachment = result.matchedLines.length > rule.maxLinesInMessage ? this.buildAttachment(result) : null; for (const channel of rule.notifications) { try { switch (channel.type) { case 'webhook': await this.sendWebhook(channel, summary, attachment); break; case 'telegram': await this.sendTelegram(channel, summary, attachment); break; case 'teams': await this.sendTeams(channel, summary); break; case 'email': console.log(` [Monitor] Email notification not yet implemented`); break; } } catch (err) { console.error(` [Monitor] Notification error (${channel.type}):`, err.message); } } } buildSummary(rule, result) { const lines = result.matchedLines.slice(0, rule.maxLinesInMessage); let msg = `๐Ÿšจ **Log Monitor Alert: ${rule.name}**\n\n`; msg += `**Matches:** ${result.totalMatches} lines\n`; msg += `**Sources:** ${result.sources.join(', ')}\n`; msg += `**Time:** ${result.timestamp}\n`; msg += `**Patterns:** ${rule.patterns.join(' | ')}\n\n`; msg += `**Sample (first ${lines.length} of ${result.totalMatches}):**\n`; msg += '```\n' + lines.join('\n') + '\n```'; if (result.totalMatches > rule.maxLinesInMessage) { msg += `\n\n๐Ÿ“Ž Full results (${result.totalMatches} lines) attached as .gz file.`; } return msg; } buildAttachment(result) { const content = result.matchedLines.join('\n'); return zlib.gzipSync(Buffer.from(content, 'utf-8')); } async sendWebhook(channel, message, attachment) { if (!channel.url) return; const payload = JSON.stringify({ text: message, matches: attachment ? `(${attachment.length} bytes gzipped attachment)` : undefined, }); await this.httpPost(channel.url, payload, 'application/json'); console.log(` [Monitor] Webhook sent to ${channel.url}`); } async sendTelegram(channel, message, attachment) { if (!channel.botToken || !channel.chatId) return; // Send text message - use plain text to avoid Markdown parsing issues const payload = JSON.stringify({ chat_id: channel.chatId, text: message.replace(/[*_`\[]/g, '').substring(0, 4000), // Strip markdown, respect limit }); await this.httpPost(`https://api.telegram.org/bot${channel.botToken}/sendMessage`, payload, 'application/json'); console.log(` [Monitor] Telegram sent to chat ${channel.chatId}`); } async sendTeams(channel, message) { if (!channel.url) return; const payload = JSON.stringify({ '@type': 'MessageCard', summary: 'Log Monitor Alert', text: message.replace(/\n/g, '
').replace(/```/g, ''), }); await this.httpPost(channel.url, payload, 'application/json'); console.log(` [Monitor] Teams webhook sent`); } httpPost(targetUrl, body, contentType) { return new Promise((resolve, reject) => { const parsed = new URL(targetUrl); const isHttps = parsed.protocol === 'https:'; const transport = isHttps ? https : http; const req = transport.request({ hostname: parsed.hostname, port: parsed.port || (isHttps ? 443 : 80), path: parsed.pathname + parsed.search, method: 'POST', headers: { 'Content-Type': contentType, 'Content-Length': Buffer.byteLength(body) }, }, (res) => { let responseBody = ''; res.on('data', (c) => { responseBody += c; }); res.on('end', () => { if (res.statusCode && res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode}: ${responseBody.substring(0, 200)}`)); } else { resolve(); } }); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); req.write(body); req.end(); }); } // ---- Public API ---- getRules() { return this.config.rules; } getResults() { return this.results; } getConfig() { return { ...this.config }; } addRule(rule) { this.config.rules.push(rule); this.saveState(); if (this.config.rules.length === 1 && !this.scanTimer) this.startScanning(); } updateRule(id, updates) { const rule = this.config.rules.find(r => r.id === id); if (!rule) return false; Object.assign(rule, updates); this.saveState(); return true; } deleteRule(id) { const idx = this.config.rules.findIndex(r => r.id === id); if (idx === -1) return false; this.config.rules.splice(idx, 1); this.saveState(); return true; } triggerRule(id) { const rule = this.config.rules.find(r => r.id === id); if (!rule) return null; const matches = this.scanRule(rule); if (matches.length === 0) return null; const result = { ruleId: rule.id, ruleName: rule.name, matchedLines: matches, totalMatches: matches.length, sources: rule.sources, timestamp: new Date().toISOString(), }; this.results.push(result); rule.matchCount += matches.length; this.saveState(); return result; } getAttachment(ruleId) { const result = [...this.results].reverse().find(r => r.ruleId === ruleId); if (!result) return null; return zlib.gzipSync(Buffer.from(result.matchedLines.join('\n'), 'utf-8')); } handleRequest(req, res, pathname) { const method = req.method || 'GET'; if (pathname === '/api/monitor/rules' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(this.getRules())); return true; } if (pathname === '/api/monitor/rules' && method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', () => { try { const rule = JSON.parse(body); if (!rule.id) rule.id = 'rule-' + Date.now(); if (!rule.maxLinesInMessage) rule.maxLinesInMessage = 20; if (!rule.cooldownMs) rule.cooldownMs = 300000; if (!rule.matchCount) rule.matchCount = 0; if (!rule.lastTriggered) rule.lastTriggered = null; this.addRule(rule); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, rule })); } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); } }); return true; } // Test notification endpoint if (pathname === '/api/monitor/test-notification' && method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', async () => { try { const channel = JSON.parse(body); const testMsg = `๐Ÿงช **Test Notification**\n\nThis is a test from LogViewer Monitor.\nTimestamp: ${new Date().toISOString()}\n\nIf you see this, notifications are working! โœ…`; await this.sendTestNotification(channel, testMsg); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: e.message })); } }); return true; } const ruleMatch = pathname.match(/^\/api\/monitor\/rule\/([a-zA-Z0-9_\-]+)$/); if (ruleMatch && method === 'DELETE') { const ok = this.deleteRule(ruleMatch[1]); res.writeHead(ok ? 200 : 404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok })); return true; } if (ruleMatch && method === 'PUT') { let body = ''; req.on('data', c => body += c); req.on('end', () => { const updates = JSON.parse(body); const ok = this.updateRule(ruleMatch[1], updates); res.writeHead(ok ? 200 : 404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok })); }); return true; } const triggerMatch = pathname.match(/^\/api\/monitor\/trigger\/([a-zA-Z0-9_\-]+)$/); if (triggerMatch && method === 'POST') { const result = this.triggerRule(triggerMatch[1]); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result || { matches: 0 })); return true; } if (pathname === '/api/monitor/results' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(this.results.slice(-50))); return true; } if (pathname === '/api/monitor/config' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(this.getConfig())); return true; } return false; } async sendTestNotification(channel, message) { switch (channel.type) { case 'webhook': if (!channel.url) throw new Error('No webhook URL provided'); await this.sendWebhook(channel, message, null); break; case 'telegram': if (!channel.botToken || !channel.chatId) throw new Error('Bot token and chat ID required'); await this.sendTelegram(channel, message, null); break; case 'teams': if (!channel.url) throw new Error('No Teams webhook URL provided'); await this.sendTeams(channel, message); break; case 'email': throw new Error('Email test not yet implemented'); default: throw new Error(`Unknown channel type: ${channel.type}`); } } } exports.LogMonitor = LogMonitor; //# sourceMappingURL=monitor.js.map