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/demo/generator.py b/demo/generator.py
index 4ed5bb1e9..5a32ae377 100755
--- a/demo/generator.py
+++ b/demo/generator.py
@@ -263,7 +263,7 @@ def silences(self):
now = datetime.datetime.utcnow().replace(microsecond=0)
return [
(
- [newMatcher("alertname", SilencedAlert.name, False)],
+ [newMatcher("alertname", self.name, False)],
"{}Z".format(now.isoformat()),
"{}Z".format((now + datetime.timedelta(
minutes=30)).isoformat()),
@@ -300,7 +300,7 @@ def silences(self):
now = datetime.datetime.utcnow().replace(microsecond=0)
return [
(
- [newMatcher("alertname", MixedAlerts.name, False),
+ [newMatcher("alertname", self.name, False),
newMatcher("instance", "server(1|3|5|7)", True)],
"{}Z".format(now.isoformat()),
"{}Z".format((now + datetime.timedelta(
@@ -387,8 +387,7 @@ def silences(self):
now = datetime.datetime.utcnow().replace(microsecond=0)
return [
(
- [newMatcher(
- "alertname", SilencedAlertWithJiraLink.name, False)],
+ [newMatcher("alertname", self.name, False)],
"{}Z".format(now.isoformat()),
"{}Z".format((now + datetime.timedelta(
minutes=30)).isoformat()),
@@ -412,6 +411,28 @@ def alerts(self):
) for i in xrange(0, 1000)
]
+ def silences(self):
+ now = datetime.datetime.utcnow().replace(microsecond=0)
+ return [
+ (
+ [
+ newMatcher("alertname", self.name, False),
+ newMatcher("instance", "server{}".format(i), True)
+ ],
+ "{}Z".format(now.isoformat()),
+ "{}Z".format((now + datetime.timedelta(
+ minutes=10)).isoformat()),
+ "me@example.com",
+ "DEVOPS-123 Pagination Test alert silenced with a long text "
+ "to see if it gets truncated properly. It only matches first "
+ "20 alerts. "
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed "
+ "do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ " Ut enim ad minim veniam, quis nostrud exercitation ullamco "
+ "laboris nisi ut aliquip ex ea commodo consequat. "
+ ) for i in xrange(0, 20)
+ ]
+
if __name__ == "__main__":
generators = [
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/alertmanager/dedup_test.go b/internal/alertmanager/dedup_test.go
index 77a9f470e..91145d49d 100644
--- a/internal/alertmanager/dedup_test.go
+++ b/internal/alertmanager/dedup_test.go
@@ -90,6 +90,19 @@ func TestDedupAlertsWithoutLabels(t *testing.T) {
}
}
+func TestDedupSilences(t *testing.T) {
+ os.Setenv("ALERTMANAGER_URI", "http://localhost")
+ config.Config.Read()
+ if err := pullAlerts(); err != nil {
+ t.Error(err)
+ }
+ silences := alertmanager.DedupSilences()
+ expected := 72
+ if len(silences) != expected {
+ t.Errorf("Expected %d silences keys, got %d", expected, len(silences))
+ }
+}
+
func TestDedupAutocomplete(t *testing.T) {
if err := pullAlerts(); err != nil {
t.Error(err)
@@ -121,6 +134,32 @@ func TestDedupColors(t *testing.T) {
}
}
+func TestDedupKnownLabels(t *testing.T) {
+ os.Setenv("ALERTMANAGER_URI", "http://localhost")
+ config.Config.Read()
+ if err := pullAlerts(); err != nil {
+ t.Error(err)
+ }
+ labels := alertmanager.DedupKnownLabels()
+ expected := 6
+ if len(labels) != expected {
+ t.Errorf("Expected %d knownLabels keys, got %d", expected, len(labels))
+ }
+}
+
+func TestDedupKnownLabelValues(t *testing.T) {
+ os.Setenv("ALERTMANAGER_URI", "http://localhost")
+ config.Config.Read()
+ if err := pullAlerts(); err != nil {
+ t.Error(err)
+ }
+ values := alertmanager.DedupKnownLabelValues("alertname")
+ expected := 4
+ if len(values) != expected {
+ t.Errorf("Expected %d knownLabelValues keys, got %d", expected, len(values))
+ }
+}
+
func TestStripReceivers(t *testing.T) {
os.Setenv("RECEIVERS_STRIP", "by-name by-cluster-service")
os.Setenv("ALERTMANAGER_URI", "http://localhost")
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"`
+}
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 8addab363..8dfd22ee1 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -8159,6 +8159,34 @@
"pend": "~1.2.0"
}
},
+ "fetch-mock": {
+ "version": "8.0.0-alpha.5",
+ "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-8.0.0-alpha.5.tgz",
+ "integrity": "sha512-/FShpzvtDt/535q+9un5Ve+GGNsCKX3AW9CQN5htZ2eLY11E7yM///SrXoK8QteETSnKbT2N0vyHHIiRcVi7/A==",
+ "dev": true,
+ "requires": {
+ "babel-runtime": "^6.26.0",
+ "core-js": "^3.0.0",
+ "glob-to-regexp": "^0.4.0",
+ "lodash.isequal": "^4.5.0",
+ "path-to-regexp": "^2.2.1",
+ "whatwg-url": "^6.5.0"
+ },
+ "dependencies": {
+ "glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz",
+ "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==",
+ "dev": true
+ }
+ }
+ },
"figgy-pudding": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index a86082606..fb82e43c8 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -82,6 +82,7 @@
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"eslint-plugin-prettier": "3.1.1",
+ "fetch-mock": "8.0.0-alpha.5",
"jest-canvas-mock": "2.1.2",
"jest-date-mock": "1.0.7",
"jest-fetch-mock": "2.1.2",
diff --git a/ui/src/App.scss b/ui/src/App.scss
index 7d0ac387d..3d47c6ddb 100644
--- a/ui/src/App.scss
+++ b/ui/src/App.scss
@@ -1,6 +1,8 @@
// bundled font assets, so we don't need to talk to Google Fonts API
@import "src/Fonts.scss";
+$lead-font-weight: 400;
+
// custom "dark" color, little less dark than flatly
$blue: #455a64;
// body background color should be same as navbar, so it blends into one
@@ -17,6 +19,15 @@ $dark: #3b4247;
// fix active tab color, for some reason it ends up with $primary as bg color
$nav-tabs-link-active-bg: #fff;
+// pagination tweaks
+$pagination-color: #fff;
+$pagination-bg: #b4bcc2; // gray-500
+$pagination-hover-color: #fff;
+$pagination-hover-bg: #7b8a8b; // gray-700
+$pagination-active-bg: $pagination-hover-bg;
+$pagination-disabled-color: #fff;
+$pagination-disabled-bg: #dee2e6; // gray-300
+
@import "~bootswatch/dist/flatly/variables";
@import "~bootstrap/scss/bootstrap";
@import "~bootswatch/dist/flatly/bootswatch";
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
index 9c9847b67..cd8e45c11 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
@@ -14,8 +14,8 @@ import { StaticLabels } from "Common/Query";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
-import { Silence } from "../Silence";
import { AlertMenu } from "./AlertMenu";
+import { RenderSilence } from "../Silences";
import "./index.scss";
@@ -132,16 +132,16 @@ const Alert = observer(
value={a.value}
/>
))}
- {Object.values(silences).map(clusterSilences =>
- clusterSilences.silences.map(silenceID => (
-