diff --git a/README.md b/README.md
index f48a1ca0f..0d20f299a 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,8 @@ see [CONFIGURATION.md](docs/CONFIGURATION.md#alertmanagers).
### Alert visualization
+#### Alert groups
+
Alerts are displayed grouped preserving
[group_by](https://prometheus.io/docs/alerting/configuration/#route)
configuration option in Alertmanager.
@@ -55,6 +57,20 @@ and annotations that are shared between all alerts are moved to the footer.
![Example](/docs/img/alertGroup.png)
+#### Inhibited alerts
+
+Inhibited alerts (suppressed by other alerts,
+[see Alertmanager docs](https://prometheus.io/docs/alerting/latest/alertmanager/#inhibition))
+will have a "muted" button.
+
+![Inhibited alert](/docs/img/inhibited.png)
+
+Clicking on that button will bring a modal with a list of inhibiting alerts.
+
+![Inhibiting alerts](/docs/img/inhibitedByModal.png)
+
+#### Silence deduplication
+
If all alerts in a group were suppressed by the same silence then, to save
screen space, the silence will also be moved to the footer.
diff --git a/docs/img/alertGroup.png b/docs/img/alertGroup.png
index ee0799074..54c02fef3 100644
Binary files a/docs/img/alertGroup.png and b/docs/img/alertGroup.png differ
diff --git a/docs/img/dark.png b/docs/img/dark.png
index d208ff56d..b2bfe2e34 100644
Binary files a/docs/img/dark.png and b/docs/img/dark.png differ
diff --git a/docs/img/footerSilence.png b/docs/img/footerSilence.png
index 6df38449e..756d4c216 100644
Binary files a/docs/img/footerSilence.png and b/docs/img/footerSilence.png differ
diff --git a/docs/img/inhibited.png b/docs/img/inhibited.png
new file mode 100644
index 000000000..89ba67896
Binary files /dev/null and b/docs/img/inhibited.png differ
diff --git a/docs/img/inhibitedByModal.png b/docs/img/inhibitedByModal.png
new file mode 100644
index 000000000..625388d43
Binary files /dev/null and b/docs/img/inhibitedByModal.png differ
diff --git a/docs/img/multiGrid.png b/docs/img/multiGrid.png
index 4e9a128b7..a2d2e650c 100644
Binary files a/docs/img/multiGrid.png and b/docs/img/multiGrid.png differ
diff --git a/docs/img/overview.png b/docs/img/overview.png
index fabc43ca8..a1535e377 100644
Binary files a/docs/img/overview.png and b/docs/img/overview.png differ
diff --git a/docs/img/screenshot.png b/docs/img/screenshot.png
index 4e25e23b8..e3dce9f7e 100644
Binary files a/docs/img/screenshot.png and b/docs/img/screenshot.png differ
diff --git a/docs/img/silenceBrowser.png b/docs/img/silenceBrowser.png
index a33869473..4cc9aca99 100644
Binary files a/docs/img/silenceBrowser.png and b/docs/img/silenceBrowser.png differ
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",
diff --git a/ui/src/Common/Query.js b/ui/src/Common/Query.js
index dcaea36c0..afb527a05 100644
--- a/ui/src/Common/Query.js
+++ b/ui/src/Common/Query.js
@@ -8,6 +8,7 @@ const StaticLabels = Object.freeze({
AlertName: "alertname",
AlertManager: "@alertmanager",
AlertmanagerCluster: "@cluster",
+ Fingerprint: "@fingerprint",
Receiver: "@receiver",
State: "@state",
SilenceID: "@silence_id",
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap
index 3c344eb53..7bf9ce9d5 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap
@@ -81,7 +81,7 @@ exports[` matches snapshot when inhibited 1`] = `
-
+