diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 9fdc6c160..47d42f398 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -244,6 +244,7 @@ func (am *Alertmanager) pullAlerts(version string) error { alert.Alertmanager = []models.AlertmanagerInstance{ { + Fingerprint: alert.Fingerprint, Name: am.Name, Cluster: am.ClusterName(), State: alert.State, diff --git a/internal/filters/filter_fingerprint.go b/internal/filters/filter_fingerprint.go new file mode 100644 index 000000000..7c78b7ed6 --- /dev/null +++ b/internal/filters/filter_fingerprint.go @@ -0,0 +1,39 @@ +package filters + +import ( + "fmt" + + "github.com/prymitive/karma/internal/models" +) + +type fingerprintFilter struct { + alertFilter +} + +func (filter *fingerprintFilter) Match(alert *models.Alert, matches 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 + } + } + if isMatch { + filter.Hits++ + } + return isMatch + } + e := fmt.Sprintf("Match() called on invalid filter %#v", filter) + panic(e) +} + +func (filter *fingerprintFilter) MatchAlertmanager(am *models.AlertmanagerInstance) bool { + return filter.Matcher.Compare(am.Fingerprint, filter.Value) +} + +func newFingerprintFilter() FilterT { + f := fingerprintFilter{} + f.IsAlertmanagerFilter = true + return &f +} diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index 319c3d305..f8c72256f 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -114,6 +114,83 @@ var tests = []filterTest{ IsValid: false, }, + { + Expression: "@fingerprint=", + IsValid: false, + }, + { + Expression: "@fingerprint==", + IsValid: false, + }, + { + Expression: "@fingerprint<=active", + IsValid: false, + }, + { + Expression: "@fingerprint=123", + IsValid: true, + Alert: models.Alert{}, + IsMatch: false, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint!=123", + IsValid: true, + Alert: models.Alert{}, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=1234", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "1234"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=~123", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "01234"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { + Expression: "@fingerprint=abc", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "12345"}, + }, + IsMatch: false, + IsAlertmanagerMatch: false, + }, + { + Expression: "@fingerprint!=1a1", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "1a1"}, + }, + IsMatch: false, + IsAlertmanagerMatch: false, + }, + { + Expression: "@fingerprint!=cde", + IsValid: true, + Alert: models.Alert{}, + Alertmanagers: []models.AlertmanagerInstance{ + {Fingerprint: "abc"}, + }, + IsMatch: true, + IsAlertmanagerMatch: true, + }, + { Expression: "@silence_id=abcdef", IsValid: true, @@ -699,7 +776,7 @@ func TestFilters(t *testing.T) { } if f.GetIsValid() { isAlertmanagerFilter := slices.StringInSlice( - []string{"@age", "@alertmanager", "@cluster", "@state", "@silence_id", "@silence_ticket", "@silence_author"}, + []string{"@age", "@alertmanager", "@cluster", "@state", "@silence_id", "@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) diff --git a/internal/filters/registry.go b/internal/filters/registry.go index 95418136b..8b5b90f1c 100644 --- a/internal/filters/registry.go +++ b/internal/filters/registry.go @@ -61,6 +61,12 @@ var AllFilters = []filterConfig{ Factory: newStateFilter, Autocomplete: stateAutocomplete, }, + { + Label: "@fingerprint", + LabelRe: regexp.MustCompile("^@fingerprint$"), + SupportedOperators: []string{regexpOperator, equalOperator, notEqualOperator}, + Factory: newFingerprintFilter, + }, { Label: "@receiver", LabelRe: regexp.MustCompile("^@receiver$"), diff --git a/internal/mapper/v017/api.go b/internal/mapper/v017/api.go index 8faa7a041..2ae073d83 100644 --- a/internal/mapper/v017/api.go +++ b/internal/mapper/v017/api.go @@ -56,6 +56,7 @@ func groups(c *client.Alertmanager, timeout time.Duration) ([]models.AlertGroup, } for _, alert := range group.Alerts { a := models.Alert{ + Fingerprint: *alert.Fingerprint, Receiver: *group.Receiver.Name, Annotations: models.AnnotationsFromMap(alert.Annotations), Labels: alert.Labels, diff --git a/internal/models/alert.go b/internal/models/alert.go index 194d7080c..9d18092b3 100644 --- a/internal/models/alert.go +++ b/internal/models/alert.go @@ -37,6 +37,7 @@ type Alert struct { // those are not exposed in JSON, Alertmanager specific value will be in kept // in the Alertmanager slice // skip those when generating alert fingerprint too + Fingerprint string `json:"-" hash:"-"` GeneratorURL string `json:"-" hash:"-"` SilencedBy []string `json:"-" hash:"-"` InhibitedBy []string `json:"-" hash:"-"` diff --git a/internal/models/alertmanager.go b/internal/models/alertmanager.go index 900b112c7..0b7318034 100644 --- a/internal/models/alertmanager.go +++ b/internal/models/alertmanager.go @@ -5,8 +5,9 @@ import "time" // AlertmanagerInstance describes the Alertmanager instance alert was collected // from type AlertmanagerInstance struct { - Name string `json:"name"` - Cluster string `json:"cluster"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` + Cluster string `json:"cluster"` // per instance alert state State string `json:"state"` // timestamp collected from this instance, those on the alert itself diff --git a/internal/models/api_test.go b/internal/models/api_test.go index 2270f17dc..829e3661b 100644 --- a/internal/models/api_test.go +++ b/internal/models/api_test.go @@ -12,8 +12,10 @@ import ( func TestDedupSharedMaps(t *testing.T) { am := models.AlertmanagerInstance{ - Cluster: "fakeCluster", - SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, + Fingerprint: "1", + Name: "am", + Cluster: "fakeCluster", + SilencedBy: []string{"fakeSilence1", "fakeSilence2"}, } ag := models.APIAlertGroup{ AlertGroup: models.AlertGroup{ @@ -98,7 +100,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -110,7 +113,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -135,7 +139,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -147,7 +152,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -172,7 +178,8 @@ func TestDedupSharedMaps(t *testing.T) { "state": "suppressed", "alertmanager": [ { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z", @@ -184,7 +191,8 @@ func TestDedupSharedMaps(t *testing.T) { "inhibitedBy": null }, { - "name": "", + "fingerprint": "1", + "name": "am", "cluster": "fakeCluster", "state": "", "startsAt": "0001-01-01T00:00:00Z",