Files
logwisp/src/internal/monitor/monitor.go

380 lines
7.6 KiB
Go

// FILE: src/internal/monitor/monitor.go
package monitor
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sync"
"sync/atomic"
"time"
"github.com/lixenwraith/log"
)
type LogEntry struct {
Time time.Time `json:"time"`
Source string `json:"source"`
Level string `json:"level,omitempty"`
Message string `json:"message"`
Fields json.RawMessage `json:"fields,omitempty"`
}
type Monitor interface {
Start(ctx context.Context) error
Stop()
Subscribe() chan LogEntry
AddTarget(path, pattern string, isFile bool) error
RemoveTarget(path string) error
SetCheckInterval(interval time.Duration)
GetStats() Stats
GetActiveWatchers() []WatcherInfo
}
type Stats struct {
ActiveWatchers int
TotalEntries uint64
DroppedEntries uint64
StartTime time.Time
LastEntryTime time.Time
}
type WatcherInfo struct {
Path string
Size int64
Position int64
ModTime time.Time
EntriesRead uint64
LastReadTime time.Time
Rotations int
}
type monitor struct {
subscribers []chan LogEntry
targets []target
watchers map[string]*fileWatcher
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
checkInterval time.Duration
totalEntries atomic.Uint64
droppedEntries atomic.Uint64
startTime time.Time
lastEntryTime atomic.Value // time.Time
logger *log.Logger
}
type target struct {
path string
pattern string
isFile bool
regex *regexp.Regexp
}
func New(logger *log.Logger) Monitor {
m := &monitor{
watchers: make(map[string]*fileWatcher),
checkInterval: 100 * time.Millisecond,
startTime: time.Now(),
logger: logger,
}
m.lastEntryTime.Store(time.Time{})
return m
}
func (m *monitor) Subscribe() chan LogEntry {
m.mu.Lock()
defer m.mu.Unlock()
ch := make(chan LogEntry, 1000)
m.subscribers = append(m.subscribers, ch)
return ch
}
func (m *monitor) publish(entry LogEntry) {
m.mu.RLock()
defer m.mu.RUnlock()
m.totalEntries.Add(1)
m.lastEntryTime.Store(entry.Time)
for _, ch := range m.subscribers {
select {
case ch <- entry:
default:
m.droppedEntries.Add(1)
m.logger.Debug("msg", "Dropped log entry - subscriber buffer full")
}
}
}
func (m *monitor) SetCheckInterval(interval time.Duration) {
m.mu.Lock()
m.checkInterval = interval
m.mu.Unlock()
m.logger.Debug("msg", "Check interval updated", "interval_ms", interval.Milliseconds())
}
func (m *monitor) AddTarget(path, pattern string, isFile bool) error {
absPath, err := filepath.Abs(path)
if err != nil {
m.logger.Error("msg", "Failed to resolve absolute path",
"component", "monitor",
"path", path,
"error", err)
return fmt.Errorf("invalid path %s: %w", path, err)
}
var compiledRegex *regexp.Regexp
if !isFile && pattern != "" {
regexPattern := globToRegex(pattern)
compiledRegex, err = regexp.Compile(regexPattern)
if err != nil {
m.logger.Error("msg", "Failed to compile pattern regex",
"component", "monitor",
"pattern", pattern,
"regex", regexPattern,
"error", err)
return fmt.Errorf("invalid pattern %s: %w", pattern, err)
}
}
m.mu.Lock()
m.targets = append(m.targets, target{
path: absPath,
pattern: pattern,
isFile: isFile,
regex: compiledRegex,
})
m.mu.Unlock()
m.logger.Info("msg", "Added monitor target",
"component", "monitor",
"path", absPath,
"pattern", pattern,
"is_file", isFile)
return nil
}
func (m *monitor) RemoveTarget(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("invalid path %s: %w", path, err)
}
m.mu.Lock()
defer m.mu.Unlock()
// Remove from targets
newTargets := make([]target, 0, len(m.targets))
for _, t := range m.targets {
if t.path != absPath {
newTargets = append(newTargets, t)
}
}
m.targets = newTargets
// Stop any watchers for this path
if w, exists := m.watchers[absPath]; exists {
w.stop()
delete(m.watchers, absPath)
m.logger.Info("msg", "Monitor started",
"component", "monitor",
"check_interval_ms", m.checkInterval.Milliseconds())
}
return nil
}
func (m *monitor) Start(ctx context.Context) error {
m.ctx, m.cancel = context.WithCancel(ctx)
m.wg.Add(1)
go m.monitorLoop()
m.logger.Info("msg", "Monitor started", "check_interval_ms", m.checkInterval.Milliseconds())
return nil
}
func (m *monitor) Stop() {
if m.cancel != nil {
m.cancel()
}
m.wg.Wait()
m.mu.Lock()
for _, w := range m.watchers {
w.close()
}
for _, ch := range m.subscribers {
close(ch)
}
m.mu.Unlock()
m.logger.Info("msg", "Monitor stopped")
}
func (m *monitor) GetStats() Stats {
lastEntry, _ := m.lastEntryTime.Load().(time.Time)
m.mu.RLock()
watcherCount := len(m.watchers)
m.mu.RUnlock()
return Stats{
ActiveWatchers: watcherCount,
TotalEntries: m.totalEntries.Load(),
DroppedEntries: m.droppedEntries.Load(),
StartTime: m.startTime,
LastEntryTime: lastEntry,
}
}
func (m *monitor) GetActiveWatchers() []WatcherInfo {
m.mu.RLock()
defer m.mu.RUnlock()
info := make([]WatcherInfo, 0, len(m.watchers))
for _, w := range m.watchers {
info = append(info, w.getInfo())
}
return info
}
func (m *monitor) monitorLoop() {
defer m.wg.Done()
m.checkTargets()
m.mu.RLock()
interval := m.checkInterval
m.mu.RUnlock()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
m.checkTargets()
m.mu.RLock()
newInterval := m.checkInterval
m.mu.RUnlock()
if newInterval != interval {
ticker.Stop()
ticker = time.NewTicker(newInterval)
interval = newInterval
}
}
}
}
func (m *monitor) checkTargets() {
m.mu.RLock()
targets := make([]target, len(m.targets))
copy(targets, m.targets)
m.mu.RUnlock()
for _, t := range targets {
if t.isFile {
m.ensureWatcher(t.path)
} else {
// Directory scanning for pattern matching
files, err := m.scanDirectory(t.path, t.regex)
if err != nil {
m.logger.Warn("msg", "Failed to scan directory",
"component", "monitor",
"path", t.path,
"pattern", t.pattern,
"error", err)
continue
}
for _, file := range files {
m.ensureWatcher(file)
}
}
}
m.cleanupWatchers()
}
func (m *monitor) scanDirectory(dir string, pattern *regexp.Regexp) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var files []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if pattern == nil || pattern.MatchString(name) {
files = append(files, filepath.Join(dir, name))
}
}
return files, nil
}
func (m *monitor) ensureWatcher(path string) {
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.watchers[path]; exists {
return
}
w := newFileWatcher(path, m.publish, m.logger)
m.watchers[path] = w
m.logger.Debug("msg", "Created watcher", "path", path)
m.wg.Add(1)
go func() {
defer m.wg.Done()
if err := w.watch(m.ctx); err != nil {
// Log based on error type
if errors.Is(err, context.Canceled) {
m.logger.Debug("msg", "Watcher cancelled",
"component", "monitor",
"path", path)
} else {
m.logger.Error("msg", "Watcher failed",
"component", "monitor",
"path", path,
"error", err)
}
}
m.mu.Lock()
delete(m.watchers, path)
m.mu.Unlock()
}()
}
func (m *monitor) cleanupWatchers() {
m.mu.Lock()
defer m.mu.Unlock()
for path, w := range m.watchers {
if _, err := os.Stat(path); os.IsNotExist(err) {
w.stop()
delete(m.watchers, path)
m.logger.Debug("msg", "Cleaned up watcher for non-existent file", "path", path)
}
}
}