diff --git a/README.md b/README.md index 6fa4be97c..0832b36d2 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,62 @@ This option can also be set using `-alertmanager.uris` flag. Example: This variable is required and there is no default value. +#### ANNOTATIONS_DEFAULT_HIDDEN + +Enabling this option will hide all annotations in the UI, except for those +that are listed in the `ANNOTATIONS_VISIBLE` option. + +Examples: + + ANNOTATIONS_DEFAULT_HIDDEN=true + ANNOTATIONS_DEFAULT_HIDDEN=false + +This option can also be set using `-annotations.default.hidden` flag. Example: + + $ unsee -annotations.default.hidden + +Default is `false`, which means that all annotations are visible. + +#### ANNOTATIONS_HIDDEN + +List of annotation names that should be hidden in the UI. Hidden annotations +can still be accessed if needed by clicking on a zoom button that will appear +if there are any hidden annotations. + +Examples: + + ANNOTATIONS_HIDDEN=summary + ANNOTATIONS_HIDDEN="summary owner" + +This option can also be set using `-annotations.hidden` flag. Example: + + $ unsee -annotations.hidden "summary owner" + +This variable is optional and default is not set (all annotations are visible), +unless user enables `ANNOTATIONS_DEFAULT_HIDDEN` option. + +#### ANNOTATIONS_VISIBLE + +List of annotation names that should be visible in the UI. This option is only +useful when `ANNOTATIONS_DEFAULT_HIDDEN` is set. +With `ANNOTATIONS_DEFAULT_HIDDEN` all annotations are hidden by default unless +they are present in the `ANNOTATIONS_VISIBLE` option. +If `ANNOTATIONS_DEFAULT_HIDDEN` is not enabled this option is no-op. + +Examples: + + ANNOTATIONS_VISIBLE=summary + ANNOTATIONS_VISIBLE="summary owner" + +This option can also be set using `-annotations.visible` flag. Example: + + $ unsee -annotations.visible "summary owner" + +This variable is optional and default is not set. +If `ANNOTATIONS_HIDDEN` is enabled then all annotations are hidden by default. +If `ANNOTATIONS_HIDDEN` is not enabled then all annotations are visible by +default. + #### DEBUG Will enable [gin](https://github.com/gin-gonic/gin) debug mode. This will diff --git a/api_test.go b/api_test.go index 8cf3cd316..dd7b0455a 100644 --- a/api_test.go +++ b/api_test.go @@ -29,8 +29,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "alert": "Memory usage exceeding threshold", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "alert", Value: "Memory usage exceeding threshold"}, + models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true}, }, Labels: map[string]string{ "alertname": "Memory_Usage_Too_High", @@ -48,9 +49,6 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{ - "dashboard": "http://localhost/dashboard.html", - }, }, }, id: "099c5ca6d1c92f615b13056b935d0c8dee70f18c", @@ -69,8 +67,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "alert": "Memory usage exceeding threshold", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "alert", Value: "Memory usage exceeding threshold"}, + models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -86,9 +85,6 @@ var groupTests = []groupTest{ "instance": "server2", "job": "node_exporter", }, - Links: map[string]string{ - "dashboard": "http://localhost/dashboard.html", - }, State: models.AlertStateActive, Receiver: "by-cluster-service", }, @@ -109,8 +105,8 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -126,13 +122,12 @@ var groupTests = []groupTest{ "instance": "server3", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -148,13 +143,12 @@ var groupTests = []groupTest{ "instance": "server4", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -170,7 +164,6 @@ var groupTests = []groupTest{ "instance": "server5", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-cluster-service", }, @@ -191,8 +184,8 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -214,13 +207,12 @@ var groupTests = []groupTest{ "instance": "server6", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateSuppressed, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -245,13 +237,12 @@ var groupTests = []groupTest{ "instance": "server7", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateSuppressed, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -272,7 +263,6 @@ var groupTests = []groupTest{ "instance": "server8", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateSuppressed, Receiver: "by-cluster-service", }, @@ -292,8 +282,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, + models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -311,13 +302,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{ - "url": "http://localhost/example.html", - }, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -335,11 +323,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -357,11 +344,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -379,11 +365,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -401,11 +386,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -428,11 +412,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -459,11 +442,10 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Labels: map[string]string{ "alertname": "Host_Down", @@ -486,7 +468,6 @@ var groupTests = []groupTest{ }, }, Receiver: "by-name", - Links: map[string]string{}, }, }, id: "58c6a3467cebc53abe68ecbe8643ce478c5a1573", @@ -505,8 +486,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "alert": "Less than 10% disk space is free", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "alert", Value: "Less than 10% disk space is free"}, + models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -522,9 +504,6 @@ var groupTests = []groupTest{ "instance": "server5", "job": "node_exporter", }, - Links: map[string]string{ - "dashboard": "http://localhost/dashboard.html", - }, State: models.AlertStateActive, Receiver: "by-cluster-service", }, @@ -545,8 +524,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, + models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -562,15 +542,12 @@ var groupTests = []groupTest{ "instance": "server1", "job": "node_ping", }, - Links: map[string]string{ - "url": "http://localhost/example.html", - }, State: models.AlertStateActive, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -586,7 +563,6 @@ var groupTests = []groupTest{ "instance": "server2", "job": "node_ping", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-cluster-service", }, @@ -606,9 +582,10 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", - "help": "Example help annotation", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"}, + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, + models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -629,15 +606,12 @@ var groupTests = []groupTest{ "instance": "web1", "job": "node_exporter", }, - Links: map[string]string{ - "url": "http://localhost/example.html", - }, State: models.AlertStateSuppressed, Receiver: "by-name", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -653,7 +627,6 @@ var groupTests = []groupTest{ "instance": "web2", "job": "node_exporter", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-name", }, @@ -673,8 +646,9 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "alert": "Less than 10% disk space is free", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "alert", Value: "Less than 10% disk space is free"}, + models.Annotation{Visible: true, Name: "dashboard", Value: "http://localhost/dashboard.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -690,9 +664,6 @@ var groupTests = []groupTest{ "instance": "server5", "job": "node_exporter", }, - Links: map[string]string{ - "dashboard": "http://localhost/dashboard.html", - }, State: models.AlertStateActive, Receiver: "by-name", }, @@ -713,9 +684,10 @@ var groupTests = []groupTest{ }, alerts: []models.Alert{ models.Alert{ - Annotations: map[string]string{ - "help": "Example help annotation", - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "help", Value: "Example help annotation"}, + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, + models.Annotation{Visible: true, Name: "url", Value: "http://localhost/example.html", IsLink: true}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -736,15 +708,12 @@ var groupTests = []groupTest{ "instance": "web1", "job": "node_exporter", }, - Links: map[string]string{ - "url": "http://localhost/example.html", - }, State: models.AlertStateSuppressed, Receiver: "by-cluster-service", }, models.Alert{ - Annotations: map[string]string{ - "summary": "Example summary", + Annotations: models.Annotations{ + models.Annotation{Visible: true, Name: "summary", Value: "Example summary"}, }, Alertmanager: []models.AlertmanagerInstance{ models.AlertmanagerInstance{ @@ -760,7 +729,6 @@ var groupTests = []groupTest{ "instance": "web2", "job": "node_exporter", }, - Links: map[string]string{}, State: models.AlertStateActive, Receiver: "by-cluster-service", }, @@ -875,10 +843,6 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler t.Errorf("[%s] Labels mismatch on alert receiver='%s', expected labels=%v but got %v", version, expectedAlert.Receiver, expectedAlert.Labels, gotAlert.Labels) } - if !reflect.DeepEqual(gotAlert.Links, expectedAlert.Links) { - t.Errorf("[%s] Links mismatch on alert receiver='%s' labels=%v expected %v but got %v", - version, expectedAlert.Receiver, expectedAlert.Labels, expectedAlert.Links, gotAlert.Links) - } if len(gotAlert.Alertmanager) != len(expectedAlert.Alertmanager) { t.Errorf("[%s] Expected %d alertmanager instances but got %d on alert receiver='%s' labels=%v", version, len(expectedAlert.Alertmanager), len(gotAlert.Alertmanager), gotAlert.Receiver, expectedAlert.Labels) diff --git a/assets/static/alerts.js b/assets/static/alerts.js index 830afe7e2..1ed950ea9 100644 --- a/assets/static/alerts.js +++ b/assets/static/alerts.js @@ -33,6 +33,7 @@ AlertGroup.prototype.Added = function() { var elem = $("#" + this.id); ui.setupGroupTooltips(elem); ui.setupGroupLinkHover(elem); + ui.setupGroupAnnotationToggles(elem); }; AlertGroup.prototype.Update = function() { diff --git a/assets/static/templates.js b/assets/static/templates.js index ddaedaab3..9012b87d9 100644 --- a/assets/static/templates.js +++ b/assets/static/templates.js @@ -36,8 +36,9 @@ var templates = {}, silenceFormFatal: "#silence-form-fatal", silenceFormLoading: "#silence-form-loading", - // label button + // alert partials buttonLabel: "#label-button-filter", + alertAnnotation: "#alert-annotation", // alert group alertGroup: "#alert-group", diff --git a/assets/static/ui.js b/assets/static/ui.js index 2849a75a3..1c0fdacb8 100644 --- a/assets/static/ui.js +++ b/assets/static/ui.js @@ -5,6 +5,7 @@ const $ = require("jquery"); const alerts = require("./alerts"); const autocomplete = require("./autocomplete"); const filters = require("./filters"); +const grid = require("./grid"); const summary = require("./summary"); const templates = require("./templates"); const unsee = require("./unsee"); @@ -75,6 +76,29 @@ function setupGroupTooltips(groupElem) { }); } +function setupGroupAnnotationToggles(groupElem) { + $(groupElem).on("click", "[data-toggle=toggle-hidden-annotation]", function() { + var alert = $(this).parent(); + var icon = $(this).find("i.fa"); + var showingHidden = icon.hasClass("fa-search-minus"); + if (showingHidden) { + // we're currently showing hidden annotations, so the action is to hide them + icon.removeClass("fa-search-minus").addClass("fa-search-plus"); + $.each(alert.find(".hidden-annotation"), function(i, annotation){ + $(annotation).addClass("hidden"); + }); + } else { + // we're currently hiding hidden annotations, so the action is to show them + icon.removeClass("fa-search-plus").addClass("fa-search-minus"); + $.each(alert.find(".hidden-annotation"), function(i, annotation){ + $(annotation).removeClass("hidden"); + }); + } + grid.redraw(); + }); +} + exports.setupModal = setupModal; exports.setupGroupTooltips = setupGroupTooltips; exports.setupGroupLinkHover = setupGroupLinkHover; +exports.setupGroupAnnotationToggles = setupGroupAnnotationToggles; diff --git a/assets/templates/alertgroup.html b/assets/templates/alertgroup.html index b524d00c6..a5a0faa1b 100644 --- a/assets/templates/alertgroup.html +++ b/assets/templates/alertgroup.html @@ -27,28 +27,32 @@ @@ -308,3 +312,14 @@ <%- label.text %> > + + diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index c1714d4a9..344eb0e19 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -172,8 +172,6 @@ func (am *Alertmanager) pullAlerts(version string) error { }, } - alert.Annotations, alert.Links = transform.DetectLinks(alert.Annotations) - transform.ColorLabel(colors, "@receiver", alert.Receiver) for k, v := range alert.Labels { transform.ColorLabel(colors, k, v) diff --git a/internal/config/config.go b/internal/config/config.go index 9a503448b..720e84186 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,20 +24,23 @@ func (mvd *spaceSeparatedList) Decode(value string) error { } type configEnvs struct { - AlertmanagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s" help:"Timeout for all request send to Alertmanager"` - AlertmanagerTTL time.Duration `envconfig:"ALERTMANAGER_TTL" default:"1m" help:"TTL for Alertmanager alerts and silences"` - AlertmanagerURIs spaceSeparatedList `envconfig:"ALERTMANAGER_URIS" required:"true" help:"List of Alertmanager URIs (name:uri)"` - ColorLabelsStatic spaceSeparatedList `envconfig:"COLOR_LABELS_STATIC" help:"List of label names that should have the same (but distinct) color"` - ColorLabelsUnique spaceSeparatedList `envconfig:"COLOR_LABELS_UNIQUE" help:"List of label names that should have unique color"` - Debug bool `envconfig:"DEBUG" default:"false" help:"Enable debug mode"` - FilterDefault string `envconfig:"FILTER_DEFAULT" help:"Default filter string"` - JiraRegexp spaceSeparatedList `envconfig:"JIRA_REGEX" help:"List of JIRA regex rules"` - Port int `envconfig:"PORT" default:"8080" help:"HTTP port to listen on"` - SentryDSN string `envconfig:"SENTRY_DSN" help:"Sentry DSN for Go exceptions"` - SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN" help:"Sentry DSN for javascript exceptions"` - StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS" help:"List of labels to ignore"` - KeepLabels spaceSeparatedList `envconfig:"KEEP_LABELS" help:"List of labels to keep, all other labels will be stripped"` - WebPrefix string `envconfig:"WEB_PREFIX" default:"/" help:"URL prefix"` + AlertmanagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s" help:"Timeout for all request send to Alertmanager"` + AlertmanagerTTL time.Duration `envconfig:"ALERTMANAGER_TTL" default:"1m" help:"TTL for Alertmanager alerts and silences"` + AlertmanagerURIs spaceSeparatedList `envconfig:"ALERTMANAGER_URIS" required:"true" help:"List of Alertmanager URIs (name:uri)"` + AnnotationsHidden spaceSeparatedList `envconfig:"ANNOTATIONS_HIDDEN" help:"List of annotations that are hidden by default"` + AnnotationsDefaultHidden bool `envconfig:"ANNOTATIONS_DEFAULT_HIDDEN" default:"false" help:"Hide all annotations by default unless listed in ANNOTATIONS_VISIBLE"` + AnnotationsVisible spaceSeparatedList `envconfig:"ANNOTATIONS_VISIBLE" help:"List of annotations that are visible by default"` + ColorLabelsStatic spaceSeparatedList `envconfig:"COLOR_LABELS_STATIC" help:"List of label names that should have the same (but distinct) color"` + ColorLabelsUnique spaceSeparatedList `envconfig:"COLOR_LABELS_UNIQUE" help:"List of label names that should have unique color"` + Debug bool `envconfig:"DEBUG" default:"false" help:"Enable debug mode"` + FilterDefault string `envconfig:"FILTER_DEFAULT" help:"Default filter string"` + JiraRegexp spaceSeparatedList `envconfig:"JIRA_REGEX" help:"List of JIRA regex rules"` + Port int `envconfig:"PORT" default:"8080" help:"HTTP port to listen on"` + SentryDSN string `envconfig:"SENTRY_DSN" help:"Sentry DSN for Go exceptions"` + SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN" help:"Sentry DSN for javascript exceptions"` + StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS" help:"List of labels to ignore"` + KeepLabels spaceSeparatedList `envconfig:"KEEP_LABELS" help:"List of labels to keep, all other labels will be stripped"` + WebPrefix string `envconfig:"WEB_PREFIX" default:"/" help:"URL prefix"` } // Config exposes all options required to run diff --git a/internal/filters/filter_fuzzy.go b/internal/filters/filter_fuzzy.go index 1dae8e2b2..d1dd7f876 100644 --- a/internal/filters/filter_fuzzy.go +++ b/internal/filters/filter_fuzzy.go @@ -27,7 +27,7 @@ func (filter *fuzzyFilter) init(name string, matcher *matcherT, rawText string, func (filter *fuzzyFilter) Match(alert *models.Alert, matches int) bool { if filter.IsValid { for _, val := range alert.Annotations { - if filter.Matcher.Compare(val, filter.Value) { + if filter.Matcher.Compare(val.Value, filter.Value) { filter.Hits++ return true } diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index e0dfb7515..9f39d8225 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -355,20 +355,32 @@ var tests = []filterTest{ filterTest{ Expression: "abc", IsValid: true, - Alert: models.Alert{Annotations: map[string]string{"key": "abc"}}, - IsMatch: true, + Alert: models.Alert{ + Annotations: models.Annotations{ + models.Annotation{Name: "key", Value: "abc"}, + }, + }, + IsMatch: true, }, filterTest{ Expression: "abc", IsValid: true, - Alert: models.Alert{Annotations: map[string]string{"key": "ccc abc"}}, - IsMatch: true, + Alert: models.Alert{ + Annotations: models.Annotations{ + models.Annotation{Name: "key", Value: "ccc abc"}, + }, + }, + IsMatch: true, }, filterTest{ Expression: "abc", IsValid: true, - Alert: models.Alert{Annotations: map[string]string{"abc": "zzz"}}, - IsMatch: false, + Alert: models.Alert{ + Annotations: models.Annotations{ + models.Annotation{Name: "abc", Value: "zzz"}, + }, + }, + IsMatch: false, }, filterTest{ Expression: "abc", diff --git a/internal/mapper/v04/alerts.go b/internal/mapper/v04/alerts.go index 2ba82b7f7..eb03db9e3 100644 --- a/internal/mapper/v04/alerts.go +++ b/internal/mapper/v04/alerts.go @@ -103,7 +103,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler } a := models.Alert{ Receiver: rcv.Name, - Annotations: a.Annotations, + Annotations: models.AnnotationsFromMap(a.Annotations), Labels: a.Labels, StartsAt: a.StartsAt, EndsAt: a.EndsAt, diff --git a/internal/mapper/v05/alerts.go b/internal/mapper/v05/alerts.go index 8a9089247..6c6b422a3 100644 --- a/internal/mapper/v05/alerts.go +++ b/internal/mapper/v05/alerts.go @@ -102,7 +102,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler } a := models.Alert{ Receiver: rcv.Name, - Annotations: a.Annotations, + Annotations: models.AnnotationsFromMap(a.Annotations), Labels: a.Labels, StartsAt: a.StartsAt, EndsAt: a.EndsAt, diff --git a/internal/mapper/v061/alerts.go b/internal/mapper/v061/alerts.go index 60283f359..32b4f8865 100644 --- a/internal/mapper/v061/alerts.go +++ b/internal/mapper/v061/alerts.go @@ -101,7 +101,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler } a := models.Alert{ Receiver: rcv.Name, - Annotations: a.Annotations, + Annotations: models.AnnotationsFromMap(a.Annotations), Labels: a.Labels, StartsAt: a.StartsAt, EndsAt: a.EndsAt, diff --git a/internal/mapper/v062/alerts.go b/internal/mapper/v062/alerts.go index a3831397f..af0337fcf 100644 --- a/internal/mapper/v062/alerts.go +++ b/internal/mapper/v062/alerts.go @@ -105,7 +105,7 @@ func (m AlertMapper) GetAlerts(uri string, timeout time.Duration) ([]models.Aler } a := models.Alert{ Receiver: rcv.Name, - Annotations: a.Annotations, + Annotations: models.AnnotationsFromMap(a.Annotations), Labels: a.Labels, StartsAt: a.StartsAt, EndsAt: a.EndsAt, diff --git a/internal/models/alert.go b/internal/models/alert.go index 0d3573196..f4879f395 100644 --- a/internal/models/alert.go +++ b/internal/models/alert.go @@ -30,7 +30,7 @@ var AlertStateList = []string{ // it's pulled out of annotation map and returned under links field, // unsee UI used this to show links differently than other annotations type Alert struct { - Annotations map[string]string `json:"annotations"` + Annotations Annotations `json:"annotations"` Labels map[string]string `json:"labels"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` @@ -44,7 +44,6 @@ type Alert struct { // unsee fields Alertmanager []AlertmanagerInstance `json:"alertmanager"` Receiver string `json:"receiver"` - Links map[string]string `json:"links"` // fingerprints are precomputed for speed labelsFP string `hash:"-"` contentFP string `hash:"-"` diff --git a/internal/models/alert_test.go b/internal/models/alert_test.go index 4cac8c5dc..cbc8817ad 100644 --- a/internal/models/alert_test.go +++ b/internal/models/alert_test.go @@ -72,9 +72,17 @@ func BenchmarkLabelsFingerprint(b *testing.B) { func BenchmarkLabelsContent(b *testing.B) { alert := models.Alert{ - Annotations: map[string]string{ - "foo": "bar", - "abc": "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...", + Annotations: models.Annotations{ + models.Annotation{ + Name: "foo", + Value: "bar", + Visible: true, + }, + models.Annotation{ + Name: "abc", + Value: "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...", + Visible: true, + }, }, Labels: map[string]string{ "foo1": "bar1", diff --git a/internal/models/annotation.go b/internal/models/annotation.go new file mode 100644 index 000000000..573a40536 --- /dev/null +++ b/internal/models/annotation.go @@ -0,0 +1,84 @@ +package models + +import ( + "net/url" + "sort" + + "github.com/cloudflare/unsee/internal/config" + "github.com/cloudflare/unsee/internal/slices" +) + +// Annotation extends Alertmanager scheme of key:value with additional data +// to control how given annotation should be rendered +type Annotation struct { + Name string `json:"name"` + Value string `json:"value"` + Visible bool `json:"visible"` + IsLink bool `json:"isLink"` +} + +// Annotations is a slice of Annotation structs, needed to implement sorting +type Annotations []Annotation + +func (a Annotations) Len() int { + return len(a) + +} +func (a Annotations) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} +func (a Annotations) Less(i, j int) bool { + return a[i].Name < a[j].Name +} + +// AnnotationsFromMap will convert a map[string]string to a list of Annotation +// instances, it takes care of setting proper value for Visible attribute +func AnnotationsFromMap(m map[string]string) Annotations { + annotations := Annotations{} + for name, value := range m { + a := Annotation{ + Name: name, + Value: value, + Visible: isVisible(name), + IsLink: isLink(value), + } + annotations = append(annotations, a) + } + sort.Sort(annotations) + return annotations +} + +var linkSchemes = []string{ + "ftp", + "http", + "https", +} + +func isLink(s string) bool { + u, err := url.Parse(s) + if err != nil { + return false + } + if slices.StringInSlice(linkSchemes, u.Scheme) { + // parses with url.Parse and scheme is in the list of supported schemes + return true + } + return false +} + +func isVisible(name string) bool { + if slices.StringInSlice(config.Config.AnnotationsVisible, name) { + // annotation was explicitly marked as visible + return true + } + if slices.StringInSlice(config.Config.AnnotationsHidden, name) { + // annotation was explicitly marked as hidden + return false + } + if config.Config.AnnotationsDefaultHidden { + // user specified that default is to hide anything without explicit rules + return false + } + // default to show everything + return true +} diff --git a/internal/models/annotation_test.go b/internal/models/annotation_test.go new file mode 100644 index 000000000..3b7f01d83 --- /dev/null +++ b/internal/models/annotation_test.go @@ -0,0 +1,85 @@ +package models_test + +import ( + "reflect" + "testing" + + "github.com/cloudflare/unsee/internal/models" +) + +type annotationMapsTestCase struct { + annotationMap map[string]string + annotations models.Annotations +} + +var annotationMapsTestCases = []annotationMapsTestCase{ + annotationMapsTestCase{ + annotationMap: map[string]string{ + "foo": "bar", + }, + annotations: models.Annotations{ + models.Annotation{ + Name: "foo", + Value: "bar", + Visible: true, + IsLink: false, + }, + }, + }, + annotationMapsTestCase{ + annotationMap: map[string]string{ + "foo": "http://localhost", + }, + annotations: models.Annotations{ + models.Annotation{ + Name: "foo", + Value: "http://localhost", + Visible: true, + IsLink: true, + }, + }, + }, + annotationMapsTestCase{ + annotationMap: map[string]string{ + "foo": "ftp://localhost", + }, + annotations: models.Annotations{ + models.Annotation{ + Name: "foo", + Value: "ftp://localhost", + Visible: true, + IsLink: true, + }, + }, + }, + annotationMapsTestCase{ + annotationMap: map[string]string{ + "foo": "https://localhost/xxx", + "abc": "xyz", + }, + annotations: models.Annotations{ + models.Annotation{ + Name: "abc", + Value: "xyz", + Visible: true, + IsLink: false, + }, + models.Annotation{ + Name: "foo", + Value: "https://localhost/xxx", + Visible: true, + IsLink: true, + }, + }, + }, +} + +func TestAnnotationsFromMap(t *testing.T) { + for _, testCase := range annotationMapsTestCases { + result := models.AnnotationsFromMap(testCase.annotationMap) + if !reflect.DeepEqual(testCase.annotations, result) { + t.Errorf("AnnotationsFromMap result mismatch for map %v, expected %v got %v", + testCase.annotationMap, testCase.annotations, result) + } + } +} diff --git a/internal/transform/links.go b/internal/transform/links.go deleted file mode 100644 index 2fd9deb71..000000000 --- a/internal/transform/links.go +++ /dev/null @@ -1,35 +0,0 @@ -package transform - -import ( - "net/url" - - "github.com/cloudflare/unsee/internal/slices" -) - -// list of URI schema which we turn into links in the UI -var schemes = []string{ - "ftp", - "http", - "https", -} - -// DetectLinks takes alert annotation dict and returns two dicts: -// first with regular annotations -// secondd with annotations where values are URLs -func DetectLinks(sourceAnnotations map[string]string) (map[string]string, map[string]string) { - links := make(map[string]string) - annotations := make(map[string]string) - - for k, v := range sourceAnnotations { - u, err := url.Parse(v) - if err != nil { - annotations[k] = v - } else if slices.StringInSlice(schemes, u.Scheme) { - links[k] = v - } else { - annotations[k] = v - } - } - - return annotations, links -} diff --git a/internal/transform/links_test.go b/internal/transform/links_test.go deleted file mode 100644 index 77b92dca2..000000000 --- a/internal/transform/links_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package transform_test - -import ( - "reflect" - "testing" - - "github.com/cloudflare/unsee/internal/transform" -) - -type linkTest struct { - before map[string]string - after map[string]string - links map[string]string -} - -var linkTests = []linkTest{ - linkTest{ - before: map[string]string{}, - after: map[string]string{}, - links: map[string]string{}, - }, - linkTest{ - before: map[string]string{ - "key1": "value 1", - "key2": "value2", - "level": "info", - }, - after: map[string]string{ - "key1": "value 1", - "key2": "value2", - "level": "info", - }, - links: map[string]string{}, - }, - linkTest{ - before: map[string]string{ - "key1": "value 1", - "key2": "http://localhost", - "level": "info", - }, - after: map[string]string{ - "key1": "value 1", - "level": "info", - }, - links: map[string]string{ - "key2": "http://localhost", - }, - }, - linkTest{ - before: map[string]string{ - "key1": "value 1", - "key2": "https://example.com/abc", - "level": "info", - }, - after: map[string]string{ - "key1": "value 1", - "level": "info", - }, - links: map[string]string{ - "key2": "https://example.com/abc", - }, - }, - linkTest{ - before: map[string]string{ - "key1": "value 1", - "key2": "file://example/abc", - "level": "info", - }, - after: map[string]string{ - "key1": "value 1", - "key2": "file://example/abc", - "level": "info", - }, - links: map[string]string{}, - }, - linkTest{ - before: map[string]string{ - "key1": "value 1", - "key2": "ftp://example/abc", - "level": "info", - }, - after: map[string]string{ - "key1": "value 1", - "level": "info", - }, - links: map[string]string{ - "key2": "ftp://example/abc", - }, - }, -} - -func TestDetectLinks(t *testing.T) { - for _, testCase := range linkTests { - after, links := transform.DetectLinks(testCase.before) - if !reflect.DeepEqual(after, testCase.after) { - t.Errorf("DetectLinks returned invalid annotation map, expected %v, got %v", testCase.after, after) - } - if !reflect.DeepEqual(links, testCase.links) { - t.Errorf("DetectLinks returned invalid link map, expected %v, got %v", testCase.links, links) - } - } -} diff --git a/views_test.go b/views_test.go index fb1faed04..95832adb5 100644 --- a/views_test.go +++ b/views_test.go @@ -163,8 +163,14 @@ func TestAlerts(t *testing.T) { } for _, ag := range ur.AlertGroups { for _, a := range ag.Alerts { - if len(a.Links) != 1 { - t.Errorf("Invalid number of links, got %d, expected 1, %v", len(a.Links), a) + linkCount := 0 + for _, annotation := range a.Annotations { + if annotation.IsLink { + linkCount++ + } + } + if linkCount != 1 { + t.Errorf("Invalid number of links, got %d, expected 1, %v", linkCount, a) } if len(a.Alertmanager) == 0 { t.Errorf("Alertmanager instance list is empty, %v", a)