package syslog import ( "bufio" "io" "log" "os" "strings" "sync" "time" "github.com/nemunaire/repeater/internal/logging" ) // SyslogTailer tails a syslog file and filters messages to the logging system. type SyslogTailer struct { path string filters []string source string file *os.File done chan struct{} wg sync.WaitGroup mu sync.Mutex running bool } // NewSyslogTailer creates a new syslog tailer. // path: Path to the syslog file (e.g., "/var/log/messages") // filter: Filter string to match in lines (e.g., "daemon.info iwd:") // source: Source name for logging (e.g., "iwd") func NewSyslogTailer(path string, filters []string, source string) *SyslogTailer { return &SyslogTailer{ path: path, filters: filters, source: source, done: make(chan struct{}), } } // Start opens the syslog file and begins tailing it. func (t *SyslogTailer) Start() error { t.mu.Lock() defer t.mu.Unlock() if t.running { return nil } // Try to open the file file, err := os.Open(t.path) if err != nil { // File might not exist yet, we'll retry in the goroutine log.Printf("Warning: Cannot open syslog file %s: %v (will retry)", t.path, err) } else { // Seek to the end to only read new entries _, err = file.Seek(0, io.SeekEnd) if err != nil { file.Close() return err } t.file = file } t.running = true t.wg.Add(1) go t.tail() return nil } // Stop signals the tailer to stop and waits for it to finish. func (t *SyslogTailer) Stop() { t.mu.Lock() if !t.running { t.mu.Unlock() return } t.mu.Unlock() close(t.done) t.wg.Wait() t.mu.Lock() if t.file != nil { t.file.Close() t.file = nil } t.running = false t.mu.Unlock() } // tail is the main loop that reads from the syslog file. func (t *SyslogTailer) tail() { defer t.wg.Done() retryDelay := 1 * time.Second maxRetryDelay := 30 * time.Second for { select { case <-t.done: return default: } // Check if we have a file open t.mu.Lock() file := t.file t.mu.Unlock() if file == nil { // Try to open the file newFile, err := os.Open(t.path) if err != nil { // File doesn't exist or can't be opened, wait and retry select { case <-t.done: return case <-time.After(retryDelay): // Exponential backoff retryDelay *= 2 if retryDelay > maxRetryDelay { retryDelay = maxRetryDelay } continue } } // Seek to the end _, err = newFile.Seek(0, io.SeekEnd) if err != nil { log.Printf("Error seeking syslog file: %v", err) newFile.Close() time.Sleep(retryDelay) continue } t.mu.Lock() t.file = newFile file = newFile t.mu.Unlock() retryDelay = 1 * time.Second log.Printf("Syslog tailer: opened %s", t.path) } // Read lines from the file if err := t.readLines(file); err != nil { if err == io.EOF { // End of file, wait a bit and try again time.Sleep(100 * time.Millisecond) continue } // Other error, close the file and retry log.Printf("Error reading syslog file: %v", err) t.mu.Lock() if t.file != nil { t.file.Close() t.file = nil } t.mu.Unlock() time.Sleep(retryDelay) } } } // readLines reads and processes lines from the file. func (t *SyslogTailer) readLines(file *os.File) error { scanner := bufio.NewScanner(file) // Increase buffer size to handle long log lines const maxCapacity = 512 * 1024 buf := make([]byte, maxCapacity) scanner.Buffer(buf, maxCapacity) for scanner.Scan() { select { case <-t.done: return nil default: } line := scanner.Text() // Check if the line contains any of the filter strings var matchedFilter string for _, filter := range t.filters { if strings.Contains(line, filter) { matchedFilter = filter break } } if matchedFilter == "" { continue } // Parse the syslog line to extract the message // We look for "iwd:" (or whatever comes after the filter) // The filter is "daemon.info iwd:" so we want to extract text after "iwd:" daemonPrefix := extractDaemonPrefix(matchedFilter) message, ok := ParseSyslogLine(line, daemonPrefix) if !ok { // Couldn't parse the line, skip it continue } // Add to logging system logging.AddLog(t.source, message) } return scanner.Err() } // extractDaemonPrefix extracts the daemon prefix from the filter string. // For example, "daemon.info iwd:" returns "iwd:" func extractDaemonPrefix(filter string) string { parts := strings.Fields(filter) if len(parts) > 0 { return parts[len(parts)-1] } return filter }