package parser import ( "fmt" "log/slog" "os" "path/filepath" "strconv" "strings" "time" "watch-tool/models" "watch-tool/patterns" "codeberg.org/pata1704/drain3" ) type GenericParser struct { ServiceName string Hostname string Extractors []patterns.CompiledExtractor CommonExt []patterns.CompiledExtractor drainMiner *drain3.TemplateMiner drainEnabled bool drainStatePath string } func NewGenericParser(serviceName, hostname string, drainCfg *drain3.Config, stateDir string) *GenericParser { repo := patterns.GetInstance() var svcExt, commonExt []patterns.CompiledExtractor if repo != nil { svcExt = repo.GetExtractors(serviceName) commonExt = repo.GetExtractors("common") } else { slog.Error("CRITICAL: Pattern Repository is nil. Parser will not work correctly.") } parser := &GenericParser{ ServiceName: serviceName, Hostname: hostname, Extractors: svcExt, CommonExt: commonExt, } if drainCfg != nil && stateDir != "" { parser.drainEnabled = true parser.drainStatePath = filepath.Join(stateDir, serviceName+".bin") if err := os.MkdirAll(stateDir, 0755); err != nil { slog.Error("Failed to create drain3 state dir", "error", err) parser.drainEnabled = false return parser } persister := drain3.NewFilePersistence(parser.drainStatePath, false) state, err := persister.LoadState() if err == nil && state != nil { parser.drainMiner = drain3.NewTemplateMiner(drainCfg, persister) slog.Info("Drain3 state loaded", "service", serviceName, "clusters", len(state.Clusters)) } else { parser.drainMiner = drain3.NewTemplateMiner(drainCfg, persister) slog.Info("Drain3 initialized fresh", "service", serviceName) } } return parser } func (p *GenericParser) Parse(line string) (models.LogMessage, error) { entry := models.LogMessage{ Service: p.ServiceName, Host: p.Hostname, Timestamp: time.Now(), Raw: line, Fields: make(map[string]any), Type: "log_entry", } trimmedLine := strings.TrimSpace(line) if trimmedLine == "" { return entry, nil } if p.drainEnabled && p.drainMiner != nil { cluster := p.drainMiner.AddLogMessage(trimmedLine) if cluster != nil { entry.Fields["drain_template_id"] = cluster.ClusterID entry.Fields["drain_template"] = cluster.TemplateMined // Optional: Parameter extrahieren, die Drain gefunden hat (Wildcards) } } allExtractors := append(p.CommonExt, p.Extractors...) matchedAny := false for _, ext := range allExtractors { matches := ext.Pattern.FindStringSubmatch(trimmedLine) if matches == nil { continue } matchedAny = true subexpNames := ext.Pattern.SubexpNames() for i, matchValue := range matches { if i == 0 { continue } groupName := subexpNames[i] if groupName == "" { continue } cleanValue := strings.TrimSpace(matchValue) targetType := ext.Fields[groupName] parsedValue := p.safeConvert(cleanValue, targetType) p.mapField(&entry, groupName, parsedValue) } } if !matchedAny { entry.LogMessage = trimmedLine entry.Fields["_parse_status"] = "failed" } else if entry.LogMessage == "" { entry.LogMessage = trimmedLine } return entry, nil } func (p *GenericParser) Close() error { if p.drainEnabled && p.drainMiner != nil { if err := p.drainMiner.SaveState("shutdown"); err != nil { slog.Error("Failed to save drain3 state", "service", p.ServiceName, "error", err) return err } slog.Debug("Drain3 state saved", "service", p.ServiceName) } return nil } func (p *GenericParser) safeConvert(value, typeDef string) any { if value == "" || value == "-" { if strings.HasPrefix(typeDef, "int") || strings.HasPrefix(typeDef, "float") { return 0 } return value } var err error var result any switch { case strings.HasPrefix(typeDef, "int"): var i int i, err = strconv.Atoi(value) result = i case strings.HasPrefix(typeDef, "float"): var f float64 f, err = strconv.ParseFloat(value, 64) result = f case strings.HasPrefix(typeDef, "time:"): layout := strings.TrimPrefix(typeDef, "time:") result, err = p.parseTimeRobust(value, layout) case typeDef == "bool": var b bool b, err = strconv.ParseBool(value) result = b default: return value } if err != nil { return value } return result } func (p *GenericParser) parseTimeRobust(value, layout string) (time.Time, error) { if layout == "Jan 02 15:04:05" { t, err := time.Parse(layout, value) if err != nil { return time.Time{}, err } now := time.Now() year := now.Year() if t.Month() > now.Month() { year-- } return t.AddDate(year, 0, 0), nil } return time.Parse(layout, value) } func (p *GenericParser) mapField(entry *models.LogMessage, key string, value any) { switch key { case "timestamp", "time": if t, ok := value.(time.Time); ok { entry.Timestamp = t } case "log_level", "level": entry.LogLevel = fmt.Sprintf("%v", value) case "message", "msg": entry.LogMessage = fmt.Sprintf("%v", value) case "host", "hostname": entry.Host = fmt.Sprintf("%v", value) case "service": entry.Service = fmt.Sprintf("%v", value) case "pid": if v, ok := value.(int); ok { entry.PID = v } else if vStr, ok := value.(string); ok { if pid, err := strconv.Atoi(vStr); err == nil { entry.PID = pid } } default: entry.Fields[key] = value } }