fix(backend): refactor filters

This commit is contained in:
Lukasz Mierzwa
2026-03-11 13:10:15 +00:00
committed by Łukasz Mierzwa
parent 723d2a4ded
commit 7f9be48800
27 changed files with 801 additions and 1002 deletions
+6 -6
View File
@@ -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{}{}
}
}
+10 -10
View File
@@ -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)
}
}
+22 -23
View File
@@ -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
@@ -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)
}
}
}
+6 -6
View File
@@ -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
+2 -2
View File
@@ -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)
+55 -66
View File
@@ -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<matched>(%s))(?P<operator>(%s))(?P<value>(.*))", 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}
}
+35 -38
View File
@@ -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) {
+25 -34
View File
@@ -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) {
+25 -34
View File
@@ -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) {
+25 -36
View File
@@ -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,
},
}
}
+41 -49
View File
@@ -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,
}
}
+37 -38
View File
@@ -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) {
+32 -44
View File
@@ -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) {
-10
View File
@@ -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
}
+19 -28
View File
@@ -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) {
+27 -32
View File
@@ -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) {
+19 -28
View File
@@ -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) {
+37 -49
View File
@@ -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) {
+37 -49
View File
@@ -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) {
+32 -44
View File
@@ -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) {
+28 -37
View File
@@ -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) {
+34 -50
View File
@@ -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)
}
}
}
}
+20 -30
View File
@@ -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)
}
}
+50 -120
View File
@@ -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)
}
}
+91 -128
View File
@@ -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)
}
}
+5 -11
View File
@@ -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: &notEqualMatcher{abstractMatcher{Operator: notEqualOperator}},
moreThanOperator: &moreThanMatcher{abstractMatcher{Operator: moreThanOperator}},
lessThanOperator: &lessThanMatcher{abstractMatcher{Operator: lessThanOperator}},
regexpOperator: &regexpMatcher{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,
},
{