repeater/internal/syslog/syslog.go

225 lines
4.5 KiB
Go

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
}