Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
182 lines
4.2 KiB
Go
182 lines
4.2 KiB
Go
package filter
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/lixenwraith/logwisp/internal/config"
|
|
"github.com/lixenwraith/logwisp/internal/core"
|
|
|
|
"github.com/lixenwraith/log"
|
|
)
|
|
|
|
// Filter applies regex-based filtering to log entries
|
|
type Filter struct {
|
|
config config.FilterConfig
|
|
patterns []*regexp.Regexp
|
|
mu sync.RWMutex
|
|
logger *log.Logger
|
|
|
|
// Statistics
|
|
totalProcessed atomic.Uint64
|
|
totalMatched atomic.Uint64
|
|
totalDropped atomic.Uint64
|
|
}
|
|
|
|
// NewFilter creates a new filter from a configuration
|
|
func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
|
|
if err := config.Settle(&cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f := &Filter{
|
|
config: cfg,
|
|
patterns: make([]*regexp.Regexp, 0, len(cfg.Patterns)),
|
|
logger: logger,
|
|
}
|
|
|
|
for i, pattern := range cfg.Patterns {
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pattern[%d] '%s': %w", i, pattern, err)
|
|
}
|
|
f.patterns = append(f.patterns, re)
|
|
}
|
|
|
|
logger.Debug("msg", "Filter created",
|
|
"component", "filter",
|
|
"type", cfg.Type,
|
|
"logic", cfg.Logic,
|
|
"pattern_count", len(cfg.Patterns))
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// Apply determines if a log entry should be passed through based on the filter's rules
|
|
func (f *Filter) Apply(entry core.LogEntry) bool {
|
|
f.totalProcessed.Add(1)
|
|
|
|
// Check against all fields that might contain the log content
|
|
text := entry.Message
|
|
if entry.Level != "" {
|
|
text = entry.Level + " " + text
|
|
}
|
|
if entry.Source != "" {
|
|
text = entry.Source + " " + text
|
|
}
|
|
|
|
f.logger.Debug("msg", "Filter checking entry",
|
|
"component", "filter",
|
|
"type", f.config.Type,
|
|
"logic", f.config.Logic,
|
|
"entry_level", entry.Level,
|
|
"entry_source", entry.Source,
|
|
"entry_message", entry.Message[:min(100, len(entry.Message))],
|
|
"text_to_match", text[:min(150, len(text))],
|
|
"patterns", f.config.Patterns)
|
|
|
|
for i, pattern := range f.config.Patterns {
|
|
isMatch := f.patterns[i].MatchString(text)
|
|
f.logger.Debug("msg", "Pattern match result",
|
|
"component", "filter",
|
|
"pattern_index", i,
|
|
"pattern", pattern,
|
|
"matched", isMatch)
|
|
}
|
|
|
|
matched := f.matches(text)
|
|
if matched {
|
|
f.totalMatched.Add(1)
|
|
}
|
|
f.logger.Debug("msg", "Filter final match result",
|
|
"component", "filter",
|
|
"matched", matched)
|
|
|
|
shouldPass := false
|
|
switch f.config.Type {
|
|
case config.FilterTypeInclude:
|
|
shouldPass = matched
|
|
case config.FilterTypeExclude:
|
|
shouldPass = !matched
|
|
}
|
|
|
|
f.logger.Debug("msg", "Filter decision",
|
|
"component", "filter",
|
|
"type", f.config.Type,
|
|
"matched", matched,
|
|
"should_pass", shouldPass)
|
|
|
|
if !shouldPass {
|
|
f.totalDropped.Add(1)
|
|
}
|
|
|
|
return shouldPass
|
|
}
|
|
|
|
// GetStats returns the filter's current statistics
|
|
func (f *Filter) GetStats() map[string]any {
|
|
return map[string]any{
|
|
"type": f.config.Type,
|
|
"logic": f.config.Logic,
|
|
"pattern_count": len(f.patterns),
|
|
"total_processed": f.totalProcessed.Load(),
|
|
"total_matched": f.totalMatched.Load(),
|
|
"total_dropped": f.totalDropped.Load(),
|
|
}
|
|
}
|
|
|
|
// UpdatePatterns replaces the patterns under f.mu, which Apply does not take:
|
|
// it is safe only before entries flow
|
|
func (f *Filter) UpdatePatterns(patterns []string) error {
|
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
|
|
|
// Compiled before the swap, so a bad pattern leaves the filter as it was
|
|
for i, pattern := range patterns {
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid regex pattern[%d] '%s': %w", i, pattern, err)
|
|
}
|
|
compiled = append(compiled, re)
|
|
}
|
|
|
|
f.mu.Lock()
|
|
f.patterns = compiled
|
|
f.config.Patterns = patterns
|
|
f.mu.Unlock()
|
|
|
|
f.logger.Info("msg", "Filter patterns updated",
|
|
"component", "filter",
|
|
"pattern_count", len(patterns))
|
|
return nil
|
|
}
|
|
|
|
// matches checks if the given text matches the filter's patterns according to its logic
|
|
func (f *Filter) matches(text string) bool {
|
|
switch f.config.Logic {
|
|
case config.FilterLogicOr:
|
|
for _, re := range f.patterns {
|
|
if re.MatchString(text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
|
|
case config.FilterLogicAnd:
|
|
for _, re := range f.patterns {
|
|
if !re.MatchString(text) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
|
|
default:
|
|
// Shouldn't happen after validation
|
|
f.logger.Warn("msg", "Unknown filter logic",
|
|
"component", "filter",
|
|
"logic", f.config.Logic)
|
|
return false
|
|
}
|
|
}
|