package main import ( "context" "fmt" "log/slog" "regexp" "strings" "watch-tool/models" "watch-tool/parser" "watch-tool/patterns" "codeberg.org/pata1704/drain3" "github.com/hpcloud/tail" ) type FileMonitor struct { config ToolConfig parser parser.Parser hostname string } func NewFileMonitor(config ToolConfig, hostname string, drainCfg *drain3.Config, stateDir string) *FileMonitor { var logParser parser.Parser pCfg := parser.ParserConfig{ ServiceName: config.Name, LogType: "custom", Hostname: hostname, DrainConfig: drainCfg, StateDir: stateDir, } if config.Format.Pattern != "" { compiledRegex, err := regexp.Compile(config.Format.Pattern) if err != nil { slog.Error("Invalid regex pattern in tool config", "tool", config.Name, "error", err) logParser = parser.NewGenericParser(config.Name, hostname, pCfg.DrainConfig, pCfg.StateDir) } else { gp := parser.NewGenericParser(config.Name, hostname, pCfg.DrainConfig, pCfg.StateDir) customExtractor := patterns.CompiledExtractor{ Name: "config_custom_pattern", Pattern: compiledRegex, Fields: config.Format.Fields, } gp.Extractors = append(gp.Extractors, customExtractor) logParser = gp } } else { var err error logParser, err = parser.New(pCfg) if err != nil { slog.Error("Cannot get tool specific parser from factory", "error", err) logParser = parser.NewGenericParser(config.Name, hostname, pCfg.DrainConfig, pCfg.StateDir) } } return &FileMonitor{ config: config, parser: logParser, hostname: hostname, } } func (fm *FileMonitor) Start(ctx context.Context, out chan<- models.LogMessage) error { defer fm.parser.Close() t, err := tail.TailFile(fm.config.LogFile, tail.Config{ Follow: true, ReOpen: true, MustExist: false, Poll: true, Location: &tail.SeekInfo{Offset: 0, Whence: 2}, }) if err != nil { return fmt.Errorf("tail.TailFile: %w", err) } defer t.Stop() slog.Debug("Started tailing file", "file", fm.config.LogFile, "tool", fm.config.Name) for { select { case <-ctx.Done(): slog.Debug("File monitor stopped", "tool", fm.config.Name) return nil case line, ok := <-t.Lines: if !ok { return nil } if line.Err != nil { slog.Error("Error reading log file", "tool", fm.config.Name, "error", line.Err) continue } if strings.TrimSpace(line.Text) == "" { continue } entry, err := fm.parser.Parse(line.Text) if err != nil { slog.Error("Error parsing log line", "tool", fm.config.Name, "error", err) } else { if entry.Tool == "" { entry.Tool = fm.config.Name } } select { case out <- entry: case <-ctx.Done(): return nil default: slog.Warn("Log-Channel is full, entry dropped", "tool", fm.config.Name) } } } }