Skip to main content
Glama
analyzers.ts10.7 kB
/** * Log Analyzers Module * Analyzes parsed logs for errors, performance, AI agents, and health * Ported from log-analyzer-mcp for DXP-110 */ interface ParsedLogEntry { timestamp: Date; type: 'console' | 'http' | 'unknown'; method: string; path: string; statusCode: number; userAgent: string; ip: string; message?: string; responseTime?: number; size?: number; referrer?: string; resourceId?: string; host?: string; isError?: boolean; level?: string; } interface ErrorAnalysis { total: number; byStatusCode: Record<string, number>; topErrors: Array<{ path: string; count: number; statusCodes: number[]; }>; } interface PerformanceAnalysis { avgResponseTime: number | null; p95ResponseTime: number | null; p99ResponseTime: number | null; slowestPaths: Array<{ path: string; avgTime: number; count: number; }>; } interface AIAgentAnalysis { detected: string[]; byAgent: Record<string, { requests: number; successRate: number; paths: Array<{ path: string; count: number; statusCodes: number[]; }>; }>; } interface HealthScore { score: number; healthy: boolean; } /** * Analyze errors in logs * @param {Object[]} logs - Parsed log entries * @returns {Object} Error analysis with guaranteed structure */ function analyzeErrors(logs: ParsedLogEntry[]): ErrorAnalysis { // DXP-128: Filter errors differently for console vs HTTP logs const errors = logs.filter(log => { // Console logs: use isError flag (level + keywords) if (log.type === 'console') { return log.isError === true; } // HTTP logs: use statusCode >= 400 return log.statusCode >= 400; }); const byStatusCode: Record<string, number> = {}; const byUrl: Record<string, { count: number; statusCodes: Set<number> }> = {}; for (const error of errors) { // DXP-137: Only populate byStatusCode for HTTP logs, not console logs // Console logs don't have meaningful HTTP status codes if (error.type !== 'console' && error.statusCode) { const code = String(error.statusCode); // Count by status code if (!byStatusCode[code]) { byStatusCode[code] = 0; } byStatusCode[code]++; } // Count by URL (for all error types) if (!byUrl[error.path]) { byUrl[error.path] = { count: 0, statusCodes: new Set() }; } byUrl[error.path].count++; if (error.statusCode) { byUrl[error.path].statusCodes.add(error.statusCode); } } // Convert to array of top errors const topErrors = Object.entries(byUrl) .map(([path, data]) => ({ path, count: data.count, statusCodes: Array.from(data.statusCodes) })) .sort((a, b) => b.count - a.count) .slice(0, 20); // DXP-137: Filter byStatusCode to only include error status codes (>= 400) // Exclude 2xx (success) and 3xx (redirects) from error reporting const errorStatusCodes: Record<string, number> = {}; for (const [code, count] of Object.entries(byStatusCode)) { const statusCode = parseInt(code, 10); if (statusCode >= 400) { errorStatusCodes[code] = count; } } return { total: errors.length, byStatusCode: errorStatusCodes, topErrors }; } /** * Analyze performance metrics * @param {Object[]} logs - Parsed log entries * @returns {Object} Performance analysis with guaranteed structure */ function analyzePerformance(logs: ParsedLogEntry[]): PerformanceAnalysis { const logsWithTiming = logs.filter(log => log.responseTime !== undefined && log.responseTime > 0); if (logsWithTiming.length === 0) { return { avgResponseTime: null, p95ResponseTime: null, p99ResponseTime: null, slowestPaths: [] }; } // Calculate average const sum = logsWithTiming.reduce((acc, log) => acc + log.responseTime!, 0); const avg = Math.round(sum / logsWithTiming.length); // Calculate percentiles const sorted = logsWithTiming.map(log => log.responseTime!).sort((a, b) => a - b); const p95Index = Math.floor(sorted.length * 0.95); const p99Index = Math.floor(sorted.length * 0.99); const p95 = sorted[p95Index] || 0; const p99 = sorted[p99Index] || 0; // Find slowest paths const pathTimes: Record<string, { total: number; count: number }> = {}; for (const log of logsWithTiming) { if (!pathTimes[log.path]) { pathTimes[log.path] = { total: 0, count: 0 }; } pathTimes[log.path].total += log.responseTime!; pathTimes[log.path].count++; } const slowestPaths = Object.entries(pathTimes) .map(([path, data]) => ({ path, avgTime: Math.round(data.total / data.count), count: data.count })) .sort((a, b) => b.avgTime - a.avgTime) .slice(0, 10); return { avgResponseTime: avg, p95ResponseTime: Math.round(p95), p99ResponseTime: Math.round(p99), slowestPaths }; } /** * Detect AI agents in logs * @param {Object[]} logs - Parsed log entries * @returns {Object} AI agent analysis with guaranteed structure */ function detectAIAgents(logs: ParsedLogEntry[]): AIAgentAnalysis { // AI agent patterns (from log-analyzer-mcp) const patterns = [ { name: 'ChatGPT-User', pattern: /ChatGPT-User/i }, { name: 'GPTBot', pattern: /GPTBot/i }, { name: 'ClaudeBot', pattern: /ClaudeBot|Claude-Web/i }, { name: 'Google-Extended', pattern: /Google-Extended/i }, { name: 'Bingbot', pattern: /bingbot/i }, { name: 'Anthropic-AI', pattern: /anthropic/i }, { name: 'PerplexityBot', pattern: /PerplexityBot/i }, { name: 'Applebot-Extended', pattern: /Applebot-Extended/i } ]; const aiLogs: (ParsedLogEntry & { aiAgent: string })[] = []; const agentStats: Record<string, { requests: number; successCount: number; paths: Set<string>; }> = {}; for (const log of logs) { const userAgent = log.userAgent || ''; for (const { name, pattern } of patterns) { if (pattern.test(userAgent)) { aiLogs.push({ ...log, aiAgent: name }); if (!agentStats[name]) { agentStats[name] = { requests: 0, successCount: 0, paths: new Set() }; } agentStats[name].requests++; if (log.statusCode < 400) { agentStats[name].successCount++; } agentStats[name].paths.add(log.path); break; } } } // Build structured output const detected = Object.keys(agentStats); const byAgent: Record<string, { requests: number; successRate: number; paths: Array<{ path: string; count: number; statusCodes: number[]; }>; }> = {}; for (const [agent, stats] of Object.entries(agentStats)) { byAgent[agent] = { requests: stats.requests, successRate: stats.requests > 0 ? Math.round((stats.successCount / stats.requests) * 100) / 100 : 0, paths: Array.from(stats.paths).slice(0, 10).map(path => ({ path, count: 1, // Simplified statusCodes: [] })) }; } return { detected, byAgent }; } /** * Calculate health score based on error rate and issues * @param {Object} errorAnalysis - Error analysis results * @param {number} totalLogs - Total log count * @returns {Object} Health score and status */ function calculateHealthScore(errorAnalysis: ErrorAnalysis, totalLogs: number): HealthScore { if (totalLogs === 0) { return { score: 100, healthy: true }; } const errorRate = (errorAnalysis.total / totalLogs) * 100; // Deduct points for error rate let score = 100 - (errorRate * 2); // Deduct points for 500 errors const serverErrors = Object.keys(errorAnalysis.byStatusCode || {}) .filter(code => code.startsWith('5')) .reduce((sum, code) => sum + errorAnalysis.byStatusCode[code], 0); score -= serverErrors * 0.5; // Cap between 0-100 score = Math.max(0, Math.min(100, Math.round(score))); return { score, healthy: score >= 70 }; } /** * Generate recommendations based on analysis * @param {Object} errorAnalysis - Error analysis results * @param {Object} perfAnalysis - Performance analysis results * @param {Object} aiAnalysis - AI agent analysis results * @returns {string[]} Array of recommendation strings */ function generateRecommendations( errorAnalysis: ErrorAnalysis, perfAnalysis: PerformanceAnalysis, aiAnalysis: AIAgentAnalysis ): string[] { const recommendations: string[] = []; // High error rate if (errorAnalysis.total > 0) { const total404s = errorAnalysis.byStatusCode['404'] || 0; if (total404s > 10) { recommendations.push(`HIGH: ${total404s} 404 errors detected - review and fix broken links`); } const serverErrors = Object.keys(errorAnalysis.byStatusCode) .filter(code => code.startsWith('5')) .reduce((sum, code) => sum + errorAnalysis.byStatusCode[code], 0); if (serverErrors > 0) { recommendations.push(`HIGH: ${serverErrors} server errors detected - investigate application issues`); } } // Slow performance if (perfAnalysis.p95ResponseTime && perfAnalysis.p95ResponseTime > 3000) { recommendations.push(`MEDIUM: Slow response times detected (P95: ${perfAnalysis.p95ResponseTime}ms) - optimize slow endpoints`); } // AI agent issues for (const [agent, data] of Object.entries(aiAnalysis.byAgent)) { if (data.successRate < 0.8) { recommendations.push(`MEDIUM: ${agent} has low success rate (${(data.successRate * 100).toFixed(0)}%) - review AI agent access patterns`); } } return recommendations; } export default { analyzeErrors, analyzePerformance, detectAIAgents, calculateHealthScore, generateRecommendations };

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/JaxonDigital/optimizely-dxp-mcp'

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