From 24af6616361a20c59c366b28c51ba381aa000ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 12:40:53 -0700 Subject: [PATCH 1/9] Add config options for controlling annotations visibility --- internal/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 9a503448b..ecf20783d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,8 @@ 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)"` + AnnotationsHidden spaceSeparatedList `envconfig:"ANNOTATIONS_HIDDEN" help:"List of annotations that are hidden by default"` + 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"` From 54b14552762304135b17176870a957af3c3940d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 14:19:03 -0700 Subject: [PATCH 2/9] Use dedicated struct for storing alert annotations Annotations are just string maps, but to render those we need a bit more information (is it a link, should it be visible by default). Store them as more complex structs internally, this also allows us to drop alert.Links since we now have enough information to tell if annotation is a link --- internal/alertmanager/models.go | 2 - internal/filters/filter_fuzzy.go | 2 +- internal/mapper/v04/alerts.go | 2 +- internal/mapper/v05/alerts.go | 2 +- internal/mapper/v061/alerts.go | 2 +- internal/mapper/v062/alerts.go | 2 +- internal/models/alert.go | 3 +- internal/models/annotation.go | 66 ++++++++++++++++++++ internal/transform/links.go | 35 ----------- internal/transform/links_test.go | 102 ------------------------------- 10 files changed, 72 insertions(+), 146 deletions(-) create mode 100644 internal/models/annotation.go delete mode 100644 internal/transform/links.go delete mode 100644 internal/transform/links_test.go 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/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/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/annotation.go b/internal/models/annotation.go new file mode 100644 index 000000000..fc3d0e79a --- /dev/null +++ b/internal/models/annotation.go @@ -0,0 +1,66 @@ +package models + +import ( + "net/url" + "sort" + + "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 key, value := range m { + a := Annotation{ + Name: key, + Value: value, + Visible: true, // FIXME needs implementing + 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 +} 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) - } - } -} From 21a8091a702127f5ce3c2d2afba686b88ab50d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 14:19:53 -0700 Subject: [PATCH 3/9] Handle complex annotations correctly in the frontend template --- assets/templates/alertgroup.html | 46 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/assets/templates/alertgroup.html b/assets/templates/alertgroup.html index b524d00c6..e7e31bcc2 100644 --- a/assets/templates/alertgroup.html +++ b/assets/templates/alertgroup.html @@ -27,28 +27,32 @@ From aa606eff12d2fc55628146c3caf152eb281c6f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 14:20:18 -0700 Subject: [PATCH 4/9] Migrate tests to new annotation scheme --- api_test.go | 152 ++++++++++++-------------------- internal/filters/filter_test.go | 24 +++-- internal/models/alert_test.go | 14 ++- views_test.go | 10 ++- 4 files changed, 95 insertions(+), 105 deletions(-) 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/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/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/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) From 843eb800602be8cbc432382d19d33c5872b7a2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 18:25:52 -0700 Subject: [PATCH 5/9] Add button to toggle annotation visibility If user make annotation hidden by default, then render a button that allows to show it per label --- assets/static/alerts.js | 1 + assets/static/templates.js | 3 ++- assets/static/ui.js | 22 ++++++++++++++++++++++ assets/templates/alertgroup.html | 31 +++++++++++++++++++++---------- 4 files changed, 46 insertions(+), 11 deletions(-) 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..c12a2a2b5 100644 --- a/assets/static/ui.js +++ b/assets/static/ui.js @@ -75,6 +75,28 @@ 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"); + }); + } + }); +} + 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 e7e31bcc2..a5a0faa1b 100644 --- a/assets/templates/alertgroup.html +++ b/assets/templates/alertgroup.html @@ -27,20 +27,20 @@ + + From ab25daf6c9953049832afee610545c00eb727a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 18:44:59 -0700 Subject: [PATCH 6/9] Implement logic for setting annotation visibility This allows to signal to the UI which annotations to hide and which to show by default, user still can view hidden ones --- internal/config/config.go | 33 +++++++++++++++++---------------- internal/models/annotation.go | 24 +++++++++++++++++++++--- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index ecf20783d..720e84186 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,22 +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)"` - AnnotationsHidden spaceSeparatedList `envconfig:"ANNOTATIONS_HIDDEN" help:"List of annotations that are hidden by default"` - 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"` + 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/models/annotation.go b/internal/models/annotation.go index fc3d0e79a..573a40536 100644 --- a/internal/models/annotation.go +++ b/internal/models/annotation.go @@ -4,6 +4,7 @@ import ( "net/url" "sort" + "github.com/cloudflare/unsee/internal/config" "github.com/cloudflare/unsee/internal/slices" ) @@ -34,11 +35,11 @@ func (a Annotations) Less(i, j int) bool { // instances, it takes care of setting proper value for Visible attribute func AnnotationsFromMap(m map[string]string) Annotations { annotations := Annotations{} - for key, value := range m { + for name, value := range m { a := Annotation{ - Name: key, + Name: name, Value: value, - Visible: true, // FIXME needs implementing + Visible: isVisible(name), IsLink: isLink(value), } annotations = append(annotations, a) @@ -64,3 +65,20 @@ func isLink(s string) bool { } 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 +} From dca9fba4606b7db0eb8468605990bffa7af5e81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 19:00:50 -0700 Subject: [PATCH 7/9] Document new options for controlling annotation visibility --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) 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 From 40604453eeda005cc9885783b9dc97d047502a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 19:20:27 -0700 Subject: [PATCH 8/9] Add tests for annotations --- internal/models/annotation_test.go | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/models/annotation_test.go 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) + } + } +} From 5d0366a743e7c08d06fe6aff16429b94767e7fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 19 Aug 2017 20:15:58 -0700 Subject: [PATCH 9/9] Redraw alert grid after annotations visibility toggle Alert group needs more space after expanding, so we need to redraw everything. --- assets/static/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/static/ui.js b/assets/static/ui.js index c12a2a2b5..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"); @@ -93,6 +94,7 @@ function setupGroupAnnotationToggles(groupElem) { $(annotation).removeClass("hidden"); }); } + grid.redraw(); }); }