Stream logs from syslog
This commit is contained in:
parent
02b93a3ef0
commit
f4481bca62
5 changed files with 287 additions and 5 deletions
|
|
@ -15,16 +15,18 @@ import (
|
||||||
"github.com/nemunaire/repeater/internal/hotspot"
|
"github.com/nemunaire/repeater/internal/hotspot"
|
||||||
"github.com/nemunaire/repeater/internal/logging"
|
"github.com/nemunaire/repeater/internal/logging"
|
||||||
"github.com/nemunaire/repeater/internal/models"
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
|
"github.com/nemunaire/repeater/internal/syslog"
|
||||||
"github.com/nemunaire/repeater/internal/wifi"
|
"github.com/nemunaire/repeater/internal/wifi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App represents the application
|
// App represents the application
|
||||||
type App struct {
|
type App struct {
|
||||||
Status models.SystemStatus
|
Status models.SystemStatus
|
||||||
StatusMutex sync.RWMutex
|
StatusMutex sync.RWMutex
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
Assets embed.FS
|
Assets embed.FS
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
SyslogTailer *syslog.SyslogTailer
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new application instance
|
// New creates a new application instance
|
||||||
|
|
@ -60,6 +62,19 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||||
// Don't fail - polling fallback still works
|
// Don't fail - polling fallback still works
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start syslog tailing if enabled
|
||||||
|
if cfg.SyslogEnabled {
|
||||||
|
a.SyslogTailer = syslog.NewSyslogTailer(
|
||||||
|
cfg.SyslogPath,
|
||||||
|
cfg.SyslogFilter,
|
||||||
|
cfg.SyslogSource,
|
||||||
|
)
|
||||||
|
if err := a.SyslogTailer.Start(); err != nil {
|
||||||
|
log.Printf("Warning: Failed to start syslog tailing: %v", err)
|
||||||
|
// Don't fail - app continues without syslog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start periodic tasks
|
// Start periodic tasks
|
||||||
go a.periodicStatusUpdate()
|
go a.periodicStatusUpdate()
|
||||||
go a.periodicDeviceUpdate()
|
go a.periodicDeviceUpdate()
|
||||||
|
|
@ -78,6 +93,11 @@ func (a *App) Run(addr string) error {
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the application
|
// Shutdown gracefully shuts down the application
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
|
// Stop syslog tailing if running
|
||||||
|
if a.SyslogTailer != nil {
|
||||||
|
a.SyslogTailer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
wifi.StopEventMonitoring()
|
wifi.StopEventMonitoring()
|
||||||
wifi.Close()
|
wifi.Close()
|
||||||
logging.AddLog("Système", "Application arrêtée")
|
logging.AddLog("Système", "Application arrêtée")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ func declareFlags(o *Config) {
|
||||||
flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases")
|
flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases")
|
||||||
flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file")
|
flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file")
|
||||||
flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file")
|
flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file")
|
||||||
|
flag.BoolVar(&o.SyslogEnabled, "syslog-enabled", false, "Enable syslog tailing for iwd messages")
|
||||||
|
flag.StringVar(&o.SyslogPath, "syslog-path", "/var/log/messages", "Path to syslog file")
|
||||||
|
flag.StringVar(&o.SyslogFilter, "syslog-filter", "daemon.info iwd:", "Filter string for syslog lines")
|
||||||
|
flag.StringVar(&o.SyslogSource, "syslog-source", "iwd", "Source name for syslog entries in logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCLI parse the flags and treats extra args as configuration filename.
|
// parseCLI parse the flags and treats extra args as configuration filename.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ type Config struct {
|
||||||
UseARPDiscovery bool
|
UseARPDiscovery bool
|
||||||
DHCPLeasesPath string
|
DHCPLeasesPath string
|
||||||
ARPTablePath string
|
ARPTablePath string
|
||||||
|
SyslogEnabled bool
|
||||||
|
SyslogPath string
|
||||||
|
SyslogFilter string
|
||||||
|
SyslogSource string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsolidateConfig fills an Options struct by reading configuration from
|
// ConsolidateConfig fills an Options struct by reading configuration from
|
||||||
|
|
@ -28,6 +32,10 @@ func ConsolidateConfig() (opts *Config, err error) {
|
||||||
UseARPDiscovery: true,
|
UseARPDiscovery: true,
|
||||||
DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
|
DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
|
||||||
ARPTablePath: "/proc/net/arp",
|
ARPTablePath: "/proc/net/arp",
|
||||||
|
SyslogEnabled: false,
|
||||||
|
SyslogPath: "/var/log/messages",
|
||||||
|
SyslogFilter: "daemon.info iwd:",
|
||||||
|
SyslogSource: "iwd",
|
||||||
}
|
}
|
||||||
|
|
||||||
declareFlags(opts)
|
declareFlags(opts)
|
||||||
|
|
|
||||||
32
internal/syslog/parser.go
Normal file
32
internal/syslog/parser.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package syslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseSyslogLine extracts the message content from a syslog line.
|
||||||
|
// It looks for the daemon prefix in the line and returns the message after it.
|
||||||
|
//
|
||||||
|
// Example input: "Dec 2 02:01:33 tyet daemon.info iwd: Error loading /var/lib/iwd//nemuphone.psk"
|
||||||
|
// Example output: "Error loading /var/lib/iwd//nemuphone.psk", true
|
||||||
|
//
|
||||||
|
// Returns the message and a boolean indicating if the line was successfully parsed.
|
||||||
|
func ParseSyslogLine(line, daemonPrefix string) (string, bool) {
|
||||||
|
// Find the daemon prefix in the line (e.g., "iwd:")
|
||||||
|
idx := strings.Index(line, daemonPrefix)
|
||||||
|
if idx == -1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract everything after the daemon prefix
|
||||||
|
message := line[idx+len(daemonPrefix):]
|
||||||
|
|
||||||
|
// Trim leading/trailing whitespace
|
||||||
|
message = strings.TrimSpace(message)
|
||||||
|
|
||||||
|
if message == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, true
|
||||||
|
}
|
||||||
218
internal/syslog/syslog.go
Normal file
218
internal/syslog/syslog.go
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
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
|
||||||
|
filter 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, filter, source string) *SyslogTailer {
|
||||||
|
return &SyslogTailer{
|
||||||
|
path: path,
|
||||||
|
filter: filter,
|
||||||
|
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 the filter string
|
||||||
|
if !strings.Contains(line, t.filter) {
|
||||||
|
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(t.filter)
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue