Files
karma/views.go
Łukasz Mierzwa 8a77d620fd Rename @status to @state
Fixes #104

@status filter was added to the master branch to support new status key from Alertmanager >=0.6.1
status ended up being nested in Alertmanager (it was added to solve AM issue 609 and that was a long PR with lots of changes), current unsee implementation ended being slightly off with how Alertmanager is naming this, it should actually be @state rather than @status.
2017-05-23 21:52:08 +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{},
StateCount: map[string]int{},
}
for _, s := range models.AlertStateList {
agCopy.StateCount[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, "@state", alert.State)
agCopy.StateCount[alert.State]++
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)
}