From 7f9be48800ec751104d70f89910641774ca5b0f0 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Wed, 11 Mar 2026 11:24:51 +0000 Subject: [PATCH] fix(backend): refactor filters --- cmd/karma/alerts.go | 12 +- cmd/karma/views.go | 20 +- internal/alertmanager/dedup.go | 45 ++-- internal/alertmanager/dedup_internal_test.go | 81 +++++++ internal/alertmanager/models.go | 12 +- internal/alertmanager/upstream.go | 4 +- internal/filters/filter.go | 121 +++++----- internal/filters/filter_age.go | 73 +++---- internal/filters/filter_alertmanager.go | 59 +++-- internal/filters/filter_cluster.go | 59 +++-- internal/filters/filter_fingerprint.go | 61 +++--- internal/filters/filter_fuzzy.go | 90 ++++---- internal/filters/filter_inhibited.go | 75 ++++--- internal/filters/filter_inhibited_by.go | 76 +++---- internal/filters/filter_invalid.go | 10 - internal/filters/filter_label.go | 47 ++-- internal/filters/filter_limit.go | 59 +++-- internal/filters/filter_receiver.go | 47 ++-- internal/filters/filter_silence_author.go | 86 ++++---- internal/filters/filter_silence_ticket.go | 86 ++++---- internal/filters/filter_silenced_by.go | 76 +++---- internal/filters/filter_state.go | 65 +++--- internal/filters/filter_test.go | 84 +++---- internal/filters/invalid_match_test.go | 50 ++--- internal/filters/matcher.go | 170 +++++--------- internal/filters/matcher_test.go | 219 ++++++++----------- internal/filters/registry.go | 16 +- 27 files changed, 801 insertions(+), 1002 deletions(-) create mode 100644 internal/alertmanager/dedup_internal_test.go delete mode 100644 internal/filters/filter_invalid.go diff --git a/cmd/karma/alerts.go b/cmd/karma/alerts.go index 2ff9618b7..dce940b5f 100644 --- a/cmd/karma/alerts.go +++ b/cmd/karma/alerts.go @@ -18,8 +18,8 @@ import ( "github.com/prymitive/karma/internal/uri" ) -func getFiltersFromQuery(filterStrings []string) []filters.FilterT { - matchFilters := make([]filters.FilterT, 0, len(filterStrings)) +func getFiltersFromQuery(filterStrings []string) []filters.Filter { + matchFilters := make([]filters.Filter, 0, len(filterStrings)) for _, filterExpression := range filterStrings { f := filters.NewFilter(filterExpression) matchFilters = append(matchFilters, f) @@ -324,11 +324,11 @@ func autoGridLabel(dedupedAlerts []models.AlertGroup) string { return lastLabel } -func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filteredAlerts []models.AlertGroup) { +func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.Filter) (filteredAlerts []models.AlertGroup) { var matches int var hasAMFilters bool for _, filter := range fl { - if filter.GetIsValid() && filter.GetIsAlertmanagerFilter() { + if filter.Valid() && filter.IsAlertmanagerFilter() { hasAMFilters = true break } @@ -350,7 +350,7 @@ func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filt for _, alert := range ag.Alerts { var hadMismatch bool for _, filter := range fl { - if filter.GetIsValid() { + if filter.Valid() { if !filter.Match(&alert, matches) { hadMismatch = true } @@ -364,7 +364,7 @@ func filterAlerts(dedupedAlerts []models.AlertGroup, fl []filters.FilterT) (filt clear(blockedAMs) for _, am := range alert.Alertmanager { for _, filter := range fl { - if filter.GetIsValid() && filter.GetIsAlertmanagerFilter() && !filter.MatchAlertmanager(&am) { + if filter.Valid() && filter.IsAlertmanagerFilter() && !filter.MatchAlertmanager(&am) { blockedAMs[am.Name] = struct{}{} } } diff --git a/cmd/karma/views.go b/cmd/karma/views.go index d60ec297d..9cdf787d3 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -91,16 +91,16 @@ func index(w http.ResponseWriter, _ *http.Request) { }) } -func populateAPIFilters(matchFilters []filters.FilterT) []models.Filter { +func populateAPIFilters(matchFilters []filters.Filter) []models.Filter { apiFilters := []models.Filter{} for _, filter := range matchFilters { af := models.Filter{ - Text: filter.GetRawText(), - Name: filter.GetName(), - Matcher: filter.GetMatcher(), - Value: filter.GetValue(), - Hits: filter.GetHits(), - IsValid: filter.GetIsValid(), + Text: filter.RawText(), + Name: filter.Name(), + Matcher: filter.MatcherOperation(), + Value: filter.Value(), + Hits: filter.Hits(), + IsValid: filter.Valid(), } if af.Text != "" { apiFilters = append(apiFilters, af) @@ -426,9 +426,9 @@ func alerts(w http.ResponseWriter, r *http.Request) { } for _, filter := range matchFilters { - if filter.GetValue() != "" && filter.GetMatcher() == "=" { - transform.ColorLabel(colors, filter.GetName(), filter.GetValue()) - labelSettings(filter.GetName(), resp.Settings.Labels) + if filter.Value() != "" && filter.MatcherOperation() == "=" { + transform.ColorLabel(colors, filter.Name(), filter.Value()) + labelSettings(filter.Name(), resp.Settings.Labels) } } diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index eec5eb229..a9c0535f3 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -193,35 +193,34 @@ func DedupColors() models.LabelsColorMap { // DedupAutocomplete returns a list of autocomplete hints merged from all // Alertmanager upstreams func DedupAutocomplete() []models.Autocomplete { - uniqueAutocomplete := map[string]*models.Autocomplete{} - upstreams := GetAlertmanagers() + var result []models.Autocomplete + index := map[string]int{} + for _, am := range upstreams { - ac := am.Autocomplete() - for _, hint := range ac { - h, found := uniqueAutocomplete[hint.Value] - if found { - for _, token := range hint.Tokens { - if !slices.Contains(h.Tokens, token) { - h.Tokens = append(h.Tokens, token) - } - } - } else { - uniqueAutocomplete[hint.Value] = &models.Autocomplete{ - Value: hint.Value, - Tokens: hint.Tokens, - } + am.ForEachAutocomplete(func(hint models.Autocomplete) { + mergeAutocompleteHint(&result, index, hint) + }) + } + + return result +} + +func mergeAutocompleteHint(result *[]models.Autocomplete, index map[string]int, hint models.Autocomplete) { + if idx, found := index[hint.Value]; found { + for _, token := range hint.Tokens { + if !slices.Contains((*result)[idx].Tokens, token) { + (*result)[idx].Tokens = append((*result)[idx].Tokens, token) } } + } else { + index[hint.Value] = len(*result) + *result = append(*result, models.Autocomplete{ + Value: hint.Value, + Tokens: hint.Tokens, + }) } - - dedupedAutocomplete := make([]models.Autocomplete, 0, len(uniqueAutocomplete)) - for _, hint := range uniqueAutocomplete { - dedupedAutocomplete = append(dedupedAutocomplete, *hint) - } - - return dedupedAutocomplete } // DedupKnownLabels returns a deduplicated slice of all known label names diff --git a/internal/alertmanager/dedup_internal_test.go b/internal/alertmanager/dedup_internal_test.go new file mode 100644 index 000000000..bad2d0478 --- /dev/null +++ b/internal/alertmanager/dedup_internal_test.go @@ -0,0 +1,81 @@ +package alertmanager + +import ( + "testing" + + "github.com/prymitive/karma/internal/models" +) + +func TestMergeAutocompleteHintNewEntry(t *testing.T) { + // verifies that a new hint is appended to the result slice + var result []models.Autocomplete + index := map[string]int{} + + mergeAutocompleteHint(&result, index, models.Autocomplete{ + Value: "foo=bar", + Tokens: []string{"foo", "foo=bar"}, + }) + + if len(result) != 1 { + t.Fatalf("expected 1 result, got %d", len(result)) + } + if result[0].Value != "foo=bar" { + t.Errorf("expected Value %q, got %q", "foo=bar", result[0].Value) + } + if len(result[0].Tokens) != 2 { + t.Errorf("expected 2 tokens, got %d", len(result[0].Tokens)) + } + if idx, ok := index["foo=bar"]; !ok || idx != 0 { + t.Errorf("expected index[\"foo=bar\"] == 0, got %d (ok=%v)", idx, ok) + } +} + +func TestMergeAutocompleteHintDuplicateTokens(t *testing.T) { + // verifies that duplicate tokens from a second upstream are not appended + var result []models.Autocomplete + index := map[string]int{} + + mergeAutocompleteHint(&result, index, models.Autocomplete{ + Value: "foo=bar", + Tokens: []string{"foo", "foo=bar"}, + }) + mergeAutocompleteHint(&result, index, models.Autocomplete{ + Value: "foo=bar", + Tokens: []string{"foo", "foo=bar"}, + }) + + if len(result) != 1 { + t.Fatalf("expected 1 result after merge, got %d", len(result)) + } + if len(result[0].Tokens) != 2 { + t.Errorf("expected 2 tokens after merge with identical tokens, got %d", len(result[0].Tokens)) + } +} + +func TestMergeAutocompleteHintNewTokens(t *testing.T) { + // verifies that new tokens from a second upstream are appended + var result []models.Autocomplete + index := map[string]int{} + + mergeAutocompleteHint(&result, index, models.Autocomplete{ + Value: "foo=bar", + Tokens: []string{"foo", "foo=bar"}, + }) + mergeAutocompleteHint(&result, index, models.Autocomplete{ + Value: "foo=bar", + Tokens: []string{"extra_token"}, + }) + + if len(result) != 1 { + t.Fatalf("expected 1 result after merge, got %d", len(result)) + } + expected := []string{"foo", "foo=bar", "extra_token"} + if len(result[0].Tokens) != len(expected) { + t.Fatalf("expected %d tokens, got %d: %v", len(expected), len(result[0].Tokens), result[0].Tokens) + } + for i, tok := range expected { + if result[0].Tokens[i] != tok { + t.Errorf("token[%d] = %q, want %q", i, result[0].Tokens[i], tok) + } + } +} diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 29ad84db3..88fdb1c5f 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -34,7 +34,7 @@ type alertmanagerMetrics struct { } type HealthCheck struct { - filters []filters.FilterT + filters []filters.Filter wasFound bool } @@ -459,14 +459,14 @@ func (am *Alertmanager) Colors() models.LabelsColorMap { return colors } -// Autocomplete returns a copy of all autocomplete data -func (am *Alertmanager) Autocomplete() []models.Autocomplete { +// ForEachAutocomplete calls fn for each autocomplete hint while holding a read lock. +func (am *Alertmanager) ForEachAutocomplete(fn func(models.Autocomplete)) { am.lock.RLock() defer am.lock.RUnlock() - autocomplete := make([]models.Autocomplete, len(am.autocomplete)) - copy(autocomplete, am.autocomplete) - return autocomplete + for _, hint := range am.autocomplete { + fn(hint) + } } // KnownLabels returns a copy of a map with known labels diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index 803449026..d6d686f5f 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -180,11 +180,11 @@ func WithHealthchecks(val map[string][]string) Option { healthchecks := map[string]HealthCheck{} for name, filterExpressions := range val { hc := HealthCheck{ - filters: []filters.FilterT{}, + filters: []filters.Filter{}, } for _, filterExpression := range filterExpressions { f := filters.NewFilter(filterExpression) - if f == nil || !f.GetIsValid() { + if f == nil || !f.Valid() { return fmt.Errorf("%q is not a valid filter", filterExpression) } hc.filters = append(hc.filters, f) diff --git a/internal/filters/filter.go b/internal/filters/filter.go index e045f80b3..bbce708e2 100644 --- a/internal/filters/filter.go +++ b/internal/filters/filter.go @@ -9,70 +9,70 @@ import ( "github.com/prymitive/karma/internal/models" ) -// FilterT provides methods for interacting with alert filters -type FilterT interface { - init(name string, matcher *matcherT, rawText string, isValid bool, value string) +// Filter provides methods for interacting with alert filters. +type Filter interface { Match(alert *models.Alert, matches int) bool MatchAlertmanager(am *models.AlertmanagerInstance) bool - GetRawText() string - GetHits() int - GetIsValid() bool - GetName() string - GetMatcher() string - GetValue() string - GetIsAlertmanagerFilter() bool + RawText() string + Hits() int + Valid() bool + Name() string + MatcherOperation() string + Value() string + IsAlertmanagerFilter() bool } -type alertFilter struct { - FilterT - Matcher matcherT - Matched string - RawText string - Hits int - IsValid bool - IsAlertmanagerFilter bool +// filterBase holds common state shared by all filter implementations. +// Concrete filters embed this struct and override Match and optionally +// MatchAlertmanager and Value. +type filterBase struct { + matcher Matcher + name string + rawText string + value string + hits int + isValid bool + isAlertmanagerFilter bool } -func (filter *alertFilter) GetRawText() string { - return filter.RawText -} +func (f *filterBase) RawText() string { return f.rawText } +func (f *filterBase) Hits() int { return f.hits } +func (f *filterBase) Valid() bool { return f.isValid } +func (f *filterBase) Name() string { return f.name } +func (f *filterBase) Value() string { return f.value } +func (f *filterBase) IsAlertmanagerFilter() bool { return f.isAlertmanagerFilter } +func (f *filterBase) MatcherOperation() string { return f.matcher.Operator } -func (filter *alertFilter) GetHits() int { - return filter.Hits -} +func (f *filterBase) Match(*models.Alert, int) bool { return false } +func (f *filterBase) MatchAlertmanager(*models.AlertmanagerInstance) bool { return false } -func (filter *alertFilter) GetIsValid() bool { - return filter.IsValid -} - -func (filter *alertFilter) GetName() string { - return filter.Matched -} - -func (filter *alertFilter) GetMatcher() string { - if filter.Matcher == nil { - return "" +// buildMatcher creates a Matcher for the given operator and value. +// For regex operators it compiles the pattern; for others it delegates to newMatcher. +func buildMatcher(operator, value string) (Matcher, bool) { + switch operator { + case regexpOperator, negativeRegexOperator: + m, err := newRegexpMatcher(operator, value) + if err != nil { + return Matcher{}, false + } + return m, true + default: + m, err := newMatcher(operator) + if err != nil { + return Matcher{}, false + } + return m, true } - return filter.Matcher.GetOperator() } -func (filter *alertFilter) GetIsAlertmanagerFilter() bool { - return filter.IsAlertmanagerFilter -} - -type newFilterFactory func() FilterT - -// NewFilter creates new filter object from filter expression like "key=value" -// expression will be parsed and best filter implementation and value matcher -// will be selected -func NewFilter(expression string) FilterT { +// NewFilter creates a new filter from a filter expression like "key=value". +// The expression is parsed and the best filter implementation and value +// matcher are selected. +func NewFilter(expression string) Filter { trimmed := strings.Trim(expression, " \t") - invalid := alwaysInvalidFilter{} - invalid.init("", nil, trimmed, false, trimmed) - if trimmed == "" { - return &invalid + return &filterBase{rawText: trimmed} } reExp := fmt.Sprintf("^(?P(%s))(?P(%s))(?P(.*))", filterRegex, matcherRegex) @@ -90,33 +90,22 @@ func NewFilter(expression string) FilterT { value := result["value"] if matched == "" && operator == "" && value == "" { - // no "filter=" part, just the value, use fuzzy filter - f := newFuzzyFilter() - matcher, _ := newMatcher(regexpOperator) - f.init("", &matcher, trimmed, true, trimmed) - return f + return newFuzzyFilter(trimmed) } if value == "" { - // there's no value, so it's always invalid - return &invalid + return &filterBase{rawText: trimmed} } - // we have "filter=" part, lookup filter that matches for _, fc := range AllFilters { - f := fc.Factory() if !fc.LabelRe.MatchString(matched) { - // filter name doesn't match, keep searching continue } if !slices.Contains(fc.SupportedOperators, operator) { - return &invalid + return &filterBase{rawText: trimmed} } - // we validate operator above, no need to re-check - matcher, _ := newMatcher(operator) - f.init(matched, &matcher, trimmed, true, value) - return f + return fc.Factory(matched, operator, trimmed, value) } - return &invalid + return &filterBase{rawText: trimmed} } diff --git a/internal/filters/filter_age.go b/internal/filters/filter_age.go index 338ac14db..03d7ba62a 100644 --- a/internal/filters/filter_age.go +++ b/internal/filters/filter_age.go @@ -2,6 +2,7 @@ package filters import ( "fmt" + "strconv" "strings" "time" @@ -9,55 +10,51 @@ import ( ) type ageFilter struct { - alertFilter - value time.Duration + filterBase + duration time.Duration } -func (filter *ageFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - - dur, err := time.ParseDuration(value) - if err != nil { - filter.IsValid = false - } - if dur > 0 { - filter.value = -dur - } else { - filter.value = dur - } -} - -func (filter *ageFilter) GetValue() string { - return fmt.Sprintf("%v", filter.value) +func (filter *ageFilter) Value() string { + return fmt.Sprintf("%v", filter.duration) } func (filter *ageFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - ts := time.Now().Add(filter.value) - isMatch := filter.Matcher.Compare(int(ts.Unix()), int(alert.StartsAt.Unix())) - if isMatch { - filter.Hits++ - } - return isMatch + ts := time.Now().Add(filter.duration) + isMatch := filter.matcher.Compare(strconv.Itoa(int(ts.Unix())), strconv.Itoa(int(alert.StartsAt.Unix()))) + if isMatch { + filter.hits++ } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + return isMatch } func (filter *ageFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - ts := time.Now().Add(filter.value) - return filter.Matcher.Compare(int(ts.Unix()), int(am.StartsAt.Unix())) + ts := time.Now().Add(filter.duration) + return filter.matcher.Compare(strconv.Itoa(int(ts.Unix())), strconv.Itoa(int(am.StartsAt.Unix()))) } -func newAgeFilter() FilterT { - f := ageFilter{} - f.IsAlertmanagerFilter = true - return &f +func newAgeFilter(name, operator, rawText, value string) Filter { + dur, err := time.ParseDuration(value) + if err != nil { + return &filterBase{rawText: rawText} + } + if dur > 0 { + dur = -dur + } + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &ageFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + duration: dur, + } } func ageAutocomplete(name string, operators []string, _ []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_alertmanager.go b/internal/filters/filter_alertmanager.go index e1dc6b245..0a11bf123 100644 --- a/internal/filters/filter_alertmanager.go +++ b/internal/filters/filter_alertmanager.go @@ -1,56 +1,47 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type alertmanagerInstanceFilter struct { - value string - alertFilter -} - -func (filter *alertmanagerInstanceFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *alertmanagerInstanceFilter) GetValue() string { - return filter.value + filterBase } func (filter *alertmanagerInstanceFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - if filter.Matcher.Compare(am.Name, filter.value) { - isMatch = true - } + var isMatch bool + for _, am := range alert.Alertmanager { + if filter.matcher.Compare(am.Name, filter.value) { + isMatch = true } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + if isMatch { + filter.hits++ + } + return isMatch } func (filter *alertmanagerInstanceFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return filter.Matcher.Compare(am.Name, filter.value) + return filter.matcher.Compare(am.Name, filter.value) } -func newAlertmanagerInstanceFilter() FilterT { - f := alertmanagerInstanceFilter{} - f.IsAlertmanagerFilter = true - return &f +func newAlertmanagerInstanceFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &alertmanagerInstanceFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func alertmanagerInstanceAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_cluster.go b/internal/filters/filter_cluster.go index 85f2f17c5..283e22bf6 100644 --- a/internal/filters/filter_cluster.go +++ b/internal/filters/filter_cluster.go @@ -1,56 +1,47 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type alertmanagerClusterFilter struct { - value string - alertFilter -} - -func (filter *alertmanagerClusterFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *alertmanagerClusterFilter) GetValue() string { - return filter.value + filterBase } func (filter *alertmanagerClusterFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - if filter.Matcher.Compare(am.Cluster, filter.value) { - isMatch = true - } + var isMatch bool + for _, am := range alert.Alertmanager { + if filter.matcher.Compare(am.Cluster, filter.value) { + isMatch = true } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + if isMatch { + filter.hits++ + } + return isMatch } func (filter *alertmanagerClusterFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return filter.Matcher.Compare(am.Cluster, filter.value) + return filter.matcher.Compare(am.Cluster, filter.value) } -func newAlertmanagerClusterFilter() FilterT { - f := alertmanagerClusterFilter{} - f.IsAlertmanagerFilter = true - return &f +func newAlertmanagerClusterFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &alertmanagerClusterFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func alertmanagerClusterAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_fingerprint.go b/internal/filters/filter_fingerprint.go index f44681101..8b8a579aa 100644 --- a/internal/filters/filter_fingerprint.go +++ b/internal/filters/filter_fingerprint.go @@ -1,54 +1,43 @@ package filters import ( - "fmt" - "github.com/prymitive/karma/internal/models" ) type fingerprintFilter struct { - value string - alertFilter -} - -func (filter *fingerprintFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *fingerprintFilter) GetValue() string { - return filter.value + filterBase } func (filter *fingerprintFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - m := filter.Matcher.Compare(am.Fingerprint, filter.value) - if m { - isMatch = m - } + var isMatch bool + for _, am := range alert.Alertmanager { + if filter.matcher.Compare(am.Fingerprint, filter.value) { + isMatch = true } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + if isMatch { + filter.hits++ + } + return isMatch } func (filter *fingerprintFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return filter.Matcher.Compare(am.Fingerprint, filter.value) + return filter.matcher.Compare(am.Fingerprint, filter.value) } -func newFingerprintFilter() FilterT { - f := fingerprintFilter{} - f.IsAlertmanagerFilter = true - return &f +func newFingerprintFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &fingerprintFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } diff --git a/internal/filters/filter_fuzzy.go b/internal/filters/filter_fuzzy.go index 4738ab2cf..add8fa7b2 100644 --- a/internal/filters/filter_fuzzy.go +++ b/internal/filters/filter_fuzzy.go @@ -10,67 +10,59 @@ import ( ) type fuzzyFilter struct { - value *regexp.Regexp - alertFilter + filterBase + re *regexp.Regexp } -func (filter *fuzzyFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - var err error - if filter.value, err = regexp.Compile("(?i)" + value); err != nil { - filter.IsValid = false - } -} - -func (filter *fuzzyFilter) GetValue() string { - return fmt.Sprintf("%v", filter.value) +func (filter *fuzzyFilter) Value() string { + return fmt.Sprintf("%v", filter.re) } func (filter *fuzzyFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - for _, val := range alert.Annotations { - if filter.value.MatchString(val.Value) { - filter.Hits++ - return true - } - } - - var labelMatch bool - alert.Labels.Range(func(l labels.Label) { - if filter.value.MatchString(l.Value) { - labelMatch = true - } - }) - if labelMatch { - filter.Hits++ + for _, val := range alert.Annotations { + if filter.re.MatchString(val.Value) { + filter.hits++ return true } + } - for _, silenceID := range alert.SilencedBy { - for _, am := range alert.Alertmanager { - silence, found := am.Silences[silenceID] - if found { - if filter.value.MatchString(silence.Comment) { - filter.Hits++ - return true - } + var labelMatch bool + alert.Labels.Range(func(l labels.Label) { + if filter.re.MatchString(l.Value) { + labelMatch = true + } + }) + if labelMatch { + filter.hits++ + return true + } + + for _, silenceID := range alert.SilencedBy { + for _, am := range alert.Alertmanager { + silence, found := am.Silences[silenceID] + if found { + if filter.re.MatchString(silence.Comment) { + filter.hits++ + return true } } } - - return false - } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + + return false } -func newFuzzyFilter() FilterT { - f := fuzzyFilter{} - return &f +func newFuzzyFilter(rawText string) Filter { + re, err := regexp.Compile("(?i)" + rawText) + if err != nil { + return &filterBase{rawText: rawText} + } + return &fuzzyFilter{ + filterBase: filterBase{ + matcher: Matcher{Operator: regexpOperator}, + rawText: rawText, + isValid: true, + }, + re: re, + } } diff --git a/internal/filters/filter_inhibited.go b/internal/filters/filter_inhibited.go index 338218f83..ef8be075b 100644 --- a/internal/filters/filter_inhibited.go +++ b/internal/filters/filter_inhibited.go @@ -1,7 +1,6 @@ package filters import ( - "fmt" "strconv" "strings" @@ -14,55 +13,55 @@ const ( ) type inhibitedFilter struct { - alertFilter - value bool + filterBase + boolValue bool } -func (filter *inhibitedFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - switch value { - case trueValue: - filter.value = true - case falseValue: - filter.value = false - default: - filter.IsValid = false - } -} - -func (filter *inhibitedFilter) GetValue() string { - return strconv.FormatBool(filter.value) +func (filter *inhibitedFilter) Value() string { + return strconv.FormatBool(filter.boolValue) } func (filter *inhibitedFilter) Match(alert *models.Alert, _ int) (isMatch bool) { - if filter.IsValid { - for _, am := range alert.Alertmanager { - if len(am.InhibitedBy) > 0 == filter.value { - isMatch = true - } + for _, am := range alert.Alertmanager { + if len(am.InhibitedBy) > 0 == filter.boolValue { + isMatch = true } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + if isMatch { + filter.hits++ + } + return isMatch } func (filter *inhibitedFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return len(am.InhibitedBy) > 0 == filter.value + return len(am.InhibitedBy) > 0 == filter.boolValue } -func newInhibitedFilter() FilterT { - f := inhibitedFilter{} - f.IsAlertmanagerFilter = true - return &f +func newInhibitedFilter(name, operator, rawText, value string) Filter { + var bv bool + switch value { + case trueValue: + bv = true + case falseValue: + bv = false + default: + return &filterBase{rawText: rawText} + } + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &inhibitedFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + boolValue: bv, + } } func inhibitedAutocomplete(name string, _ []string, _ []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_inhibited_by.go b/internal/filters/filter_inhibited_by.go index a419ede1c..b897bdc65 100644 --- a/internal/filters/filter_inhibited_by.go +++ b/internal/filters/filter_inhibited_by.go @@ -1,66 +1,54 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type inhibitedByFilter struct { - value string - alertFilter -} - -func (filter *inhibitedByFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *inhibitedByFilter) GetValue() string { - return filter.value + filterBase } func (filter *inhibitedByFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - for _, silenceID := range am.InhibitedBy { - m := filter.Matcher.Compare(silenceID, filter.value) - if m { - isMatch = m - } + var isMatch bool + for _, am := range alert.Alertmanager { + for _, silenceID := range am.InhibitedBy { + if filter.matcher.Compare(silenceID, filter.value) { + isMatch = true } } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) -} - -func (filter *inhibitedByFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - var isMatch bool - for _, silenceID := range am.InhibitedBy { - m := filter.Matcher.Compare(silenceID, filter.value) - if m { - isMatch = m - } + if isMatch { + filter.hits++ } return isMatch } -func newInhibitedByFilter() FilterT { - f := inhibitedByFilter{} - f.IsAlertmanagerFilter = true - return &f +func (filter *inhibitedByFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + for _, silenceID := range am.InhibitedBy { + if filter.matcher.Compare(silenceID, filter.value) { + return true + } + } + return false +} + +func newInhibitedByFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &inhibitedByFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func inhibitedByAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_invalid.go b/internal/filters/filter_invalid.go deleted file mode 100644 index 9d8f98c06..000000000 --- a/internal/filters/filter_invalid.go +++ /dev/null @@ -1,10 +0,0 @@ -package filters - -type alwaysInvalidFilter struct { - alertFilter -} - -func (filter *alwaysInvalidFilter) init(name string, _ *matcherT, rawText string, _ bool, _ string) { - filter.Matched = name - filter.RawText = rawText -} diff --git a/internal/filters/filter_label.go b/internal/filters/filter_label.go index f1f591c31..7a5f9e7d1 100644 --- a/internal/filters/filter_label.go +++ b/internal/filters/filter_label.go @@ -1,7 +1,6 @@ package filters import ( - "fmt" "strings" "github.com/prometheus/prometheus/model/labels" @@ -26,39 +25,31 @@ func isDigits(s string) bool { } type labelFilter struct { - value string - alertFilter -} - -func (filter *labelFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *labelFilter) GetValue() string { - return filter.value + filterBase } func (filter *labelFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - isMatch := filter.Matcher.Compare(alert.Labels.Get(filter.Matched), filter.value) - if isMatch { - filter.Hits++ - } - return isMatch + isMatch := filter.matcher.Compare(alert.Labels.Get(filter.name), filter.value) + if isMatch { + filter.hits++ } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + return isMatch } -func newLabelFilter() FilterT { - f := labelFilter{} - return &f +func newLabelFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &labelFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + }, + } } func LabelAutocomplete(labelPairs [][]labels.Label, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_limit.go b/internal/filters/filter_limit.go index 2d8db399c..72b795d07 100644 --- a/internal/filters/filter_limit.go +++ b/internal/filters/filter_limit.go @@ -9,46 +9,41 @@ import ( ) type limitFilter struct { - alertFilter - value int + filterBase + limit int } -func (filter *limitFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - if filter.IsValid { - val, err := strconv.Atoi(value) - if err != nil || val < 1 { - filter.IsValid = false - } else { - filter.value = val - } - } -} - -func (filter *limitFilter) GetValue() string { - return strconv.Itoa(filter.value) +func (filter *limitFilter) Value() string { + return strconv.Itoa(filter.limit) } func (filter *limitFilter) Match(_ *models.Alert, matches int) bool { - if filter.IsValid { - if matches < filter.value { - return true - } - filter.Hits++ - return false + if matches < filter.limit { + return true } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + filter.hits++ + return false } -func newLimitFilter() FilterT { - f := limitFilter{} - return &f +func newLimitFilter(name, operator, rawText, value string) Filter { + val, err := strconv.Atoi(value) + if err != nil || val < 1 { + return &filterBase{rawText: rawText} + } + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &limitFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + }, + limit: val, + } } func limitAutocomplete(name string, operators []string, _ []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_receiver.go b/internal/filters/filter_receiver.go index 8d38939f7..cf1618dc8 100644 --- a/internal/filters/filter_receiver.go +++ b/internal/filters/filter_receiver.go @@ -1,46 +1,37 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type receiverFilter struct { - value string - alertFilter -} - -func (filter *receiverFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *receiverFilter) GetValue() string { - return filter.value + filterBase } func (filter *receiverFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - isMatch := filter.Matcher.Compare(alert.Receiver, filter.value) - if isMatch { - filter.Hits++ - } - return isMatch + isMatch := filter.matcher.Compare(alert.Receiver, filter.value) + if isMatch { + filter.hits++ } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + return isMatch } -func newreceiverFilter() FilterT { - f := receiverFilter{} - return &f +func newReceiverFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &receiverFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + }, + } } func receiverAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_silence_author.go b/internal/filters/filter_silence_author.go index 159c4a46a..02ae3b4da 100644 --- a/internal/filters/filter_silence_author.go +++ b/internal/filters/filter_silence_author.go @@ -1,72 +1,60 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type silenceAuthorFilter struct { - value string - alertFilter -} - -func (filter *silenceAuthorFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *silenceAuthorFilter) GetValue() string { - return filter.value + filterBase } func (filter *silenceAuthorFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - for _, silenceID := range am.SilencedBy { - silence, found := am.Silences[silenceID] - if found { - m := filter.Matcher.Compare(silence.CreatedBy, filter.value) - if m { - isMatch = m - } + var isMatch bool + for _, am := range alert.Alertmanager { + for _, silenceID := range am.SilencedBy { + silence, found := am.Silences[silenceID] + if found { + if filter.matcher.Compare(silence.CreatedBy, filter.value) { + isMatch = true } } } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) -} - -func (filter *silenceAuthorFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - var isMatch bool - for _, silenceID := range am.SilencedBy { - silence, found := am.Silences[silenceID] - if found { - m := filter.Matcher.Compare(silence.CreatedBy, filter.value) - if m { - isMatch = m - } - } + if isMatch { + filter.hits++ } return isMatch } -func newSilenceAuthorFilter() FilterT { - f := silenceAuthorFilter{} - f.IsAlertmanagerFilter = true - return &f +func (filter *silenceAuthorFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + for _, silenceID := range am.SilencedBy { + silence, found := am.Silences[silenceID] + if found { + if filter.matcher.Compare(silence.CreatedBy, filter.value) { + return true + } + } + } + return false +} + +func newSilenceAuthorFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &silenceAuthorFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func silenceAuthorAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_silence_ticket.go b/internal/filters/filter_silence_ticket.go index f32f021cd..ab02905c7 100644 --- a/internal/filters/filter_silence_ticket.go +++ b/internal/filters/filter_silence_ticket.go @@ -1,72 +1,60 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type silenceTicketFilter struct { - value string - alertFilter -} - -func (filter *silenceTicketFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *silenceTicketFilter) GetValue() string { - return filter.value + filterBase } func (filter *silenceTicketFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - for _, silenceID := range am.SilencedBy { - silence, found := am.Silences[silenceID] - if found { - m := filter.Matcher.Compare(silence.TicketID, filter.value) - if m { - isMatch = m - } + var isMatch bool + for _, am := range alert.Alertmanager { + for _, silenceID := range am.SilencedBy { + silence, found := am.Silences[silenceID] + if found { + if filter.matcher.Compare(silence.TicketID, filter.value) { + isMatch = true } } } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) -} - -func (filter *silenceTicketFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - var isMatch bool - for _, silenceID := range am.SilencedBy { - silence, found := am.Silences[silenceID] - if found { - m := filter.Matcher.Compare(silence.TicketID, filter.value) - if m { - isMatch = m - } - } + if isMatch { + filter.hits++ } return isMatch } -func newSilenceTicketFilter() FilterT { - f := silenceTicketFilter{} - f.IsAlertmanagerFilter = true - return &f +func (filter *silenceTicketFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + for _, silenceID := range am.SilencedBy { + silence, found := am.Silences[silenceID] + if found { + if filter.matcher.Compare(silence.TicketID, filter.value) { + return true + } + } + } + return false +} + +func newSilenceTicketFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &silenceTicketFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func silenceTicketIDAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_silenced_by.go b/internal/filters/filter_silenced_by.go index 340a65489..d1348d03a 100644 --- a/internal/filters/filter_silenced_by.go +++ b/internal/filters/filter_silenced_by.go @@ -1,66 +1,54 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type silenceIDFilter struct { - value string - alertFilter -} - -func (filter *silenceIDFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value -} - -func (filter *silenceIDFilter) GetValue() string { - return filter.value + filterBase } func (filter *silenceIDFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - for _, silenceID := range am.SilencedBy { - m := filter.Matcher.Compare(silenceID, filter.value) - if m { - isMatch = m - } + var isMatch bool + for _, am := range alert.Alertmanager { + for _, silenceID := range am.SilencedBy { + if filter.matcher.Compare(silenceID, filter.value) { + isMatch = true } } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) -} - -func (filter *silenceIDFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - var isMatch bool - for _, silenceID := range am.SilencedBy { - m := filter.Matcher.Compare(silenceID, filter.value) - if m { - isMatch = m - } + if isMatch { + filter.hits++ } return isMatch } -func newsilenceIDFilter() FilterT { - f := silenceIDFilter{} - f.IsAlertmanagerFilter = true - return &f +func (filter *silenceIDFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + for _, silenceID := range am.SilencedBy { + if filter.matcher.Compare(silenceID, filter.value) { + return true + } + } + return false +} + +func newSilenceIDFilter(name, operator, rawText, value string) Filter { + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &silenceIDFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func silenceIDAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_state.go b/internal/filters/filter_state.go index b3de28777..76edf962a 100644 --- a/internal/filters/filter_state.go +++ b/internal/filters/filter_state.go @@ -1,59 +1,50 @@ package filters import ( - "fmt" "strings" "github.com/prymitive/karma/internal/models" ) type stateFilter struct { - value string - alertFilter -} - -func (filter *stateFilter) init(name string, matcher *matcherT, rawText string, isValid bool, value string) { - filter.Matched = name - if matcher != nil { - filter.Matcher = *matcher - } - filter.RawText = rawText - filter.IsValid = isValid - filter.value = value - if _, ok := models.AlertStateFromString(value); !ok { - filter.IsValid = false - } -} - -func (filter *stateFilter) GetValue() string { - return filter.value + filterBase } func (filter *stateFilter) Match(alert *models.Alert, _ int) bool { - if filter.IsValid { - var isMatch bool - for _, am := range alert.Alertmanager { - if filter.Matcher.Compare(am.State.String(), filter.value) { - isMatch = true - } + var isMatch bool + for _, am := range alert.Alertmanager { + if filter.matcher.Compare(am.State.String(), filter.value) { + isMatch = true } - if isMatch { - filter.Hits++ - } - return isMatch } - e := fmt.Sprintf("Match() called on invalid filter %#v", filter) - panic(e) + if isMatch { + filter.hits++ + } + return isMatch } func (filter *stateFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { - return filter.Matcher.Compare(am.State.String(), filter.value) + return filter.matcher.Compare(am.State.String(), filter.value) } -func newStateFilter() FilterT { - f := stateFilter{} - f.IsAlertmanagerFilter = true - return &f +func newStateFilter(name, operator, rawText, value string) Filter { + if _, ok := models.AlertStateFromString(value); !ok { + return &filterBase{rawText: rawText} + } + m, ok := buildMatcher(operator, value) + if !ok { + return &filterBase{rawText: rawText} + } + return &stateFilter{ + filterBase: filterBase{ + matcher: m, + name: name, + rawText: rawText, + value: value, + isValid: true, + isAlertmanagerFilter: true, + }, + } } func stateAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) { diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index 2e6842522..6b084f142 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -1063,18 +1063,18 @@ func TestFilters(t *testing.T) { if f == nil { t.Errorf("[%s] No filter found", ft.Expression) } - if f.GetHits() != 0 { - t.Errorf("[%s] Hits = %#v after init(), expected 0", ft.Expression, f.GetHits()) + if f.Hits() != 0 { + t.Errorf("[%s] Hits = %#v after init(), expected 0", ft.Expression, f.Hits()) } - if f.GetIsValid() != ft.IsValid { - t.Errorf("[%s] GetIsValid() returned %#v while %#v was expected", ft.Expression, f.GetIsValid(), ft.IsValid) + if f.Valid() != ft.IsValid { + t.Errorf("[%s] Valid() returned %#v while %#v was expected", ft.Expression, f.Valid(), ft.IsValid) } - if f.GetIsValid() { + if f.Valid() { isAlertmanagerFilter := slices.Contains( []string{"@age", "@alertmanager", "@cluster", "@inhibited", "@inhibited_by", "@state", "@silenced_by", "@silence_ticket", "@silence_author", "@fingerprint"}, - f.GetName()) - if isAlertmanagerFilter != f.GetIsAlertmanagerFilter() { - t.Errorf("[%s] GetIsAlertmanagerFilter() returned %#v while %#v was expected", ft.Expression, f.GetIsAlertmanagerFilter(), isAlertmanagerFilter) + f.Name()) + if isAlertmanagerFilter != f.IsAlertmanagerFilter() { + t.Errorf("[%s] IsAlertmanagerFilter() returned %#v while %#v was expected", ft.Expression, f.IsAlertmanagerFilter(), isAlertmanagerFilter) } m := f.Match(&alert, 0) @@ -1083,17 +1083,17 @@ func TestFilters(t *testing.T) { s, _ := json.Marshal(ft.Silence) t.Errorf("[%s] Match() returned %#v while %#v was expected\nalert used: %s\nsilence used: %s", ft.Expression, m, ft.IsMatch, j, s) } - if ft.IsMatch && f.GetHits() != 1 { - t.Errorf("[%s] GetHits() returned %#v after match, expected 1", ft.Expression, f.GetHits()) + if ft.IsMatch && f.Hits() != 1 { + t.Errorf("[%s] Hits() returned %#v after match, expected 1", ft.Expression, f.Hits()) } - if !ft.IsMatch && f.GetHits() != 0 { - t.Errorf("[%s] GetHits() returned %#v after non-match, expected 0", ft.Expression, f.GetHits()) + if !ft.IsMatch && f.Hits() != 0 { + t.Errorf("[%s] Hits() returned %#v after non-match, expected 0", ft.Expression, f.Hits()) } - if f.GetRawText() != strings.Trim(ft.Expression, " \t") { - t.Errorf("[%s] GetRawText() returned %#v != %s passed as the expression", ft.Expression, f.GetRawText(), ft.Expression) + if f.RawText() != strings.Trim(ft.Expression, " \t") { + t.Errorf("[%s] RawText() returned %#v != %s passed as the expression", ft.Expression, f.RawText(), ft.Expression) } - if m && f.GetIsAlertmanagerFilter() { + if m && f.IsAlertmanagerFilter() { for _, am := range alert.Alertmanager { m := f.MatchAlertmanager(&am) if m != ft.IsAlertmanagerMatch { @@ -1105,19 +1105,11 @@ func TestFilters(t *testing.T) { } } } - if !f.GetIsValid() { - func() { - didPanic := false - defer func() { - if r := recover(); r != nil { - didPanic = true - } - }() - f.Match(&alert, 0) - if !didPanic { - t.Errorf("[%s] Match() on invalid filter didn't cause panic", ft.Expression) - } - }() + if !f.Valid() { + m := f.Match(&alert, 0) + if m { + t.Errorf("[%s] Match() on invalid filter returned true, expected false", ft.Expression) + } } }) } @@ -1181,13 +1173,13 @@ func TestLimitFilter(t *testing.T) { if f == nil { t.Errorf("[%s] No filter found", ft.Expression) } - if f.GetHits() != 0 { - t.Errorf("[%s] Hits = %#v after init(), expected 0", ft.Expression, f.GetHits()) + if f.Hits() != 0 { + t.Errorf("[%s] Hits = %#v after init(), expected 0", ft.Expression, f.Hits()) } - if f.GetIsValid() != ft.IsValid { - t.Errorf("[%s] GetIsValid() returned %#v while %#v was expected", ft.Expression, f.GetIsValid(), ft.IsValid) + if f.Valid() != ft.IsValid { + t.Errorf("[%s] Valid() returned %#v while %#v was expected", ft.Expression, f.Valid(), ft.IsValid) } - if f.GetIsValid() { + if f.Valid() { alert := models.Alert{} var index int for _, isMatch := range ft.IsMatch { @@ -1195,28 +1187,20 @@ func TestLimitFilter(t *testing.T) { if m != isMatch { t.Errorf("[%s] Match() returned %#v while %#v was expected, index %d", ft.Expression, m, isMatch, index) } - if f.GetRawText() != ft.Expression { - t.Errorf("[%s] GetRawText() returned %#v != %s passed as the expression", ft.Expression, f.GetRawText(), ft.Expression) + if f.RawText() != ft.Expression { + t.Errorf("[%s] RawText() returned %#v != %s passed as the expression", ft.Expression, f.RawText(), ft.Expression) } index++ } - if f.GetHits() != ft.Hits { - t.Errorf("[%s] GetHits() returned %#v hits, expected %d", ft.Expression, f.GetHits(), ft.Hits) + if f.Hits() != ft.Hits { + t.Errorf("[%s] Hits() returned %#v hits, expected %d", ft.Expression, f.Hits(), ft.Hits) } } else { - func() { - didPanic := false - defer func() { - if r := recover(); r != nil { - didPanic = true - } - }() - alert := models.Alert{} - f.Match(&alert, 0) - if !didPanic { - t.Errorf("[%s] Match() on invalid filter didn't cause panic", ft.Expression) - } - }() + alert := models.Alert{} + m := f.Match(&alert, 0) + if m { + t.Errorf("[%s] Match() on invalid filter returned true, expected false", ft.Expression) + } } } } diff --git a/internal/filters/invalid_match_test.go b/internal/filters/invalid_match_test.go index fb1f3f7b7..2d2ac0227 100644 --- a/internal/filters/invalid_match_test.go +++ b/internal/filters/invalid_match_test.go @@ -6,42 +6,32 @@ import ( "github.com/prymitive/karma/internal/models" ) -func TestMatchOnInvalidFilter(t *testing.T) { - for _, ft := range AllFilters { - f := ft.Factory() - m, _ := newMatcher(f.GetMatcher()) - f.init(f.GetName(), &m, f.GetRawText(), false, f.GetValue()) - func() { - didPanic := false - defer func() { - if r := recover(); r != nil { - didPanic = true - } - }() - alert := models.Alert{} - f.Match(&alert, 0) - if !didPanic { - t.Errorf("[%s] Match() on invalid filter didn't cause panic", ft.Label) - } - }() +// verifies that an invalid filter (Valid()==false) returns false from Match +// instead of panicking +func TestMatchOnInvalidFilterReturnsFalse(t *testing.T) { + f := &filterBase{rawText: "invalid", isValid: false} + alert := models.Alert{} + if f.Match(&alert, 0) { + t.Error("Match() on invalid filterBase returned true, expected false") } } -func TestGetMatcherNil(t *testing.T) { - f := alertFilter{} - m := f.GetMatcher() - if m != "" { - t.Errorf("Got %q from empty filter GetMatcher()", m) +// verifies that MatcherOperation returns empty string when no matcher is set +func TestMatcherOperationEmpty(t *testing.T) { + f := &filterBase{} + if op := f.MatcherOperation(); op != "" { + t.Errorf("MatcherOperation() = %q, want empty string", op) } } -func TestGetMatcherNotNil(t *testing.T) { - matcher, _ := newMatcher("=") - f := alertFilter{ - Matcher: matcher, +// verifies that MatcherOperation returns the operator when a matcher is set +func TestMatcherOperationSet(t *testing.T) { + m, err := newMatcher("=") + if err != nil { + t.Fatalf("newMatcher(\"=\") returned error: %v", err) } - m := f.GetMatcher() - if m != "=" { - t.Errorf("Got %q from empty filter GetMatcher()", m) + f := &filterBase{matcher: m} + if op := f.MatcherOperation(); op != "=" { + t.Errorf("MatcherOperation() = %q, want \"=\"", op) } } diff --git a/internal/filters/matcher.go b/internal/filters/matcher.go index fb720639d..68ec3c171 100644 --- a/internal/filters/matcher.go +++ b/internal/filters/matcher.go @@ -4,156 +4,86 @@ import ( "errors" "regexp" "strconv" - - lru "github.com/hashicorp/golang-lru/v2" ) -var matchCache, _ = lru.New[string, *regexp.Regexp](1000) - -type matcherT interface { - setOperator(operator string) - GetOperator() string - Compare(valA, valB any) bool -} - -type abstractMatcher struct { - matcherT +// Matcher holds the comparison operator and its implementation. +type Matcher struct { Operator string + Compare func(a, b string) bool } -func (matcher *abstractMatcher) GetOperator() string { - return matcher.Operator -} +func compareEqual(a, b string) bool { return a == b } +func compareNotEqual(a, b string) bool { return a != b } -type equalMatcher struct { - abstractMatcher -} - -func (matcher *equalMatcher) Compare(valA, valB any) bool { - return valA == valB -} - -type notEqualMatcher struct { - abstractMatcher -} - -func (matcher *notEqualMatcher) Compare(valA, valB any) bool { - return valA != valB -} - -type moreThanMatcher struct { - abstractMatcher -} - -func (matcher *moreThanMatcher) Compare(valA, valB any) bool { - if valA == nil || valA == "" || valB == nil || valB == "" { +func compareMoreThan(a, b string) bool { + if a == "" || b == "" { return false } - - intA, okA := castToInt(valA) - intB, okB := castToInt(valB) - + intA, okA := tryAtoi(a) + intB, okB := tryAtoi(b) if okA && okB { return intA > intB } - - return castToString(valA) > castToString(valB) + return a > b } -type lessThanMatcher struct { - abstractMatcher -} - -func (matcher *lessThanMatcher) Compare(valA, valB any) bool { - if valA == nil || valA == "" || valB == nil || valB == "" { +func compareLessThan(a, b string) bool { + if a == "" || b == "" { return false } - - intA, okA := castToInt(valA) - intB, okB := castToInt(valB) - + intA, okA := tryAtoi(a) + intB, okB := tryAtoi(b) if okA && okB { return intA < intB } - - return castToString(valA) < castToString(valB) + return a < b } -type regexpMatcher struct { - abstractMatcher -} - -func (matcher *regexpMatcher) Compare(valA, valB any) bool { - var ( - re *regexp.Regexp - err error - ok bool - ) - switch v := valB.(type) { - case *regexp.Regexp: - re = v - case string: - re, ok = matchCache.Get(v) - if !ok { - if re, err = regexp.Compile("(?i)" + v); err != nil { - return false - } - matchCache.Add(v, re) - } +func compareRegexp(re *regexp.Regexp) func(a, _ string) bool { + return func(a, _ string) bool { + return re.MatchString(a) } - return re.MatchString(castToString(valA)) } -type negativeRegexMatcher struct { - abstractMatcher -} - -func (matcher *negativeRegexMatcher) Compare(valA, valB any) bool { - var ( - re *regexp.Regexp - err error - ok bool - ) - switch v := valB.(type) { - case *regexp.Regexp: - re = v - case string: - re, ok = matchCache.Get(v) - if !ok { - if re, err = regexp.Compile("(?i)" + v); err != nil { - return false - } - matchCache.Add(v, re) - } +func compareNegativeRegexp(re *regexp.Regexp) func(a, _ string) bool { + return func(a, _ string) bool { + return !re.MatchString(a) } - return !re.MatchString(castToString(valA)) } -func newMatcher(matchType string) (matcherT, error) { - if m, found := matcherConfig[matchType]; found { - return m, nil +func newMatcher(operator string) (Matcher, error) { + switch operator { + case equalOperator: + return Matcher{Operator: operator, Compare: compareEqual}, nil + case notEqualOperator: + return Matcher{Operator: operator, Compare: compareNotEqual}, nil + case moreThanOperator: + return Matcher{Operator: operator, Compare: compareMoreThan}, nil + case lessThanOperator: + return Matcher{Operator: operator, Compare: compareLessThan}, nil + case regexpOperator, negativeRegexOperator: + // regex matchers need the pattern to compile, which is not known here. + // Return a placeholder; the caller must call newRegexpMatcher instead. + return Matcher{Operator: operator}, nil } - e := matchType + " not matched with any know match type" - return nil, errors.New(e) + return Matcher{}, errors.New(operator + " not matched with any known match type") } -func castToInt(val any) (int, bool) { - switch v := val.(type) { - case int: +// newRegexpMatcher compiles pattern once and returns a Matcher that uses it. +func newRegexpMatcher(operator, pattern string) (Matcher, error) { + re, err := regexp.Compile("(?i)" + pattern) + if err != nil { + return Matcher{}, err + } + if operator == negativeRegexOperator { + return Matcher{Operator: operator, Compare: compareNegativeRegexp(re)}, nil + } + return Matcher{Operator: operator, Compare: compareRegexp(re)}, nil +} + +func tryAtoi(s string) (int, bool) { + if v, err := strconv.Atoi(s); err == nil { return v, true - case string: - if atoiA, err := strconv.Atoi(v); err == nil { - return atoiA, true - } } return 0, false } - -func castToString(val any) string { - switch v := val.(type) { - case string: - return v - default: - return val.(string) - } -} diff --git a/internal/filters/matcher_test.go b/internal/filters/matcher_test.go index 954f16e63..e8117384d 100644 --- a/internal/filters/matcher_test.go +++ b/internal/filters/matcher_test.go @@ -1,186 +1,151 @@ package filters import ( - "regexp" "testing" - "time" ) type matchTest struct { - ValA any - ValB any - IsValid bool - Expacted bool + ValA string + ValB string + Expected bool } func TestEqualMatcher(t *testing.T) { - now := time.Now() tests := []matchTest{ - {"a", "a", true, true}, - {"abc", "abc", true, true}, - {123, 123, true, true}, - {now, now, true, true}, - {"1", 1, true, false}, - {"a", "ab", true, false}, - {12, 13, true, false}, - {&matchTest{}, &matchTest{}, true, false}, + // identical strings match + {"a", "a", true}, + {"abc", "abc", true}, + // different strings do not match + {"a", "ab", false}, + {"1", "2", false}, } for _, mt := range tests { - m := equalMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("EqualMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + if result := compareEqual(mt.ValA, mt.ValB); result != mt.Expected { + t.Errorf("compareEqual(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } } } func TestNotEqualMatcher(t *testing.T) { - now := time.Now() tests := []matchTest{ - {"a", "a", true, false}, - {"abc", "abc", true, false}, - {123, 123, true, false}, - {now, now, true, false}, - {"1", 1, true, true}, - {"a", "ab", true, true}, - {12, 13, true, true}, + // identical strings do not match + {"a", "a", false}, + {"abc", "abc", false}, + // different strings match + {"a", "ab", true}, + {"1", "2", true}, } for _, mt := range tests { - m := notEqualMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("NotEqualMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + if result := compareNotEqual(mt.ValA, mt.ValB); result != mt.Expected { + t.Errorf("compareNotEqual(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } } } func TestMoreThanMatcher(t *testing.T) { tests := []matchTest{ - {10, 1, true, true}, - {"10", "1", true, true}, - {8, 8, true, false}, - {"8", "8", true, false}, - {4, 9, true, false}, - {"4", "9", true, false}, - {"b", "a", true, true}, - {"a", "a", true, false}, - {"a", "b", true, false}, - {"", "", true, false}, + // numeric comparison + {"10", "1", true}, + {"8", "8", false}, + {"4", "9", false}, + // string comparison when non-numeric + {"b", "a", true}, + {"a", "a", false}, + {"a", "b", false}, + // empty strings return false + {"", "", false}, + {"1", "", false}, + {"", "1", false}, } for _, mt := range tests { - m := moreThanMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("MoreThanMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + if result := compareMoreThan(mt.ValA, mt.ValB); result != mt.Expected { + t.Errorf("compareMoreThan(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } } } func TestLessThanMatcher(t *testing.T) { tests := []matchTest{ - {10, 1, true, false}, - {"10", "1", true, false}, - {8, 8, true, false}, - {"8", "8", true, false}, - {4, 9, true, true}, - {"4", "9", true, true}, - {"b", "a", true, false}, - {"a", "a", true, false}, - {"a", "b", true, true}, - {"", "", true, false}, + // numeric comparison + {"10", "1", false}, + {"8", "8", false}, + {"4", "9", true}, + // string comparison when non-numeric + {"b", "a", false}, + {"a", "a", false}, + {"a", "b", true}, + // empty strings return false + {"", "", false}, + {"1", "", false}, + {"", "1", false}, } for _, mt := range tests { - m := lessThanMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("LessThanMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + if result := compareLessThan(mt.ValA, mt.ValB); result != mt.Expected { + t.Errorf("compareLessThan(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } } } func TestRegexpMatcher(t *testing.T) { tests := []matchTest{ - {"abcdef", "^abc", true, true}, - {"abc", "^abc", true, true}, - {"xxabcxx", "abc", true, true}, - {"123", "123", true, true}, - {"5", "^[0-9]+", true, true}, - {"xb", "abc", true, false}, - {"13", "12", true, false}, + // matching patterns + {"abcdef", "^abc", true}, + {"abc", "^abc", true}, + {"xxabcxx", "abc", true}, + {"123", "123", true}, + {"5", "^[0-9]+", true}, + // non-matching patterns + {"xb", "abc", false}, + {"13", "12", false}, } for _, mt := range tests { - m := regexpMatcher{} - if result := m.Compare(mt.ValA, regexp.MustCompile(mt.ValB.(string))); result != mt.Expacted { - t.Errorf("RegexpMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + m, err := newRegexpMatcher(regexpOperator, mt.ValB) + if err != nil { + t.Fatalf("newRegexpMatcher(%q, %q) error: %v", regexpOperator, mt.ValB, err) + } + if result := m.Compare(mt.ValA, ""); result != mt.Expected { + t.Errorf("regexpMatcher(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } - } } func TestNegativeRegexpMatcher(t *testing.T) { tests := []matchTest{ - {"abcdef", "^abc", true, false}, - {"abc", "^abc", true, false}, - {"xxabcxx", "abc", true, false}, - {"123", "123", true, false}, - {"5", "^[0-9]+", true, false}, - {"xb", "abc", true, true}, - {"13", "12", true, true}, + // matching patterns return false (negated) + {"abcdef", "^abc", false}, + {"abc", "^abc", false}, + {"xxabcxx", "abc", false}, + {"123", "123", false}, + {"5", "^[0-9]+", false}, + // non-matching patterns return true (negated) + {"xb", "abc", true}, + {"13", "12", true}, } for _, mt := range tests { - m := negativeRegexMatcher{} - if result := m.Compare(mt.ValA, regexp.MustCompile(mt.ValB.(string))); result != mt.Expacted { - t.Errorf("NegativeRegexMatcher(%#v, %#v) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + m, err := newRegexpMatcher(negativeRegexOperator, mt.ValB) + if err != nil { + t.Fatalf("newRegexpMatcher(%q, %q) error: %v", negativeRegexOperator, mt.ValB, err) } - - } -} - -func TestRegexpMatcherWithStringValB(t *testing.T) { - tests := []matchTest{ - // verifies that passing a raw string pattern (not pre-compiled) compiles and matches - {"abcdef", "^abc", true, true}, - // verifies cache hit on the same string pattern - {"abcdef", "^abc", true, true}, - // verifies that a non-matching raw string pattern returns false - {"xyz", "^abc", true, false}, - } - for _, mt := range tests { - m := regexpMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("RegexpMatcher(%#v, %#v string) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) + if result := m.Compare(mt.ValA, ""); result != mt.Expected { + t.Errorf("negativeRegexpMatcher(%q, %q) = %v, want %v", mt.ValA, mt.ValB, result, mt.Expected) } } } +// verifies that an invalid regex pattern returns an error from newRegexpMatcher func TestRegexpMatcherWithInvalidPattern(t *testing.T) { - // verifies that an invalid regex pattern string returns false instead of panicking - m := regexpMatcher{} - result := m.Compare("foo", "[invalid") - if result != false { - t.Errorf("RegexpMatcher with invalid pattern returned %v, expected false", result) - } -} - -func TestNegativeRegexpMatcherWithStringValB(t *testing.T) { - tests := []matchTest{ - // verifies that passing a raw string pattern (not pre-compiled) compiles and negation works - {"abcdef", "^abcdef$", true, false}, - // verifies cache hit on the same string pattern - {"abcdef", "^abcdef$", true, false}, - // verifies that a non-matching raw string pattern returns true (negated) - {"xyz", "^abcdef$", true, true}, - } - for _, mt := range tests { - m := negativeRegexMatcher{} - if result := m.Compare(mt.ValA, mt.ValB); result != mt.Expacted { - t.Errorf("NegativeRegexMatcher(%#v, %#v string) returned %v when %v was expected", mt.ValA, mt.ValB, result, mt.Expacted) - } + _, err := newRegexpMatcher(regexpOperator, "[invalid") + if err == nil { + t.Error("newRegexpMatcher with invalid pattern should return an error") } } +// verifies that an invalid regex pattern returns an error from newRegexpMatcher +// for the negative variant func TestNegativeRegexpMatcherWithInvalidPattern(t *testing.T) { - // verifies that an invalid regex pattern string returns false instead of panicking - m := negativeRegexMatcher{} - result := m.Compare("foo", "[invalid") - if result != false { - t.Errorf("NegativeRegexMatcher with invalid pattern returned %v, expected false", result) + _, err := newRegexpMatcher(negativeRegexOperator, "[invalid") + if err == nil { + t.Error("newRegexpMatcher with invalid pattern should return an error") } } @@ -231,21 +196,19 @@ func TestNewMatcher(t *testing.T) { for _, operator := range operators { m, err := newMatcher(operator) if err != nil { - t.Errorf("NewMatcher(%s) returned error: %s", operator, err.Error()) + t.Errorf("newMatcher(%s) returned error: %s", operator, err.Error()) } - if m.GetOperator() != operator { - t.Errorf("Got wrong matcher for %s: %s", operator, m.GetOperator()) + if m.Operator != operator { + t.Errorf("Got wrong matcher for %s: %s", operator, m.Operator) } } } +// verifies that an unknown operator returns an error func TestInvalidMatcher(t *testing.T) { operator := "<>" - m, err := newMatcher(operator) + _, err := newMatcher(operator) if err == nil { - t.Errorf("NewMatcher(%s) didn't return any error: %s", operator, m) - } - if m != nil { - t.Errorf("NewMatcher(%s) returned non-nil value: %s", operator, m) + t.Errorf("newMatcher(%s) didn't return any error", operator) } } diff --git a/internal/filters/registry.go b/internal/filters/registry.go index 64b715942..d390b0e99 100644 --- a/internal/filters/registry.go +++ b/internal/filters/registry.go @@ -20,18 +20,12 @@ var matcherRegex = "[=!<>~]+" // same as matcherRegex but for the filter name part var filterRegex = "^(@)?[a-zA-Z_][a-zA-Z0-9_]*" -var matcherConfig = map[string]matcherT{ - equalOperator: &equalMatcher{abstractMatcher{Operator: equalOperator}}, - notEqualOperator: ¬EqualMatcher{abstractMatcher{Operator: notEqualOperator}}, - moreThanOperator: &moreThanMatcher{abstractMatcher{Operator: moreThanOperator}}, - lessThanOperator: &lessThanMatcher{abstractMatcher{Operator: lessThanOperator}}, - regexpOperator: ®expMatcher{abstractMatcher{Operator: regexpOperator}}, - negativeRegexOperator: &negativeRegexMatcher{abstractMatcher{Operator: negativeRegexOperator}}, -} +// filterFactory constructs a Filter from parsed expression components. +type filterFactory func(name, operator, rawText, value string) Filter type filterConfig struct { LabelRe *regexp.Regexp - Factory newFilterFactory + Factory filterFactory Autocomplete autocompleteFactory Label string SupportedOperators []string @@ -85,7 +79,7 @@ var AllFilters = []filterConfig{ Label: "@receiver", LabelRe: regexp.MustCompile("^@receiver$"), SupportedOperators: []string{regexpOperator, negativeRegexOperator, equalOperator, notEqualOperator}, - Factory: newreceiverFilter, + Factory: newReceiverFilter, Autocomplete: receiverAutocomplete, }, { @@ -99,7 +93,7 @@ var AllFilters = []filterConfig{ Label: "@silenced_by", LabelRe: regexp.MustCompile("^@silenced_by$"), SupportedOperators: []string{equalOperator, notEqualOperator}, - Factory: newsilenceIDFilter, + Factory: newSilenceIDFilter, Autocomplete: silenceIDAutocomplete, }, {