From 22ea4393abf88da5dd4b31e4513a01c4ec5114df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 11 Oct 2019 21:27:51 +0100 Subject: [PATCH] feat(api): expose all silences under /silences.json --- cmd/karma/main.go | 1 + cmd/karma/views.go | 76 ++++++++++++++++++++++++++++++++++ cmd/karma/views_test.go | 24 +++++++++++ go.mod | 1 + internal/alertmanager/dedup.go | 35 ++++++++++++++++ internal/models/silence.go | 7 ++++ 6 files changed, 144 insertions(+) diff --git a/cmd/karma/main.go b/cmd/karma/main.go index b7bd9482e..2906cd84a 100644 --- a/cmd/karma/main.go +++ b/cmd/karma/main.go @@ -101,6 +101,7 @@ func setupRouter(router *gin.Engine) { router.GET(getViewURL("/autocomplete.json"), autocomplete) router.GET(getViewURL("/labelNames.json"), knownLabelNames) router.GET(getViewURL("/labelValues.json"), knownLabelValues) + router.GET(getViewURL("/silences.json"), silences) router.GET(getViewURL("/custom.css"), customCSS) router.GET(getViewURL("/custom.js"), customJS) diff --git a/cmd/karma/views.go b/cmd/karma/views.go index 12fa28b61..9592e73f0 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -409,3 +409,79 @@ func autocomplete(c *gin.Context) { c.Data(http.StatusOK, gin.MIMEJSON, data.([]byte)) logAlertsView(c, "MIS", time.Since(start)) } + +func silences(c *gin.Context) { + noCache(c) + + dedupedSilences := []models.ManagedSilence{} + + showExpired := false + showExpiredValue, found := c.GetQuery("showExpired") + if found && showExpiredValue == "1" { + showExpired = true + } + + searchTerm := "" + searchTermValue, found := c.GetQuery("searchTerm") + if found && searchTermValue != "" { + searchTerm = strings.ToLower(searchTermValue) + } + + for _, silence := range alertmanager.DedupSilences() { + if silence.IsExpired && !showExpired { + continue + } + if searchTerm != "" { + isMatch := false + if strings.Contains(strings.ToLower(silence.Silence.Comment), searchTerm) { + isMatch = true + } else if strings.Contains(strings.ToLower(silence.Silence.CreatedBy), searchTerm) { + isMatch = true + } else { + for _, match := range silence.Silence.Matchers { + eq := "=" + if match.IsRegex { + eq = "=~" + } + if searchTerm == fmt.Sprintf("%s%s\"%s\"", strings.ToLower(match.Name), eq, strings.ToLower(match.Value)) { + isMatch = true + } else if searchTerm == fmt.Sprintf("%s%s%s", match.Name, eq, match.Value) { + isMatch = true + } else if strings.Contains(strings.ToLower(match.Name), searchTerm) { + isMatch = true + } else if strings.Contains(strings.ToLower(match.Value), searchTerm) { + isMatch = true + } + } + } + if !isMatch { + continue + } + } + dedupedSilences = append(dedupedSilences, silence) + } + + recentFirst := true + sortReverse, found := c.GetQuery("sortReverse") + if found && sortReverse == "1" { + recentFirst = false + } + + sort.Slice(dedupedSilences, func(i int, j int) bool { + if dedupedSilences[i].Silence.EndsAt.Equal(dedupedSilences[j].Silence.EndsAt) { + if dedupedSilences[i].Silence.StartsAt.Equal(dedupedSilences[j].Silence.StartsAt) { + return dedupedSilences[i].Silence.ID < dedupedSilences[j].Silence.ID + } + return dedupedSilences[i].Silence.StartsAt.After(dedupedSilences[j].Silence.StartsAt) == recentFirst + } + return dedupedSilences[i].Silence.EndsAt.Before(dedupedSilences[j].Silence.EndsAt) == recentFirst + }) + + data, err := json.Marshal(dedupedSilences) + if err != nil { + log.Error(err.Error()) + panic(err) + } + + c.Data(http.StatusOK, gin.MIMEJSON, data) +} diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index 31500a324..ed0c4f22e 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -584,3 +584,27 @@ func TestValidateAuthorFromHeaders(t *testing.T) { } } } + +func TestSilences(t *testing.T) { + mockConfig() + for _, version := range mock.ListAllMocks() { + t.Logf("Validating silences.json response using mock files from Alertmanager %s", version) + mockAlerts(version) + r := ginTestEngine() + req := httptest.NewRequest("GET", "/silences.json?showExpired=1&sortReverse=1&searchTerm=a", nil) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != http.StatusOK { + t.Errorf("GET /silences.json returned status %d", resp.Code) + } + ur := []models.ManagedSilence{} + body := resp.Body.Bytes() + err := json.Unmarshal(body, &ur) + if err != nil { + t.Errorf("Failed to unmarshal response: %s", err) + } + if len(ur) != 3 { + t.Errorf("Incorrect number of silences: got %d, wanted 3", len(ur)) + } + } +} diff --git a/go.mod b/go.mod index 87f936c09..4eb8f0adf 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.4.0 github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect + golang.org/x/text v0.3.2 gopkg.in/go-playground/colors.v1 v1.2.0 gopkg.in/yaml.v2 v2.2.4 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index 42d306332..8c9affa76 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -2,6 +2,7 @@ package alertmanager import ( "sort" + "time" "github.com/prymitive/karma/internal/config" "github.com/prymitive/karma/internal/models" @@ -94,6 +95,40 @@ func DedupAlerts() []models.AlertGroup { return dedupedGroups } +// DedupKnownLabels returns a deduplicated slice of all known label names +func DedupSilences() []models.ManagedSilence { + silenceByCluster := map[string]map[string]models.Silence{} + upstreams := GetAlertmanagers() + + for _, am := range upstreams { + for id, silence := range am.Silences() { + cluster := am.ClusterID() + + if _, found := silenceByCluster[cluster]; !found { + silenceByCluster[cluster] = map[string]models.Silence{} + } + + if _, ok := silenceByCluster[cluster][id]; !ok { + silenceByCluster[cluster][id] = silence + } + } + } + + now := time.Now() + dedupedSilences := []models.ManagedSilence{} + for cluster, silenceMap := range silenceByCluster { + for _, silence := range silenceMap { + managedSilence := models.ManagedSilence{ + Cluster: cluster, + IsExpired: silence.EndsAt.Before(now), + Silence: silence, + } + dedupedSilences = append(dedupedSilences, managedSilence) + } + } + return dedupedSilences +} + // DedupColors returns a color map merged from all Alertmanager upstream color // maps func DedupColors() models.LabelsColorMap { diff --git a/internal/models/silence.go b/internal/models/silence.go index 4187d3dba..223507c6b 100644 --- a/internal/models/silence.go +++ b/internal/models/silence.go @@ -24,3 +24,10 @@ type Silence struct { JiraID string `json:"jiraID"` JiraURL string `json:"jiraURL"` } + +// ManagedSilence is a standalone silence detached from any alert +type ManagedSilence struct { + Cluster string `json:"cluster"` + IsExpired bool `json:"isExpired"` + Silence Silence `json:"silence"` +}