diff --git a/integration/src/services/zeromqServer.ts b/integration/src/services/zeromqServer.ts index 6cda4f9..387ec4e 100644 --- a/integration/src/services/zeromqServer.ts +++ b/integration/src/services/zeromqServer.ts @@ -263,6 +263,16 @@ Analyze only the metrics that are actually present in the data. If values are hi ); break; + // log monitoring + case MetricReplyType.logMetricReply: + const logMetricMessage = message.data.message; + console.log(logMetricMessage); + await this.sendTelexResponse( + channelId.toString(), + logMetricMessage + ); + break; + default: console.info(`Unhandled message type: ${message.type}`); } diff --git a/integration/src/types/metricType.ts b/integration/src/types/metricType.ts index 76e5849..b2b6bb9 100644 --- a/integration/src/types/metricType.ts +++ b/integration/src/types/metricType.ts @@ -12,6 +12,7 @@ export enum MetricReplyType { getNetworkMetrics = "getNetworkMetricsReply", getSecurityMetrics = "getSecurityMetricsReply", getServicesReply = "getServicesReply", + logMetricReply = "LogMetricReply", } export interface MetricsData { @@ -178,6 +179,17 @@ export interface MetricsData { }>; lastUpdated: string; }; + logMetrics?: { + timestamp: number; + systemErrors: { + timestamp: string; + message: string; + }[]; + customLogEntries: { + timestamp: string; + message: string; + }[]; + }; } export enum MetricType { diff --git a/integration/src/utils/telexConfig.ts b/integration/src/utils/telexConfig.ts index d7c2d2a..78db1bb 100644 --- a/integration/src/utils/telexConfig.ts +++ b/integration/src/utils/telexConfig.ts @@ -62,6 +62,13 @@ export const telexGeneratedConfig = { description: "The threshold for memory usage. If the memory usage exceeds this threshold, the server will send an alert to the user.", }, + { + label: "custom_log_path", + type: "text", + default: "", + description: + "The path to the custom log file to monitor. If this is not set, the server will monitor the default log file.", + }, ], endpoints: [], is_active: true, diff --git a/package/src/metrics/collector.ts b/package/src/metrics/collector.ts index 30a7a00..5ee0ce2 100644 --- a/package/src/metrics/collector.ts +++ b/package/src/metrics/collector.ts @@ -171,6 +171,17 @@ export interface IMetricsData { }>; lastUpdated: string; }; + logMetrics?: { + timestamp: number; + systemErrors: { + timestamp: string; + message: string; + }[]; + customLogEntries: { + timestamp: string; + message: string; + }[]; + }; } // get the formatted cpu metrics diff --git a/package/src/metrics/logMonitor.ts b/package/src/metrics/logMonitor.ts new file mode 100644 index 0000000..54a6d79 --- /dev/null +++ b/package/src/metrics/logMonitor.ts @@ -0,0 +1,124 @@ +import { exec } from "child_process"; +import { promisify } from "util"; +import * as fs from "fs/promises"; +import * as os from "os"; +import { IMetricsData } from "./collector.js"; +import { logger } from "../utils/logger.js"; + +const execAsync = promisify(exec); + +export type LogMetrics = IMetricsData["logMetrics"]; + +class LogMonitor { + private customLogPath: string | null = null; + private patterns: RegExp[] = []; + private lastCustomCheck: number = 0; + + constructor(customLogPath?: string, patterns?: string[]) { + this.customLogPath = customLogPath || null; + this.patterns = patterns + ? patterns.map((p) => new RegExp(p, "i")) + : [new RegExp("error", "i"), new RegExp("fail", "i")]; + } + + async logMetrics(): Promise> { + const metrics: LogMetrics = { + timestamp: Date.now(), + systemErrors: [], + customLogEntries: [], + }; + + await Promise.all([ + this.getSystemErrors(metrics), + this.getCustomLogEntries(metrics), + ]); + + return { logMetrics: metrics }; + } + + private async getSystemErrors(metrics: LogMetrics): Promise { + try { + let errors: string[] = []; + if (os.platform() === "win32") { + const { stdout } = await execAsync( + 'wevtutil qe System /c:10 /rd:true /f:text | findstr "Error"' + ); + errors = stdout + .split("\n") + .filter((line) => line.trim() && this.matchesPattern(line)); + } else { + const { stdout } = await execAsync( + 'journalctl -p 3 -n 10 --since="1 hour ago"' + ); + errors = stdout + .split("\n") + .filter((line) => line.trim() && this.matchesPattern(line)); + } + metrics!.systemErrors = errors.map((line) => ({ + timestamp: this.extractTimestamp(line) || new Date().toLocaleString(), + message: line, + })); + } catch (error) { + console.warn("Failed to fetch system errors:", error); + } + } + + private async getCustomLogEntries(metrics: LogMetrics): Promise { + if (!this.customLogPath) return; + + try { + const stats = await fs.stat(this.customLogPath); + const lastModified = stats.mtimeMs; + if (lastModified <= this.lastCustomCheck) return; + + const content = await fs.readFile(this.customLogPath, "utf8"); + const lines = content.split("\n").slice(-50); // Last 50 lines + metrics!.customLogEntries = lines + .filter((line) => line.trim() && this.matchesPattern(line)) + .map((line) => ({ + timestamp: this.extractTimestamp(line) || new Date().toLocaleString(), + message: line, + })) + .slice(-10); // Last 10 matching lines + this.lastCustomCheck = lastModified; + } catch (error) { + console.warn( + `Failed to read custom log at ${this.customLogPath}:`, + error + ); + } + } + + private matchesPattern(line: string): boolean { + return this.patterns.some((pattern) => pattern.test(line)); + } + + private extractTimestamp(line: string): string | null { + // Simple regex for common timestamp formats (e.g., "2025-03-28 14:30:45") + const match = line.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/); + return match ? match[0] : null; + } + + // Method to update custom log path or patterns dynamically + setCustomLogPath(path: string) { + this.customLogPath = path; + this.lastCustomCheck = 0; // Reset to force re-read + } + + setPatterns(patterns: string[]) { + this.patterns = patterns.map((p) => new RegExp(p, "i")); + } +} + +export const logMonitor = new LogMonitor(); + +export async function getLogMetrics(): Promise> { + try { + const metrics = await logMonitor.logMetrics(); + return metrics; + } catch (error) { + console.error("Monitoring failed:", error); + logger.error(`Failed to get network metrics: ${(error as Error).message}`); + throw error; + } +} diff --git a/package/src/services/periodicMonitorServices/logMonitor.ts b/package/src/services/periodicMonitorServices/logMonitor.ts new file mode 100644 index 0000000..092a233 --- /dev/null +++ b/package/src/services/periodicMonitorServices/logMonitor.ts @@ -0,0 +1,115 @@ +import { + getLogMetrics, + LogMetrics, + logMonitor, +} from "../../metrics/logMonitor.js"; +import { AppConstants } from "../../utils/constant.js"; +import { logger } from "../../utils/logger.js"; +import { getStoreData } from "../../utils/store.js"; +import { + connectToIntegrationServer, + OutGoingMessageReplyType, + sendReply, +} from "../zeromqService.js"; + +export async function startLogMonitor(): Promise { + const storedData = getStoreData(); + const channelId = storedData?.outputChannelId; + + if (!channelId) { + logger.error("Channel ID not found. Cannot start log monitoring."); + // Return a dummy interval that we'll clear immediately + const dummyInterval = setInterval(() => {}, 1000); + clearInterval(dummyInterval); + return dummyInterval; + } + // connect to integration server + await connectToIntegrationServer(channelId); + logger.info("Starting log monitoring"); + + // Reset alert tracking + let lastAlertTime = 0; + + // start periodic monitoring + const cooldownPeriod = 5 * 60 * 1000; // 5 minutes in ms + + return setInterval(async () => { + try { + const metrics = await getLogMetrics(); + const storedData = getStoreData(); + const customLogPath = + storedData?.customLogPath || + `${AppConstants.Package.LogsDir}/error.log`; + const currentTime = Date.now(); + if (customLogPath) { + logMonitor.setCustomLogPath(customLogPath); + } + + if (metrics.logMetrics) { + if (!lastAlertTime || currentTime - lastAlertTime > cooldownPeriod) { + await sendLogMetrics(metrics.logMetrics, channelId); + + lastAlertTime = currentTime; + } + } + } catch (error) { + logger.error("Error in log monitoring:", error); + } + }, 60000); // 1 minute interval +} + +// send log metics to the integration server +async function sendLogMetrics(metrics: LogMetrics, channelId: string) { + if (!metrics) return; + let output = ` +┌─────────────────────────┐ + ⚠️ LOG METRICS +└─────────────────────────┘ +`; + + // System Errors + if (metrics.systemErrors.length) { + output += ` +== SYSTEM ERRORS == +Last Check: ${new Date(metrics.timestamp).toLocaleString()} +`; + metrics.systemErrors.forEach((entry, i) => { + output += ` +${i + 1}. [${entry.timestamp}] ${entry.message.substring(0, 80)}${ + entry.message.length > 80 ? "..." : "" + } +`; + }); + } else { + output += ` +== SYSTEM ERRORS == +No recent errors detected +`; + } + + // Custom Log Entries + if (metrics.customLogEntries.length) { + output += ` +== CUSTOM LOG ENTRIES == +Last Check: ${new Date(metrics.timestamp).toLocaleString()} +`; + metrics.customLogEntries.forEach((entry, i) => { + output += ` +${i + 1}. [${entry.timestamp}] ${entry.message.substring(0, 80)}${ + entry.message.length > 80 ? "..." : "" + } +`; + }); + } else if (metrics.customLogEntries.length === 0 && metrics.timestamp > 0) { + output += ` +== CUSTOM LOG ENTRIES == +No matching entries found +`; + } + + await sendReply( + channelId, + { message: output }, + OutGoingMessageReplyType.logMetricReply + ); +} diff --git a/package/src/services/periodicMonitorServices/monitor.ts b/package/src/services/periodicMonitorServices/monitor.ts index b8aec75..8db32e9 100644 --- a/package/src/services/periodicMonitorServices/monitor.ts +++ b/package/src/services/periodicMonitorServices/monitor.ts @@ -1,6 +1,7 @@ import { logger } from "../../utils/logger.js"; import "./cpuMonitor.js"; import { startCpuMonitoring } from "./cpuMonitor.js"; +import { startLogMonitor } from "./logMonitor.js"; import { startMemoryMonitoring } from "./memoryMonitor.js"; import { startSecurityMonitoring } from "./securityMonitor.js"; @@ -18,8 +19,9 @@ export const startAllIntervalMonitoring = async () => { const cpuTimer = await startCpuMonitoring(); const memoryTimer = await startMemoryMonitoring(); const securityTimer = await startSecurityMonitoring(); + const logTimer = await startLogMonitor(); - monitoringIntervals.push(cpuTimer, memoryTimer, securityTimer); + monitoringIntervals.push(cpuTimer, memoryTimer, securityTimer, logTimer); isMonitoringRunning = true; logger.info("All monitoring processes started"); }; diff --git a/package/src/services/zeromqService.ts b/package/src/services/zeromqService.ts index 95d50a3..0d0dca2 100644 --- a/package/src/services/zeromqService.ts +++ b/package/src/services/zeromqService.ts @@ -35,6 +35,7 @@ export enum OutGoingMessageReplyType { cpuThresholdAlertReply = "cpuThresholdAlertReply", memoryThresholdAlertReply = "memoryThresholdAlertReply", securityAlertReply = "securityAlertReply", + logMetricReply = "LogMetricReply", } // Create a mapping of functions for metrics collection @@ -279,6 +280,19 @@ async function handleMessages(channelId: string): Promise { `Stored security settings: ${JSON.stringify(securitySettings)}` ); + // Handle custom log path settings + const customLogPathSetting = message.data.settings.find( + (s: any) => s.label === "custom_log_path" + ); + if (customLogPathSetting) { + const customLogPath = customLogPathSetting.default || ""; + + saveStoreData({ + customLogPath: customLogPath, + }); + logger.info(`Stored custom log path: ${customLogPath}`); + } + // Store server name if available if (!getStoreData()?.serverName) { saveStoreData({ diff --git a/package/src/utils/store.ts b/package/src/utils/store.ts index 891e78b..c407c77 100644 --- a/package/src/utils/store.ts +++ b/package/src/utils/store.ts @@ -16,6 +16,7 @@ export interface IStore { monitorPortScanning: boolean; // Whether to monitor for port scanning monitorFirewall: boolean; // Whether to monitor firewall logs }; + customLogPath: string; // Path to custom log file } // Ensure store directory exists