Skip to main content
Glama
log-parser.ts4.05 kB
import { randomUUID } from 'crypto'; import type { ErrorEntry, LogPattern, Config } from './types/index.js'; export class LogParser { private patterns: LogPattern[] = []; private errorHistory: ErrorEntry[] = []; private historyLimit: number; constructor(config: Config) { this.patterns = config.patterns; this.historyLimit = config.historyLimit; } updateConfig(config: Config): void { this.patterns = config.patterns; this.historyLimit = config.historyLimit; this.trimHistory(); } parseLog(logLine: string): ErrorEntry | null { const trimmedLine = logLine.trim(); if (!trimmedLine) return null; for (const pattern of this.patterns) { const match = pattern.pattern.exec(trimmedLine); if (match) { const error = this.createErrorEntry(pattern, match, trimmedLine); this.addToHistory(error); pattern.pattern.lastIndex = 0; // Reset regex state return error; } } return null; } private createErrorEntry(pattern: LogPattern, match: RegExpExecArray, raw: string): ErrorEntry { const error: ErrorEntry = { id: randomUUID(), timestamp: new Date(), severity: pattern.severity, category: pattern.category, message: this.extractField(match, pattern.extract.message) || match[0] || raw, raw, }; // Extract file information if available const file = this.extractField(match, pattern.extract.file); if (file) { error.file = file; } const line = this.extractField(match, pattern.extract.line); if (line && !isNaN(Number(line))) { error.line = Number(line); } const column = this.extractField(match, pattern.extract.column); if (column && !isNaN(Number(column))) { error.column = Number(column); } return error; } private extractField(match: RegExpExecArray, fieldIndex?: number): string | undefined { if (fieldIndex === undefined || fieldIndex >= match.length) { return undefined; } return match[fieldIndex]?.trim(); } private addToHistory(error: ErrorEntry): void { this.errorHistory.unshift(error); this.trimHistory(); } private trimHistory(): void { if (this.errorHistory.length > this.historyLimit) { this.errorHistory = this.errorHistory.slice(0, this.historyLimit); } } getHistory(): ErrorEntry[] { return [...this.errorHistory]; } getErrorsForFile(filepath: string): ErrorEntry[] { return this.errorHistory.filter(error => error.file && (error.file === filepath || error.file.endsWith(`/${filepath}`)) ); } getErrorsBySeverity(severity: ErrorEntry['severity']): ErrorEntry[] { return this.errorHistory.filter(error => error.severity === severity); } getErrorsByCategory(category: ErrorEntry['category']): ErrorEntry[] { return this.errorHistory.filter(error => error.category === category); } getRecentErrors(count = 10): ErrorEntry[] { return this.errorHistory.slice(0, count); } clearHistory(): void { this.errorHistory = []; } getErrorCounts(): { critical: number; warning: number; info: number } { return { critical: this.errorHistory.filter(e => e.severity === 'critical').length, warning: this.errorHistory.filter(e => e.severity === 'warning').length, info: this.errorHistory.filter(e => e.severity === 'info').length, }; } getCategoryCounts(): Record<ErrorEntry['category'], number> { const counts: Record<ErrorEntry['category'], number> = { typescript: 0, svelte: 0, vite: 0, network: 0, build: 0, runtime: 0, accessibility: 0, unknown: 0, }; for (const error of this.errorHistory) { counts[error.category]++; } return counts; } getFileCounts(): Record<string, number> { const counts: Record<string, number> = {}; for (const error of this.errorHistory) { if (error.file) { counts[error.file] = (counts[error.file] || 0) + 1; } } return counts; } }

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/mntlabs/devserver-mcp'

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