Skip to main content
Glama
fileWatcher.ts5.65 kB
import * as chokidar from 'chokidar'; import * as fs from 'fs/promises'; import { LogAnalysis, FileWatchResult, WatchOptions } from '../types.js'; import { LogAnalyzer } from './logAnalyzer.js'; import { LogUtils } from '../utils.js'; interface WatchedFile { filePath: string; watcher: chokidar.FSWatcher; lastSize: number; errors: LogAnalysis[]; lastUpdate: Date; options: WatchOptions; } export class FileWatcher { private watchers = new Map<string, WatchedFile>(); private logAnalyzer: LogAnalyzer; constructor() { this.logAnalyzer = new LogAnalyzer(); } async watchLogFile(filePath: string, options: WatchOptions = {}): Promise<void> { // Validate file path first const validation = await LogUtils.validateFilePath(filePath); if (!validation.valid) { throw new Error(`Cannot watch file: ${validation.error}`); } // Stop existing watcher if present if (this.watchers.has(filePath)) { await this.stopWatching(filePath); } const watchOptions: WatchOptions = { pollInterval: options.pollInterval || 1000, ignoreInitial: options.ignoreInitial ?? false, usePolling: options.usePolling ?? true }; // Get initial file size const stats = await fs.stat(filePath); const initialSize = stats.size; // Create watcher const watcher = chokidar.watch(filePath, { ignoreInitial: watchOptions.ignoreInitial, usePolling: watchOptions.usePolling, interval: watchOptions.pollInterval }); const watchedFile: WatchedFile = { filePath, watcher, lastSize: initialSize, errors: [], lastUpdate: new Date(), options: watchOptions }; // Set up event handlers watcher.on('change', async (path) => { await this.handleFileChange(path); }); watcher.on('error', (error) => { console.error(`File watcher error for ${filePath}:`, error); }); this.watchers.set(filePath, watchedFile); // Process initial content if not ignoring initial if (!watchOptions.ignoreInitial) { await this.handleFileChange(filePath); } } async stopWatching(filePath: string): Promise<void> { const watchedFile = this.watchers.get(filePath); if (!watchedFile) { throw new Error(`File ${filePath} is not being watched`); } await watchedFile.watcher.close(); this.watchers.delete(filePath); } async listWatchedFiles(): Promise<FileWatchResult[]> { const results: FileWatchResult[] = []; for (const [filePath, watchedFile] of this.watchers) { results.push({ filePath, newErrors: watchedFile.errors.slice(-5), // Last 5 errors totalErrors: watchedFile.errors.length, lastUpdate: watchedFile.lastUpdate }); } return results; } async getRecentErrors(filePath?: string, limit: number = 10): Promise<LogAnalysis[]> { if (filePath) { const watchedFile = this.watchers.get(filePath); if (!watchedFile) { throw new Error(`File ${filePath} is not being watched`); } return watchedFile.errors.slice(-limit); } // Get recent errors from all watched files const allErrors: LogAnalysis[] = []; for (const watchedFile of this.watchers.values()) { allErrors.push(...watchedFile.errors); } // Sort by timestamp and return most recent return allErrors .sort((a, b) => b.metadata.timestamp.getTime() - a.metadata.timestamp.getTime()) .slice(0, limit); } private async handleFileChange(filePath: string): Promise<void> { const watchedFile = this.watchers.get(filePath); if (!watchedFile) { return; } try { // Get current file size const stats = await fs.stat(filePath); const currentSize = stats.size; // Only process if file has grown (new content added) if (currentSize <= watchedFile.lastSize) { return; } // Read only the new content const newContent = await this.readNewContent(filePath, watchedFile.lastSize, currentSize); if (newContent.trim()) { // Check if new content contains errors const errorPatterns = LogUtils.extractErrorPatterns(newContent); if (errorPatterns.length > 0) { // Analyze the new errors const analysis = await this.logAnalyzer.analyzeLogs(newContent, { logFormat: 'auto', contextLines: 20 }); // Store the analysis watchedFile.errors.push(analysis); // Keep only last 100 errors to prevent memory issues if (watchedFile.errors.length > 100) { watchedFile.errors = watchedFile.errors.slice(-100); } console.error(`New error detected in ${filePath}: ${analysis.rootCause}`); } } // Update tracking info watchedFile.lastSize = currentSize; watchedFile.lastUpdate = new Date(); } catch (error) { console.error(`Error processing file change for ${filePath}:`, error); } } private async readNewContent(filePath: string, startByte: number, endByte: number): Promise<string> { const fileHandle = await fs.open(filePath, 'r'); try { const buffer = Buffer.alloc(endByte - startByte); await fileHandle.read(buffer, 0, buffer.length, startByte); return buffer.toString('utf8'); } finally { await fileHandle.close(); } } async stopAll(): Promise<void> { const stopPromises = Array.from(this.watchers.keys()).map(filePath => this.stopWatching(filePath) ); await Promise.all(stopPromises); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiragPatankar/loganalyzer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server