Files
karma/views.go
Łukasz Mierzwa 12521e25bc Use gray color for alert groups with only unprocessed alerts
Expose counts of all alert statuses in each group (active, suppressed, unprocessed) instead of only active, and if a group contains only unprocessed alerts then color it as gray
2017-05-10 10:58:19 +01:00

289 lines
7.2 KiB
Go

package main
import (
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
"github.com/cloudflare/unsee/store"
"github.com/cloudflare/unsee/transport"
log "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
)
var (
// needed for serving favicon from binary assets
faviconFileServer = http.FileServer(newBinaryFileSystem("static"))
)
func boolInSlice(boolArray []bool, value bool) bool {
for _, s := range boolArray {
if s == value {
return true
}
}
return false
}
func noCache(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
}
// index view, html
func index(c *gin.Context) {
start := time.Now()
cssFiles := readAssets("css")
jsFiles := readAssets("js")
noCache(c)
q, qPresent := c.GetQuery("q")
defaultUsed := true
if qPresent {
defaultUsed = false
}
silencesAPI, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/silences")
if err != nil {
log.Errorf("Can't generate silences API URL: %s", err)
}
c.HTML(http.StatusOK, "templates/index.html", gin.H{
"Version": version,
"SentryDSN": config.Config.SentryPublicDSN,
"CSSFiles": cssFiles,
"JSFiles": jsFiles,
"NowQ": start.Unix(),
"Config": config.Config,
"QFilter": q,
"DefaultUsed": defaultUsed,
"StaticColorLabels": strings.Join(config.Config.ColorLabelsStatic, " "),
"WebPrefix": config.Config.WebPrefix,
"SilencesApi": silencesAPI,
})
log.Infof("[%s] %s %s took %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI, time.Since(start))
}
// Help view, html
func help(c *gin.Context) {
start := time.Now()
cssFiles := readAssets("css")
noCache(c)
c.HTML(http.StatusOK, "templates/help.html", gin.H{
"CSSFiles": cssFiles,
"SentryDSN": config.Config.SentryPublicDSN,
"WebPrefix": config.Config.WebPrefix,
})
log.Infof("[%s] <%d> %s %s took %s", c.ClientIP(), http.StatusOK, c.Request.Method, c.Request.RequestURI, time.Since(start))
}
func logAlertsView(c *gin.Context, cacheStatus string, duration time.Duration) {
log.Infof("[%s %s] <%d> %s %s took %s", c.ClientIP(), cacheStatus, http.StatusOK, c.Request.Method, c.Request.RequestURI, duration)
}
// alerts endpoint, json, JS will query this via AJAX call
func alerts(c *gin.Context) {
noCache(c)
start := time.Now()
ts, _ := start.UTC().MarshalText()
// intialize response object, set fields that don't require any locking
resp := models.AlertsResponse{}
resp.Status = "success"
resp.Timestamp = string(ts)
resp.Version = version
// update error field, needs a lock
errorLock.RLock()
resp.Error = string(alertManagerError)
errorLock.RUnlock()
if resp.Error != "" {
apiCache.Flush()
}
// use full URI (including query args) as cache key
cacheKey := c.Request.RequestURI
data, found := apiCache.Get(cacheKey)
if found {
c.Data(http.StatusOK, gin.MIMEJSON, data.([]byte))
logAlertsView(c, "HIT", time.Since(start))
return
}
// get filters
apiFilters := []models.Filter{}
matchFilters, validFilters := getFiltersFromQuery(c.Query("q"))
// set pointers for data store objects, need a lock until end of view is reached
alerts := []models.AlertGroup{}
silences := map[string]models.Silence{}
colors := models.LabelsColorMap{}
counters := models.LabelsCountMap{}
store.Store.Lock.RLock()
var matches int
for _, ag := range store.Store.Alerts {
agCopy := models.AlertGroup{
ID: ag.ID,
Labels: ag.Labels,
Alerts: []models.Alert{},
StatusCount: map[string]int{},
}
for _, s := range models.AlertStateList {
agCopy.StatusCount[s] = 0
}
h := sha1.New()
for _, alert := range ag.Alerts {
results := []bool{}
if validFilters {
for _, filter := range matchFilters {
if filter.GetIsValid() {
match := filter.Match(&alert, matches)
results = append(results, match)
}
}
}
if !validFilters || (boolInSlice(results, true) && !boolInSlice(results, false)) {
matches++
agCopy.Alerts = append(agCopy.Alerts, alert)
aj, err := json.Marshal(alert)
if err != nil {
log.Error(err.Error())
panic(err.Error())
}
io.WriteString(h, string(aj))
if alert.IsSilenced() {
for _, silenceID := range alert.SilencedBy {
if silence := store.Store.GetSilence(silenceID); silence != nil {
silences[silenceID] = *silence
}
}
}
countLabel(counters, "@status", alert.Status)
agCopy.StatusCount[alert.Status]++
for key, value := range alert.Labels {
if keyMap, foundKey := store.Store.Colors[key]; foundKey {
if color, foundColor := keyMap[value]; foundColor {
if _, found := colors[key]; !found {
colors[key] = map[string]models.LabelColors{}
}
colors[key][value] = color
}
}
countLabel(counters, key, value)
}
}
}
if len(agCopy.Alerts) > 0 {
agCopy.Hash = fmt.Sprintf("%x", h.Sum(nil))
alerts = append(alerts, agCopy)
}
}
resp.AlertGroups = alerts
resp.Silences = silences
resp.Colors = colors
resp.Counters = counters
for _, filter := range matchFilters {
af := models.Filter{
Text: filter.GetRawText(),
Hits: filter.GetHits(),
IsValid: filter.GetIsValid(),
}
apiFilters = append(apiFilters, af)
}
resp.Filters = apiFilters
data, err := json.Marshal(resp)
if err != nil {
log.Error(err.Error())
panic(err)
}
apiCache.Set(cacheKey, data, -1)
store.Store.Lock.RUnlock()
c.Data(http.StatusOK, gin.MIMEJSON, data.([]byte))
logAlertsView(c, "MIS", time.Since(start))
}
// autocomplete endpoint, json, used for filter autocomplete hints
func autocomplete(c *gin.Context) {
noCache(c)
start := time.Now()
cacheKey := c.Request.RequestURI
if cacheKey == "" {
// FIXME c.Request.RequestURI is empty when running tests for some reason
// needs checking, below acts as a workaround
cacheKey = c.Request.URL.RawQuery
}
data, found := apiCache.Get(cacheKey)
if found {
c.Data(http.StatusOK, gin.MIMEJSON, data.([]byte))
logAlertsView(c, "HIT", time.Since(start))
return
}
term, found := c.GetQuery("term")
if !found || term == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing term=<token> parameter"})
log.Infof("[%s] <%d> %s %s took %s", c.ClientIP(), http.StatusBadRequest, c.Request.Method, c.Request.RequestURI, time.Since(start))
return
}
acData := []string{}
store.Store.Lock.RLock()
for _, hint := range store.Store.Autocomplete {
if strings.HasPrefix(strings.ToLower(hint.Value), strings.ToLower(term)) {
acData = append(acData, hint.Value)
} else {
for _, token := range hint.Tokens {
if strings.HasPrefix(strings.ToLower(token), strings.ToLower(term)) {
acData = append(acData, hint.Value)
}
}
}
}
store.Store.Lock.RUnlock()
sort.Strings(acData)
data, err := json.Marshal(acData)
if err != nil {
log.Error(err.Error())
panic(err)
}
apiCache.Set(cacheKey, data, time.Second*15)
c.Data(http.StatusOK, gin.MIMEJSON, data.([]byte))
logAlertsView(c, "MIS", time.Since(start))
}
func favicon(c *gin.Context) {
if config.Config.WebPrefix != "/" {
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, config.Config.WebPrefix)
}
faviconFileServer.ServeHTTP(c.Writer, c.Request)
}