UNPKG

@ooples/token-optimizer-mcp

Version:

Intelligent context window optimization for Claude Code - store content externally via caching and compression, freeing up your context window for what matters

695 lines • 25.1 kB
/** * Smart Logs Tool - System Log Aggregation and Analysis * * Provides intelligent log analysis with: * - Multi-source log aggregation * - Pattern filtering and error detection * - Log level analysis * - Token-optimized output */ import { spawn } from 'child_process'; import { CacheEngine } from '../../core/cache-engine.js'; import { createHash } from 'crypto'; import { existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class SmartLogs { cache; cacheNamespace = 'smart_logs'; projectRoot; constructor(cache, projectRoot) { this.cache = cache; this.projectRoot = projectRoot || process.cwd(); } /** * Aggregate and analyze logs */ async run(options = {}) { const { sources = [], level = 'all', pattern, tail = 100, follow = false, since, maxCacheAge = 300, // Logs have shorter cache (5 min) } = options; const startTime = Date.now(); // Generate cache key const cacheKey = this.generateCacheKey(sources, level, pattern, tail, since); // Check cache first (unless follow mode) if (!follow) { const cached = this.getCachedResult(cacheKey, maxCacheAge); if (cached) { return this.formatCachedOutput(cached); } } // Aggregate logs from all sources const result = await this.aggregateLogs({ sources, level, pattern, tail, since, }); const duration = Date.now() - startTime; result.duration = duration; // Cache the result (unless follow mode) if (!follow) { this.cacheResult(cacheKey, result); } // Generate insights const insights = this.generateInsights(result); // Transform to smart output return this.transformOutput(result, insights); } /** * Aggregate logs from multiple sources */ async aggregateLogs(options) { const { sources, level, pattern, tail, since } = options; const allEntries = []; // If no sources specified, try common log locations const logSources = sources.length > 0 ? sources : this.getDefaultLogSources(); // Collect from each source for (const source of logSources) { const entries = await this.readLogSource(source, tail); allEntries.push(...entries); } // Filter by level let filtered = level === 'all' ? allEntries : allEntries.filter((e) => e.level === level); // Filter by pattern if (pattern) { const regex = new RegExp(pattern, 'i'); filtered = filtered.filter((e) => regex.test(e.message)); } // Filter by time if (since) { const cutoffTime = this.parseTimeRange(since); filtered = filtered.filter((e) => new Date(e.timestamp) >= cutoffTime); } // Sort by timestamp (newest first) filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); // Limit to tail count filtered = filtered.slice(0, tail); // Calculate statistics const stats = this.calculateStats(filtered); // Detect patterns const patterns = this.detectPatterns(filtered); return { success: true, entries: filtered, stats, patterns, duration: 0, // Set by caller timestamp: Date.now(), }; } /** * Get default log sources based on OS */ getDefaultLogSources() { const sources = []; // Application logs const appLogPath = join(this.projectRoot, 'logs'); if (existsSync(appLogPath)) { sources.push(join(appLogPath, 'app.log')); sources.push(join(appLogPath, 'error.log')); } // System logs (platform-specific) if (process.platform === 'win32') { // Windows Event Logs would need PowerShell sources.push('system:application'); } else if (process.platform === 'darwin') { sources.push('/var/log/system.log'); } else { sources.push('/var/log/syslog'); } return sources.filter((s) => existsSync(s) || s.startsWith('system:')); } /** * Read logs from a single source */ async readLogSource(source, tail) { // System logs vs file logs if (source.startsWith('system:')) { return this.readSystemLogs(source, tail); } else { return this.readFileLog(source, tail); } } /** * Read logs from a file */ async readFileLog(filePath, tail) { if (!existsSync(filePath)) { return []; } return new Promise((resolve) => { const entries = []; let output = ''; // Use tail command on Unix, Get-Content on Windows const command = process.platform === 'win32' ? 'powershell' : 'tail'; const args = process.platform === 'win32' ? ['-Command', `Get-Content -Path "${filePath}" -Tail ${tail}`] : ['-n', tail.toString(), filePath]; const child = spawn(command, args, { shell: true }); child.stdout.on('data', (data) => { output += data.toString(); }); child.on('close', () => { const lines = output.split('\n').filter((l) => l.trim()); for (const line of lines) { const entry = this.parseLogLine(line, filePath); if (entry) { entries.push(entry); } } resolve(entries); }); child.on('error', () => { resolve([]); // Return empty on error }); }); } /** * Read system logs */ async readSystemLogs(source, tail) { const logType = source.split(':')[1]; return new Promise((resolve) => { const entries = []; let output = ''; let command; let args; if (process.platform === 'win32') { // Windows Event Log command = 'powershell'; args = [ '-Command', `Get-EventLog -LogName ${logType} -Newest ${tail} | Select-Object TimeGenerated,EntryType,Message | ConvertTo-Json`, ]; } else if (process.platform === 'darwin') { // macOS - use log show command = 'log'; args = ['show', '--last', '1h', '--style', 'json']; } else { // Linux - use journalctl command = 'journalctl'; args = ['-n', tail.toString(), '-o', 'json']; } const child = spawn(command, args, { shell: true }); child.stdout.on('data', (data) => { output += data.toString(); }); child.on('close', () => { try { const parsed = JSON.parse(output); const items = Array.isArray(parsed) ? parsed : [parsed]; for (const item of items) { entries.push(this.parseSystemLogEntry(item, source)); } } catch { // Failed to parse JSON, try line-by-line const lines = output.split('\n').filter((l) => l.trim()); for (const line of lines) { const entry = this.parseLogLine(line, source); if (entry) entries.push(entry); } } resolve(entries); }); child.on('error', () => { resolve([]); }); }); } /** * Parse a log line */ parseLogLine(line, source) { // Try common log formats // ISO timestamp format: 2024-01-01T12:00:00.000Z [ERROR] message const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\s+\[(ERROR|WARN|INFO|DEBUG)\]\s+(.+)$/); if (isoMatch) { return { timestamp: isoMatch[1], level: isoMatch[2].toLowerCase(), source, message: isoMatch[3], }; } // Syslog format: Jan 1 12:00:00 hostname message const syslogMatch = line.match(/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+(.+)$/); if (syslogMatch) { return { timestamp: new Date(syslogMatch[1]).toISOString(), level: this.detectLogLevel(syslogMatch[2]), source, message: syslogMatch[2], }; } // Default: treat whole line as message return { timestamp: new Date().toISOString(), level: this.detectLogLevel(line), source, message: line, }; } /** * Parse system log entry */ parseSystemLogEntry(item, source) { const entry = item; return { timestamp: entry.TimeGenerated || entry.timestamp || new Date().toISOString(), level: this.mapSystemLogLevel(entry.EntryType || entry.level), source, message: (entry.Message || entry.message || ''), metadata: entry, }; } /** * Detect log level from message */ detectLogLevel(message) { const lower = message.toLowerCase(); if (lower.includes('error') || lower.includes('fatal') || lower.includes('critical')) { return 'error'; } if (lower.includes('warn') || lower.includes('warning')) { return 'warn'; } if (lower.includes('debug') || lower.includes('trace')) { return 'debug'; } return 'info'; } /** * Map system log level to our levels */ mapSystemLogLevel(level) { const str = String(level).toLowerCase(); if (str.includes('error') || str === '1') return 'error'; if (str.includes('warn') || str === '2' || str === '3') return 'warn'; if (str.includes('debug') || str === '5') return 'debug'; return 'info'; } /** * Parse time range string */ parseTimeRange(since) { const now = Date.now(); const match = since.match(/^(\d+)([hdwm])$/); if (!match) { return new Date(now - 3600000); // Default 1 hour } const value = parseInt(match[1], 10); const unit = match[2]; const multipliers = { h: 3600000, // hours d: 86400000, // days w: 604800000, // weeks m: 2592000000, // months (30 days) }; const offset = value * (multipliers[unit] || 3600000); return new Date(now - offset); } /** * Calculate statistics */ calculateStats(entries) { const byLevel = {}; const bySource = {}; let earliest = new Date(); let latest = new Date(0); for (const entry of entries) { byLevel[entry.level] = (byLevel[entry.level] || 0) + 1; bySource[entry.source] = (bySource[entry.source] || 0) + 1; const time = new Date(entry.timestamp); if (time < earliest) earliest = time; if (time > latest) latest = time; } return { total: entries.length, byLevel, bySource, timeRange: { start: earliest.toISOString(), end: latest.toISOString(), }, }; } /** * Detect common patterns */ detectPatterns(entries) { const patterns = new Map(); for (const entry of entries) { // Extract error codes and common patterns const errorCode = entry.message.match(/([A-Z]{2,}\d{4}|ERR_[A-Z_]+)/); if (errorCode) { const key = errorCode[1]; patterns.set(key, (patterns.get(key) || 0) + 1); } // Extract exception types const exception = entry.message.match(/(\w+Exception|Error):/); if (exception) { const key = exception[1]; patterns.set(key, (patterns.get(key) || 0) + 1); } } return Array.from(patterns.entries()) .map(([pattern, count]) => ({ pattern, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); } /** * Generate insights */ generateInsights(result) { const insights = []; // High error rate const errorRate = (result.stats.byLevel.error || 0) / result.stats.total; if (errorRate > 0.1) { insights.push({ type: 'error', message: `High error rate: ${(errorRate * 100).toFixed(1)}% of logs are errors`, impact: 'high', }); } // Repeated patterns const topPattern = result.patterns[0]; if (topPattern && topPattern.count > 10) { insights.push({ type: 'error', message: `Repeated error pattern: "${topPattern.pattern}" appears ${topPattern.count} times`, impact: 'high', }); } // Warning accumulation const warnCount = result.stats.byLevel.warn || 0; if (warnCount > 50) { insights.push({ type: 'warning', message: `High warning count: ${warnCount} warnings detected`, impact: 'medium', }); } return insights; } /** * Generate cache key */ generateCacheKey(sources, level, pattern, tail, since) { const keyParts = [ sources.join(','), level, pattern || '', tail.toString(), since || '', ]; return createHash('md5').update(keyParts.join(':')).digest('hex'); } /** * Get cached result */ getCachedResult(key, maxAge) { const cached = this.cache.get(this.cacheNamespace + ':' + key); if (!cached) return null; try { const result = JSON.parse(cached); const age = (Date.now() - result.cachedAt) / 1000; if (age <= maxAge) { return result; } } catch (err) { return null; } return null; } /** * Cache result */ cacheResult(key, result) { const cacheData = { ...result, cachedAt: Date.now() }; const dataToCache = JSON.stringify(cacheData); const originalSize = this.estimateOriginalOutputSize(result); const compactSize = dataToCache.length; this.cache.set(this.cacheNamespace + ':' + key, dataToCache, originalSize, compactSize); } /** * Transform to smart output */ transformOutput(result, insights, fromCache = false) { // For small datasets (< 20 entries), skip smart processing overhead // This prevents negative reduction percentages for small logs if (result.entries.length < 20) { const entries = result.entries.map((e) => ({ timestamp: e.timestamp, level: e.level, source: e.source, message: e.message, // Full message, no truncation for small datasets })); const originalSize = JSON.stringify(entries).length; const compactSize = originalSize; // No processing = no reduction const timeRangeDuration = new Date(result.stats.timeRange.end).getTime() - new Date(result.stats.timeRange.start).getTime(); const timeRangeStr = `${(timeRangeDuration / 60000).toFixed(0)} minutes`; return { summary: { success: result.success, totalEntries: result.stats.total, errorCount: result.stats.byLevel.error || 0, warnCount: result.stats.byLevel.warn || 0, timeRange: timeRangeStr, duration: result.duration, fromCache, }, entries: entries, // All entries, no slicing stats: { byLevel: result.stats.byLevel, bySource: result.stats.bySource, }, patterns: [], // No patterns for small datasets insights: [], // No insights for small datasets metrics: { originalTokens: Math.ceil(originalSize / 4), compactedTokens: Math.ceil(compactSize / 4), reductionPercentage: 0, // No reduction for small datasets }, }; } // For large datasets (≄ 20 entries), use full smart processing const entries = result.entries.map((e) => ({ timestamp: e.timestamp, level: e.level, source: e.source, message: e.message.substring(0, 200), // Truncate long messages })); const patterns = result.patterns.map((p) => ({ pattern: p.pattern, count: p.count, severity: p.count > 50 ? 'critical' : p.count > 20 ? 'high' : p.count > 10 ? 'medium' : 'low', })); const originalSize = this.estimateOriginalOutputSize(result); const compactSize = this.estimateCompactSize(entries.slice(0, 50), // Only measure what we actually send patterns, insights); const timeRangeDuration = new Date(result.stats.timeRange.end).getTime() - new Date(result.stats.timeRange.start).getTime(); const timeRangeStr = `${(timeRangeDuration / 60000).toFixed(0)} minutes`; return { summary: { success: result.success, totalEntries: result.stats.total, errorCount: result.stats.byLevel.error || 0, warnCount: result.stats.byLevel.warn || 0, timeRange: timeRangeStr, duration: result.duration, fromCache, }, entries: entries.slice(0, 50), // Limit to 50 for output stats: { byLevel: result.stats.byLevel, bySource: result.stats.bySource, }, patterns, insights, metrics: { originalTokens: Math.ceil(originalSize / 4), compactedTokens: Math.ceil(compactSize / 4), reductionPercentage: Math.round(((originalSize - compactSize) / originalSize) * 100), }, }; } /** * Format cached output */ formatCachedOutput(result) { return this.transformOutput(result, [], true); } /** * Estimate original output size */ estimateOriginalOutputSize(result) { // Measure what would be sent WITHOUT compaction (full JSON) return JSON.stringify(result.entries).length; } /** * Estimate compact output size */ estimateCompactSize(entries, patterns, insights) { // Measure what IS sent WITH compaction (truncated entries + patterns + insights) return JSON.stringify({ entries, patterns, insights }).length; } /** * Close cache connection */ close() { this.cache.close(); } } /** * Factory function for dependency injection */ export function getSmartLogs(cache, projectRoot) { return new SmartLogs(cache, projectRoot); } /** * CLI-friendly function for running smart logs */ export async function runSmartLogs(options = {}) { const cache = new CacheEngine(join(homedir(), '.hypercontext', 'cache'), 100); const smartLogs = getSmartLogs(cache, options.projectRoot); try { const result = await smartLogs.run(options); let output = `\nšŸ“‹ Smart Logs Analysis ${result.summary.fromCache ? '(cached)' : ''}\n`; output += `${'='.repeat(50)}\n\n`; // Summary output += `Summary:\n`; output += ` Total Entries: ${result.summary.totalEntries}\n`; output += ` Errors: ${result.summary.errorCount}\n`; output += ` Warnings: ${result.summary.warnCount}\n`; output += ` Time Range: ${result.summary.timeRange}\n`; output += ` Duration: ${(result.summary.duration / 1000).toFixed(2)}s\n\n`; // Statistics output += `Statistics by Level:\n`; for (const [level, count] of Object.entries(result.stats.byLevel)) { const icon = level === 'error' ? 'šŸ”“' : level === 'warn' ? 'āš ļø' : 'ā„¹ļø'; output += ` ${icon} ${level}: ${count}\n`; } output += '\n'; // Patterns if (result.patterns.length > 0) { output += `Common Patterns:\n`; for (const pattern of result.patterns.slice(0, 5)) { const icon = pattern.severity === 'critical' ? 'šŸ”“' : pattern.severity === 'high' ? '🟔' : '🟢'; output += ` ${icon} ${pattern.pattern} (${pattern.count} occurrences)\n`; } output += '\n'; } // Recent entries if (result.entries.length > 0) { output += `Recent Log Entries (showing ${Math.min(result.entries.length, 10)}):\n`; for (const entry of result.entries.slice(0, 10)) { const icon = entry.level === 'error' ? 'šŸ”“' : entry.level === 'warn' ? 'āš ļø' : 'ā„¹ļø'; const time = new Date(entry.timestamp).toLocaleTimeString(); output += ` ${icon} [${time}] ${entry.message.substring(0, 80)}\n`; } output += '\n'; } // Insights if (result.insights.length > 0) { output += `Insights:\n`; for (const insight of result.insights) { const icon = insight.impact === 'high' ? 'šŸ”“' : insight.impact === 'medium' ? '🟔' : '🟢'; output += ` ${icon} [${insight.type}] ${insight.message}\n`; } output += '\n'; } // Metrics output += `Token Reduction:\n`; output += ` Original: ${result.metrics.originalTokens} tokens\n`; output += ` Compacted: ${result.metrics.compactedTokens} tokens\n`; output += ` Reduction: ${result.metrics.reductionPercentage}%\n`; return output; } finally { smartLogs.close(); } } // MCP Tool definition export const SMART_LOGS_TOOL_DEFINITION = { name: 'smart_logs', description: 'System log aggregation and analysis with multi-source support, pattern filtering, error detection, and insights', inputSchema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'string' }, description: 'Log sources to aggregate (file paths or system logs)', }, level: { type: 'string', enum: ['error', 'warn', 'info', 'debug', 'all'], description: 'Filter by log level', default: 'all', }, pattern: { type: 'string', description: 'Filter by pattern (regex)', }, tail: { type: 'number', description: 'Number of lines to tail', default: 100, }, follow: { type: 'boolean', description: 'Follow mode (watch for new entries)', default: false, }, since: { type: 'string', description: "Time range filter (e.g., '1h', '24h', '7d')", }, projectRoot: { type: 'string', description: 'Project root directory', }, maxCacheAge: { type: 'number', description: 'Maximum cache age in seconds (default: 300)', default: 300, }, }, }, }; //# sourceMappingURL=smart-logs.js.map