package main import ( "context" "fmt" "log/slog" "regexp" "strings" "tixel_watch/models" "tixel_watch/parser" "github.com/hpcloud/tail" ) type FileMonitor struct { config ToolConfig parser parser.Parser } func NewFileMonitor(config ToolConfig) *FileMonitor { var logParser parser.Parser if config.Format.Pattern != "" { pattern, err := regexp.Compile(config.Format.Pattern) if err != nil { slog.Error("invalid regex pattern", "tool", config.Name, "error", err) logParser = &parser.DefaultParser{} } else { logParser = &parser.RegexLogParser{ Pattern: pattern, Fields: config.Format.Fields, Toolname: config.Name, } } } else { var err error logParser, err = parser.New(config.Name, "custom") if err != nil { slog.Error("cannot get tool specific parser", "error", err) } } return &FileMonitor{ config: config, parser: logParser, } } func (fm *FileMonitor) Start(ctx context.Context, out chan<- models.LogMessage) error { 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", "error", err) } select { case out <- entry: case <-ctx.Done(): return nil default: slog.Warn("Log-Channel is full, entry dropped", "tool", fm.config.Name) } } } }