Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add UI for managing all silences #1078

Merged
merged 21 commits into from
Oct 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
22ea439
feat(api): expose all silences under /silences.json
prymitive Oct 11, 2019
87efb25
feat(ui): add UI for managing all silences
prymitive Oct 12, 2019
96cd8a8
chore(ui): replace Silence usage with ManagedSilence
prymitive Oct 26, 2019
f3572db
fix(ui): tweak silence display
prymitive Oct 26, 2019
55ebd3a
fix(ui): correctly wrap links in comments
prymitive Oct 26, 2019
5091021
feat(demo): generate more silences in demo mode
prymitive Oct 26, 2019
39aab2e
fix(ui): tweak lead font weight
prymitive Oct 26, 2019
6dc2ea9
feat(ui): paginate silence list
prymitive Oct 26, 2019
3dc859f
chore(ui): tweak pagination css
prymitive Oct 26, 2019
c0f4e4a
fix(ui): center text in silence submit result view
prymitive Oct 26, 2019
9e8a30a
chore(ui): better pagination handling for large lists
prymitive Oct 26, 2019
1fe5ced
fix(ui): workaround multiple modals conflicting with each other
prymitive Oct 26, 2019
166dd6e
feat(ci): mock silence fetches in storybook
prymitive Oct 26, 2019
5490cb3
fix(tests): only debounce user generated requests
prymitive Oct 26, 2019
10989b8
fix(backend): run go mod tidy
prymitive Oct 26, 2019
8a544e6
fix(tests): add more dedup tests
prymitive Oct 26, 2019
a9a9efd
fix(ui): smaller padding for modal tabs
prymitive Oct 27, 2019
392e39e
fix(ui): don't allow overflow on silence details
prymitive Oct 27, 2019
791219b
fix(ci): test long string wrapping on storybook
prymitive Oct 27, 2019
40eedc8
fix(ui): silence browser controls should switch from row to columns o…
prymitive Oct 27, 2019
490e0f4
fix(ui): avoid overflows on silence component
prymitive Oct 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/karma/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
76 changes: 76 additions & 0 deletions cmd/karma/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
24 changes: 24 additions & 0 deletions cmd/karma/views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
29 changes: 25 additions & 4 deletions demo/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()),
Expand All @@ -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 = [
Expand Down
35 changes: 35 additions & 0 deletions internal/alertmanager/dedup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package alertmanager

import (
"sort"
"time"

"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/models"
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions internal/alertmanager/dedup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions internal/models/silence.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
28 changes: 28 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions ui/src/App.scss
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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";
Expand Down
22 changes: 11 additions & 11 deletions ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -132,16 +132,16 @@ const Alert = observer(
value={a.value}
/>
))}
{Object.values(silences).map(clusterSilences =>
clusterSilences.silences.map(silenceID => (
<Silence
key={silenceID}
silenceFormStore={silenceFormStore}
alertmanagerState={clusterSilences.alertmanager}
silenceID={silenceID}
afterUpdate={afterUpdate}
/>
))
{Object.entries(silences).map(([cluster, clusterSilences]) =>
clusterSilences.silences.map(silenceID =>
RenderSilence(
alertStore,
silenceFormStore,
afterUpdate,
cluster,
silenceID
)
)
)}
</li>
);
Expand Down
Loading