Skip to main content
Glama
time_based_error_parser.go8.11 kB
package logs import ( "fmt" "regexp" "strings" "time" ) // TimeBasedErrorCluster represents a group of log lines that form one error type TimeBasedErrorCluster struct { ID string ProcessID string ProcessName string StartTime time.Time EndTime time.Time Lines []LogEntry ErrorType string Message string Severity string } // TimeBasedErrorParser groups error lines using time gaps type TimeBasedErrorParser struct { // Active clusters being built per process activeClusters map[string]*TimeBasedErrorCluster // Completed error clusters completedClusters []TimeBasedErrorCluster // Time gap threshold to trigger cluster completion timeGapThreshold time.Duration // Minimum lines to consider as an error cluster minClusterSize int } func NewTimeBasedErrorParser() *TimeBasedErrorParser { return &TimeBasedErrorParser{ activeClusters: make(map[string]*TimeBasedErrorCluster), completedClusters: make([]TimeBasedErrorCluster, 0), timeGapThreshold: 200 * time.Millisecond, // 200ms gap triggers completion minClusterSize: 1, // Even single lines can be errors } } // ProcessLogEntry processes a log entry and potentially completes error clusters func (p *TimeBasedErrorParser) ProcessLogEntry(entry LogEntry, processName string, isError bool) *TimeBasedErrorCluster { // Only process error lines if !isError && entry.Level < LevelError { return nil } processKey := entry.ProcessID // Check if we have an active cluster for this process if activeCluster, exists := p.activeClusters[processKey]; exists { // Check time gap timeSinceLastLine := entry.Timestamp.Sub(activeCluster.EndTime) if timeSinceLastLine > p.timeGapThreshold { // Time gap detected - finalize the current cluster p.finalizeCluster(activeCluster) completed := *activeCluster delete(p.activeClusters, processKey) // Start a new cluster with this entry p.startNewCluster(entry, processName, processKey) return &completed } else { // Add to existing cluster activeCluster.Lines = append(activeCluster.Lines, entry) activeCluster.EndTime = entry.Timestamp p.updateClusterAnalysis(activeCluster) return nil } } else { // Start new cluster p.startNewCluster(entry, processName, processKey) return nil } } // ForceCompleteAll completes all active clusters (useful for shutdown) func (p *TimeBasedErrorParser) ForceCompleteAll() []TimeBasedErrorCluster { var completed []TimeBasedErrorCluster for processKey, cluster := range p.activeClusters { p.finalizeCluster(cluster) completed = append(completed, *cluster) delete(p.activeClusters, processKey) } return completed } // GetCompletedClusters returns all completed error clusters func (p *TimeBasedErrorParser) GetCompletedClusters() []TimeBasedErrorCluster { return p.completedClusters } func (p *TimeBasedErrorParser) startNewCluster(entry LogEntry, processName, processKey string) { cluster := &TimeBasedErrorCluster{ ID: fmt.Sprintf("%s-%d", processKey, entry.Timestamp.UnixNano()), ProcessID: entry.ProcessID, ProcessName: processName, StartTime: entry.Timestamp, EndTime: entry.Timestamp, Lines: []LogEntry{entry}, } p.updateClusterAnalysis(cluster) p.activeClusters[processKey] = cluster } func (p *TimeBasedErrorParser) finalizeCluster(cluster *TimeBasedErrorCluster) { // Only finalize if it meets minimum size if len(cluster.Lines) >= p.minClusterSize { p.completedClusters = append(p.completedClusters, *cluster) // Keep only last 100 clusters to prevent memory growth if len(p.completedClusters) > 100 { p.completedClusters = p.completedClusters[1:] } } } func (p *TimeBasedErrorParser) updateClusterAnalysis(cluster *TimeBasedErrorCluster) { if len(cluster.Lines) == 0 { return } // Combine all content from the cluster var allContent []string for _, line := range cluster.Lines { allContent = append(allContent, line.Content) } combinedContent := strings.Join(allContent, "\n") // Simple error type detection on the combined content cluster.ErrorType = p.detectErrorType(combinedContent) cluster.Message = p.extractMainMessage(cluster.Lines[0].Content) // Use first line as primary message cluster.Severity = p.determineSeverity(combinedContent) } func (p *TimeBasedErrorParser) detectErrorType(content string) string { content = strings.ToLower(content) // Check for specific error types in order of specificity errorTypes := map[string][]string{ "MongoError": {"mongoerror", "mongo", "mongodb"}, "TypeError": {"typeerror", "cannot read property", "is not a function"}, "ReferenceError": {"referenceerror", "is not defined"}, "SyntaxError": {"syntaxerror", "unexpected token", "unexpected end"}, "NetworkError": {"fetcherror", "enotfound", "connection", "network"}, "CompilationError": {"compilation failed", "build failed", "compile error"}, "LintError": {"eslint", "lint error", "tslint"}, "RuntimeError": {"runtime error", "panic", "exception"}, } for errorType, keywords := range errorTypes { for _, keyword := range keywords { if strings.Contains(content, keyword) { return errorType } } } return "Error" } func (p *TimeBasedErrorParser) extractMainMessage(firstLine string) string { // Strip common prefixes cleaned := p.stripLogPrefixes(firstLine) // Limit message length for display if len(cleaned) > 200 { return cleaned[:197] + "..." } return cleaned } func (p *TimeBasedErrorParser) stripLogPrefixes(content string) string { // Remove timestamp patterns like [12:52:32], (12:52:32), 12:52:32 timestampPatterns := []string{ `^\[\d{1,2}:\d{2}:\d{2}\]\s*`, `^\(\d{1,2}:\d{2}:\d{2}\)\s*`, `^\d{1,2}:\d{2}:\d{2}\s+`, `^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s*`, } cleaned := content for _, pattern := range timestampPatterns { re := regexp.MustCompile(pattern) cleaned = re.ReplaceAllString(cleaned, "") } // Remove process name patterns like [dev], (dev), dev: but preserve TypeScript errors processPatterns := []string{ `^\[[\w-]+\]:\s*`, `^\([\w-]+\):\s*`, } // Only apply word: pattern if it's not a TypeScript error if !regexp.MustCompile(`^TS\d+:`).MatchString(cleaned) { processPatterns = append(processPatterns, `^[\w-]+:\s+`) } for _, pattern := range processPatterns { re := regexp.MustCompile(pattern) cleaned = re.ReplaceAllString(cleaned, "") } // Remove ANSI color codes ansiPattern := regexp.MustCompile(`\x1b\[[0-9;]*m`) cleaned = ansiPattern.ReplaceAllString(cleaned, "") return strings.TrimSpace(cleaned) } func (p *TimeBasedErrorParser) determineSeverity(content string) string { content = strings.ToLower(content) if strings.Contains(content, "critical") || strings.Contains(content, "fatal") || strings.Contains(content, "panic") { return "critical" } if strings.Contains(content, "error") || strings.Contains(content, "fail") { return "error" } if strings.Contains(content, "warn") { return "warning" } return "error" // Default for unknown errors } // ConvertToErrorContext converts a cluster to the existing ErrorContext format for compatibility func (cluster *TimeBasedErrorCluster) ToErrorContext() ErrorContext { var rawLines []string var stackLines []string var contextLines []string for _, line := range cluster.Lines { rawLines = append(rawLines, line.Content) // Simple heuristics for stack vs context if strings.Contains(line.Content, " at ") || strings.Contains(line.Content, ".js:") || strings.Contains(line.Content, ".ts:") { stackLines = append(stackLines, line.Content) } else { contextLines = append(contextLines, line.Content) } } return ErrorContext{ ID: cluster.ID, ProcessID: cluster.ProcessID, ProcessName: cluster.ProcessName, Timestamp: cluster.StartTime, Type: cluster.ErrorType, Message: cluster.Message, Stack: stackLines, Context: contextLines, Severity: cluster.Severity, Language: "javascript", // Default for now Raw: rawLines, } }

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/standardbeagle/brummer'

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