mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
httptest.newRequest is suppose to be used for testing request handing, http.newRequest is for preparing outgoing requests, this allows to fix cache key selection and increase test coverage
268 lines
7.2 KiB
Go
268 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prymitive/unsee/internal/alertmanager"
|
|
"github.com/prymitive/unsee/internal/config"
|
|
"github.com/prymitive/unsee/internal/models"
|
|
"github.com/prymitive/unsee/internal/slices"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func notFound(c *gin.Context) {
|
|
c.String(404, "404 page not found")
|
|
}
|
|
|
|
func noCache(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
}
|
|
|
|
func index(c *gin.Context) {
|
|
start := time.Now()
|
|
|
|
noCache(c)
|
|
|
|
filtersJSON, err := json.Marshal(config.Config.Filters.Default)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
filtersB64 := base64.StdEncoding.EncodeToString(filtersJSON)
|
|
|
|
c.HTML(http.StatusOK, "ui/build/index.html", gin.H{
|
|
"Version": version,
|
|
"SentryDSN": config.Config.Sentry.Public,
|
|
"DefaultFilter": filtersB64,
|
|
})
|
|
|
|
log.Infof("[%s] %s %s took %s", c.ClientIP(), 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
|
|
resp.Upstreams = getUpstreams()
|
|
resp.Settings = models.Settings{
|
|
StaticColorLabels: config.Config.Labels.Color.Static,
|
|
AnnotationsDefaultHidden: config.Config.Annotations.Default.Hidden,
|
|
AnnotationsHidden: config.Config.Annotations.Hidden,
|
|
AnnotationsVisible: config.Config.Annotations.Visible,
|
|
}
|
|
|
|
// 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.QueryArray("q"))
|
|
|
|
// set pointers for data store objects, need a lock until end of view is reached
|
|
alerts := map[string]models.APIAlertGroup{}
|
|
colors := models.LabelsColorMap{}
|
|
// used for top labels dropdown
|
|
counters := models.LabelsCountMap{}
|
|
|
|
dedupedAlerts := alertmanager.DedupAlerts()
|
|
dedupedColors := alertmanager.DedupColors()
|
|
|
|
silences := map[string]map[string]models.Silence{}
|
|
for _, am := range alertmanager.GetAlertmanagers() {
|
|
silences[am.Name] = map[string]models.Silence{}
|
|
}
|
|
|
|
var matches int
|
|
for _, ag := range dedupedAlerts {
|
|
agCopy := models.AlertGroup{
|
|
ID: ag.ID,
|
|
Receiver: ag.Receiver,
|
|
Labels: ag.Labels,
|
|
Alerts: []models.Alert{},
|
|
AlertmanagerCount: map[string]int{},
|
|
StateCount: map[string]int{},
|
|
}
|
|
for _, s := range models.AlertStateList {
|
|
agCopy.StateCount[s] = 0
|
|
}
|
|
|
|
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 || (slices.BoolInSlice(results, true) && !slices.BoolInSlice(results, false)) {
|
|
matches++
|
|
// we need to update fingerprints since we've modified some fields in dedup
|
|
// and agCopy.ContentFingerprint() depends on per alert fingerprint
|
|
// we update it here rather than in dedup since here we can apply it
|
|
// only for alerts left after filtering
|
|
alert.UpdateFingerprints()
|
|
agCopy.Alerts = append(agCopy.Alerts, alert)
|
|
|
|
countLabel(counters, "@state", alert.State)
|
|
|
|
countLabel(counters, "@receiver", alert.Receiver)
|
|
if ck, foundKey := dedupedColors["@receiver"]; foundKey {
|
|
if cv, foundVal := ck[alert.Receiver]; foundVal {
|
|
if _, found := colors["@receiver"]; !found {
|
|
colors["@receiver"] = map[string]models.LabelColors{}
|
|
}
|
|
colors["@receiver"][alert.Receiver] = cv
|
|
}
|
|
}
|
|
|
|
agCopy.StateCount[alert.State]++
|
|
|
|
for _, am := range alert.Alertmanager {
|
|
if _, found := agCopy.AlertmanagerCount[am.Name]; !found {
|
|
agCopy.AlertmanagerCount[am.Name] = 1
|
|
} else {
|
|
agCopy.AlertmanagerCount[am.Name]++
|
|
}
|
|
}
|
|
|
|
for key, value := range alert.Labels {
|
|
if keyMap, foundKey := dedupedColors[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 {
|
|
for _, alert := range agCopy.Alerts {
|
|
if alert.IsSilenced() {
|
|
for _, am := range alert.Alertmanager {
|
|
for _, silence := range am.Silences {
|
|
silences[am.Name][silence.ID] = *silence
|
|
}
|
|
}
|
|
}
|
|
}
|
|
agCopy.Hash = agCopy.ContentFingerprint()
|
|
apiAG := models.APIAlertGroup{AlertGroup: agCopy}
|
|
apiAG.DedupSharedMaps()
|
|
alerts[agCopy.ID] = apiAG
|
|
resp.TotalAlerts += len(agCopy.Alerts)
|
|
}
|
|
|
|
}
|
|
|
|
resp.AlertGroups = alerts
|
|
resp.Silences = silences
|
|
resp.Colors = colors
|
|
resp.Counters = counters
|
|
|
|
for _, filter := range matchFilters {
|
|
af := models.Filter{
|
|
Text: filter.GetRawText(),
|
|
Name: filter.GetName(),
|
|
Matcher: filter.GetMatcher(),
|
|
Value: filter.GetValue(),
|
|
Hits: filter.GetHits(),
|
|
IsValid: filter.GetIsValid(),
|
|
}
|
|
if af.Text != "" {
|
|
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)
|
|
|
|
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
|
|
|
|
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 := sort.StringSlice{}
|
|
|
|
dedupedAutocomplete := alertmanager.DedupAutocomplete()
|
|
|
|
for _, hint := range dedupedAutocomplete {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(sort.Reverse(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))
|
|
}
|