feat(api): expose all silences under /silences.json

This commit is contained in:
Łukasz Mierzwa
2019-10-11 21:27:51 +01:00
parent f1e1705eca
commit 22ea4393ab
6 changed files with 144 additions and 0 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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))
}
}
}

1
go.mod
View File

@@ -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

View File

@@ -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 {

View File

@@ -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"`
}