mirror of
https://github.com/prymitive/karma
synced 2026-05-17 04:16:42 +00:00
820 lines
22 KiB
Go
820 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"runtime"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cnf/structhash"
|
|
"github.com/fvbommel/sortorder"
|
|
jsonv2 "github.com/go-json-experiment/json"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
promlabels "github.com/prometheus/prometheus/model/labels"
|
|
|
|
"github.com/prymitive/karma/internal/alertmanager"
|
|
"github.com/prymitive/karma/internal/config"
|
|
"github.com/prymitive/karma/internal/filters"
|
|
"github.com/prymitive/karma/internal/models"
|
|
sliceutils "github.com/prymitive/karma/internal/slices"
|
|
"github.com/prymitive/karma/internal/transform"
|
|
)
|
|
|
|
func marshalJSON(v any) ([]byte, error) {
|
|
return jsonv2.Marshal(v, jsontext.EscapeForHTML(true))
|
|
}
|
|
|
|
func noCache(w http.ResponseWriter) {
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
}
|
|
|
|
func mimeJSON(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
}
|
|
|
|
func badRequestJSON(w http.ResponseWriter, err string) {
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
out, _ := marshalJSON(map[string]string{"error": err})
|
|
_, _ = w.Write(out)
|
|
}
|
|
|
|
type KarmaVersion struct {
|
|
Version string `json:"version"`
|
|
Golang string `json:"golang"`
|
|
}
|
|
|
|
func versionHandler(w http.ResponseWriter, _ *http.Request) {
|
|
ver := KarmaVersion{
|
|
Version: version,
|
|
Golang: runtime.Version(),
|
|
}
|
|
data, _ := jsonv2.Marshal(ver, jsontext.Multiline(true), jsontext.WithIndent(" "))
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func pong(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("Pong\n"))
|
|
}
|
|
|
|
func robots(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("User-agent: *\nDisallow: /\n"))
|
|
}
|
|
|
|
func redirectIndex(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently)
|
|
}
|
|
|
|
func index(w http.ResponseWriter, _ *http.Request) {
|
|
noCache(w)
|
|
|
|
filtersJSON, _ := marshalJSON(config.Config.Filters.Default)
|
|
filtersB64 := base64.StdEncoding.EncodeToString(filtersJSON)
|
|
|
|
defaults, _ := marshalJSON(config.Config.UI)
|
|
defaultsB64 := base64.StdEncoding.EncodeToString(defaults)
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = indexTemplate.Execute(w, map[string]string{
|
|
"KarmaName": config.Config.Karma.Name,
|
|
"DefaultFilter": filtersB64,
|
|
"Defaults": defaultsB64,
|
|
"CustomCSS": config.Config.Custom.CSS,
|
|
"CustomJS": config.Config.Custom.JS,
|
|
})
|
|
}
|
|
|
|
func populateAPIFilters(matchFilters []filters.Filter) []models.Filter {
|
|
apiFilters := []models.Filter{}
|
|
for _, filter := range matchFilters {
|
|
af := models.Filter{
|
|
Text: filter.RawText(),
|
|
Name: filter.Name(),
|
|
Matcher: filter.MatcherOperation(),
|
|
Value: filter.Value(),
|
|
Hits: filter.Hits(),
|
|
IsValid: filter.Valid(),
|
|
}
|
|
if af.Text != "" {
|
|
apiFilters = append(apiFilters, af)
|
|
}
|
|
}
|
|
return apiFilters
|
|
}
|
|
|
|
// alerts endpoint, json, JS will query this via AJAX call
|
|
func alerts(w http.ResponseWriter, r *http.Request) {
|
|
noCache(w)
|
|
start := time.Now()
|
|
ts, _ := start.UTC().MarshalText()
|
|
|
|
var username string
|
|
var groups []string
|
|
if config.Config.Authentication.Enabled {
|
|
username = getUserFromContext(r)
|
|
groups = getGroupsFromContext(r)
|
|
}
|
|
|
|
upstreams := getUpstreams()
|
|
|
|
// initialize 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 = upstreams
|
|
resp.Settings = models.Settings{
|
|
Sorting: models.SortSettings{
|
|
Grid: models.GridSettings{
|
|
Order: config.Config.Grid.Sorting.Order,
|
|
Reverse: config.Config.Grid.Sorting.Reverse,
|
|
Label: config.Config.Grid.Sorting.Label,
|
|
},
|
|
ValueMapping: map[string]map[string]string{},
|
|
},
|
|
AnnotationsDefaultHidden: config.Config.Annotations.Default.Hidden,
|
|
AnnotationsHidden: config.Config.Annotations.Hidden,
|
|
AnnotationsVisible: config.Config.Annotations.Visible,
|
|
AnnotationsAllowHTML: config.Config.Annotations.EnableInsecureHTML,
|
|
SilenceForm: models.SilenceFormSettings{
|
|
Strip: models.SilenceFormStripSettings{
|
|
Labels: config.Config.SilenceForm.Strip.Labels,
|
|
},
|
|
DefaultAlertmanagers: config.Config.SilenceForm.DefaultAlertmanagers,
|
|
},
|
|
AlertAcknowledgement: models.AlertAcknowledgementSettings{
|
|
Enabled: config.Config.AlertAcknowledgement.Enabled,
|
|
DurationSeconds: int(config.Config.AlertAcknowledgement.Duration.Seconds()),
|
|
Author: config.Config.AlertAcknowledgement.Author,
|
|
Comment: config.Config.AlertAcknowledgement.Comment,
|
|
},
|
|
HistoryEnabled: config.Config.History.Enabled,
|
|
GridGroupLimit: config.Config.Grid.GroupLimit,
|
|
Labels: models.LabelsSettings{},
|
|
}
|
|
resp.Authentication = models.AuthenticationInfo{
|
|
Enabled: config.Config.Authentication.Enabled,
|
|
Username: username,
|
|
Groups: groups,
|
|
}
|
|
|
|
if config.Config.Grid.Sorting.CustomValues.Labels != nil {
|
|
resp.Settings.Sorting.ValueMapping = config.Config.Grid.Sorting.CustomValues.Labels
|
|
}
|
|
|
|
var request models.AlertsRequest
|
|
err := jsonv2.UnmarshalRead(r.Body, &request)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
cacheKey := hex.EncodeToString(structhash.Sha1(request, 1))
|
|
|
|
data, found := apiCache.Get(cacheKey)
|
|
if found {
|
|
// need to overwrite settings as they can have user specific data
|
|
newResp := models.AlertsResponse{}
|
|
_ = jsonv2.Unmarshal(data, &newResp)
|
|
labels := newResp.Settings.Labels
|
|
newResp.Settings = resp.Settings
|
|
newResp.Settings.Labels = labels
|
|
newResp.Timestamp = string(ts)
|
|
newResp.Authentication = resp.Authentication
|
|
data, _ = marshalJSON(&newResp)
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
return
|
|
}
|
|
|
|
grids := map[string]models.APIGrid{}
|
|
colors := models.LabelsColorMap{}
|
|
silences := map[string]map[string]models.Silence{}
|
|
allReceivers := map[string]struct{}{}
|
|
|
|
dedupedAlerts := alertmanager.DedupAlerts()
|
|
dedupedColors := alertmanager.DedupColors()
|
|
matchFilters := getFiltersFromQuery(request.Filters)
|
|
filtered := filterAlerts(dedupedAlerts, matchFilters)
|
|
|
|
gridLabel := request.GridLabel
|
|
if gridLabel == "@auto" {
|
|
gridLabel = autoGridLabel(filtered)
|
|
slog.Debug("Selected automatic grid label", slog.String("label", gridLabel))
|
|
}
|
|
|
|
var matches int
|
|
labelMap := map[string]struct{}{}
|
|
for _, ag := range filtered {
|
|
perGridAlertGroup := map[string]*models.AlertGroup{}
|
|
|
|
ag.Labels.Range(func(l promlabels.Label) {
|
|
labelMap[l.Name] = struct{}{}
|
|
})
|
|
for _, alert := range ag.Alerts {
|
|
alert.Labels.Range(func(l promlabels.Label) {
|
|
labelMap[l.Name] = struct{}{}
|
|
})
|
|
|
|
allReceivers[alert.Receiver] = struct{}{}
|
|
|
|
matches++
|
|
|
|
alertGridLabelValues := map[string]struct{}{}
|
|
switch gridLabel {
|
|
case "@receiver":
|
|
alertGridLabelValues[alert.Receiver] = struct{}{}
|
|
case "@alertmanager":
|
|
for _, am := range alert.Alertmanager {
|
|
alertGridLabelValues[am.Name] = struct{}{}
|
|
}
|
|
case "@cluster":
|
|
for _, am := range alert.Alertmanager {
|
|
alertGridLabelValues[am.Cluster] = struct{}{}
|
|
}
|
|
default:
|
|
alertGridLabelValues[alert.Labels.Get(gridLabel)] = struct{}{}
|
|
}
|
|
|
|
for alertGridLabelValue := range alertGridLabelValues {
|
|
// 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, found := perGridAlertGroup[alertGridLabelValue]
|
|
if !found {
|
|
agCopy = &models.AlertGroup{
|
|
ID: ag.ID,
|
|
Receiver: ag.Receiver,
|
|
Labels: ag.Labels,
|
|
LatestStartsAt: ag.LatestStartsAt,
|
|
Alerts: []models.Alert{},
|
|
AlertmanagerCount: map[string]int{},
|
|
StateCount: map[string]int{},
|
|
}
|
|
for _, s := range models.AlertStateList {
|
|
agCopy.StateCount[s.String()] = 0
|
|
}
|
|
perGridAlertGroup[alertGridLabelValue] = agCopy
|
|
}
|
|
|
|
stateCount := newStateCount()
|
|
switch gridLabel {
|
|
case "@alertmanager":
|
|
ams := []models.AlertmanagerInstance{}
|
|
for _, am := range alert.Alertmanager {
|
|
if am.Name == alertGridLabelValue {
|
|
ams = append(ams, am)
|
|
stateCount[am.State.String()]++
|
|
}
|
|
}
|
|
alert.Alertmanager = ams
|
|
case "@cluster":
|
|
ams := []models.AlertmanagerInstance{}
|
|
for _, am := range alert.Alertmanager {
|
|
if am.Cluster == alertGridLabelValue {
|
|
ams = append(ams, am)
|
|
stateCount[am.State.String()]++
|
|
}
|
|
}
|
|
alert.Alertmanager = ams
|
|
default:
|
|
for _, am := range alert.Alertmanager {
|
|
stateCount[am.State.String()]++
|
|
}
|
|
}
|
|
|
|
alert.State = stateFromStateCount(stateCount)
|
|
|
|
agCopy.Alerts = append(agCopy.Alerts, alert)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
if ck, foundKey := dedupedColors["@alertmanager"]; foundKey {
|
|
for _, am := range alert.Alertmanager {
|
|
if cv, foundVal := ck[am.Name]; foundVal {
|
|
if _, found := colors["@alertmanager"]; !found {
|
|
colors["@alertmanager"] = map[string]models.LabelColors{}
|
|
}
|
|
colors["@alertmanager"][am.Name] = cv
|
|
}
|
|
}
|
|
}
|
|
|
|
if ck, foundKey := dedupedColors["@cluster"]; foundKey {
|
|
for _, am := range alert.Alertmanager {
|
|
if cv, foundVal := ck[am.Cluster]; foundVal {
|
|
if _, found := colors["@cluster"]; !found {
|
|
colors["@cluster"] = map[string]models.LabelColors{}
|
|
}
|
|
colors["@cluster"][am.Cluster] = cv
|
|
}
|
|
}
|
|
}
|
|
|
|
agCopy.StateCount[alert.State.String()]++
|
|
|
|
for _, am := range alert.Alertmanager {
|
|
if _, found := agCopy.AlertmanagerCount[am.Name]; !found {
|
|
agCopy.AlertmanagerCount[am.Name] = 1
|
|
} else {
|
|
agCopy.AlertmanagerCount[am.Name]++
|
|
}
|
|
}
|
|
|
|
alert.Labels.Range(func(l promlabels.Label) {
|
|
if keyMap, foundKey := dedupedColors[l.Name]; foundKey {
|
|
if color, foundColor := keyMap[l.Value]; foundColor {
|
|
if _, found := colors[l.Name]; !found {
|
|
colors[l.Name] = map[string]models.LabelColors{}
|
|
}
|
|
colors[l.Name][l.Value] = color
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
for gridLabelValue, ag := range perGridAlertGroup {
|
|
if len(ag.Alerts) > 0 {
|
|
for _, alert := range ag.Alerts {
|
|
for _, am := range alert.Alertmanager {
|
|
for _, silence := range am.Silences {
|
|
if _, found := silences[am.Cluster][silence.ID]; !found {
|
|
if _, found := silences[am.Cluster]; !found {
|
|
silences[am.Cluster] = map[string]models.Silence{}
|
|
}
|
|
silences[am.Cluster][silence.ID] = *silence
|
|
}
|
|
}
|
|
}
|
|
}
|
|
slices.SortFunc(ag.Alerts, models.CompareAlerts)
|
|
ag.LatestStartsAt = ag.FindLatestStartsAt()
|
|
ag.Hash = ag.ContentFingerprint()
|
|
totalAlerts := len(ag.Alerts)
|
|
shared, allLabels := ag.DedupSharedMaps([]string{gridLabel})
|
|
resp.TotalAlerts += totalAlerts
|
|
|
|
alertLimit, found := request.GroupLimits[ag.ID]
|
|
if !found {
|
|
if request.DefaultGroupLimit > 0 {
|
|
alertLimit = request.DefaultGroupLimit
|
|
} else {
|
|
alertLimit = config.Config.UI.AlertsPerGroup
|
|
}
|
|
}
|
|
if alertLimit > totalAlerts {
|
|
alertLimit = totalAlerts
|
|
}
|
|
ag.Alerts = ag.Alerts[0:alertLimit]
|
|
apiAG := models.NewAPIAlertGroup(*ag, shared, allLabels, totalAlerts)
|
|
|
|
grid, found := grids[gridLabelValue]
|
|
if !found {
|
|
grid = models.APIGrid{
|
|
LabelName: gridLabel,
|
|
LabelValue: gridLabelValue,
|
|
AlertGroups: []models.APIAlertGroup{},
|
|
StateCount: map[string]int{},
|
|
}
|
|
for _, s := range models.AlertStateList {
|
|
grid.StateCount[s.String()] = 0
|
|
}
|
|
grids[gridLabelValue] = grid
|
|
}
|
|
grid.AlertGroups = append(grid.AlertGroups, apiAG)
|
|
for k, v := range apiAG.StateCount {
|
|
grid.StateCount[k] += v
|
|
}
|
|
grids[gridLabelValue] = grid
|
|
}
|
|
}
|
|
}
|
|
|
|
sortedGrids := sortGrids(request, gridLabel, grids, request.GridSortReverse)
|
|
for i := range sortedGrids {
|
|
sortedGrids[i].TotalGroups = len(sortedGrids[i].AlertGroups)
|
|
|
|
limit, found := request.GridLimits[sortedGrids[i].LabelValue]
|
|
if !found {
|
|
limit = config.Config.Grid.GroupLimit
|
|
}
|
|
|
|
l := min(limit, sortedGrids[i].TotalGroups)
|
|
if l < 1 {
|
|
l = 1
|
|
}
|
|
sortedGrids[i].AlertGroups = sortedGrids[i].AlertGroups[:l]
|
|
}
|
|
|
|
for _, filter := range matchFilters {
|
|
if filter.Value() != "" && filter.MatcherOperation() == "=" {
|
|
transform.ColorLabel(colors, filter.Name(), filter.Value())
|
|
labelSettings(filter.Name(), resp.Settings.Labels)
|
|
}
|
|
}
|
|
|
|
receivers := []string{}
|
|
for k := range allReceivers {
|
|
receivers = append(receivers, k)
|
|
}
|
|
sort.Strings(receivers)
|
|
|
|
resp.LabelNames = make([]string, 0, len(labelMap))
|
|
for label := range labelMap {
|
|
resp.LabelNames = append(resp.LabelNames, label)
|
|
}
|
|
sort.Strings(resp.LabelNames)
|
|
|
|
labelsSettings(sortedGrids, resp.Settings.Labels)
|
|
|
|
resp.Grids = sortedGrids
|
|
resp.Silences = silences
|
|
resp.Colors = colors
|
|
resp.Filters = populateAPIFilters(matchFilters)
|
|
resp.Receivers = receivers
|
|
|
|
data, _ = marshalJSON(resp)
|
|
_ = apiCache.Add(cacheKey, data)
|
|
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func labelsSettings(grids []models.APIGrid, store models.LabelsSettings) {
|
|
labelSettings("@alertmanager", store)
|
|
labelSettings("@cluster", store)
|
|
labelSettings("@receiver", store)
|
|
labelSettings("@state", store)
|
|
labelSettings("@inhibited", store)
|
|
labelSettings("@inhibited_by", store)
|
|
for _, grid := range grids {
|
|
labelSettings(grid.LabelName, store)
|
|
for _, ag := range grid.AlertGroups {
|
|
for _, l := range ag.Labels {
|
|
labelSettings(l.Name, store)
|
|
}
|
|
for _, alert := range ag.Alerts {
|
|
for _, l := range alert.Labels {
|
|
labelSettings(l.Name, store)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func labelSettings(name string, store models.LabelsSettings) {
|
|
var isStatic, isValueOnly bool
|
|
if slices.Contains(config.Config.Labels.Color.Static, name) {
|
|
isStatic = true
|
|
}
|
|
if slices.Contains(config.Config.Labels.ValueOnly, name) || sliceutils.MatchesAnyRegex(name, config.Config.Labels.CompiledValueOnlyRegex) {
|
|
isValueOnly = true
|
|
}
|
|
if isStatic || isValueOnly {
|
|
if _, ok := store[name]; !ok {
|
|
store[name] = models.LabelSettings{
|
|
IsStatic: isStatic,
|
|
IsValueOnly: isValueOnly,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// autocomplete endpoint, json, used for filter autocomplete hints
|
|
func autocomplete(w http.ResponseWriter, r *http.Request) {
|
|
noCache(w)
|
|
|
|
cacheKey := r.RequestURI
|
|
|
|
data, found := apiCache.Get(cacheKey)
|
|
if found {
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
return
|
|
}
|
|
|
|
term, found := lookupQueryString(r, "term")
|
|
if !found || term == "" {
|
|
badRequestJSON(w, "missing term=<token> parameter")
|
|
return
|
|
}
|
|
|
|
acData := sort.StringSlice{}
|
|
|
|
dedupedAutocomplete := alertmanager.DedupAutocomplete()
|
|
|
|
lowerTerm := strings.ToLower(term)
|
|
for _, hint := range dedupedAutocomplete {
|
|
if strings.HasPrefix(strings.ToLower(hint.Value), lowerTerm) {
|
|
acData = append(acData, hint.Value)
|
|
} else {
|
|
for _, token := range hint.Tokens {
|
|
if strings.HasPrefix(strings.ToLower(token), lowerTerm) {
|
|
acData = append(acData, hint.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(sort.Reverse(acData))
|
|
data, _ = marshalJSON(acData)
|
|
_ = apiCache.Add(cacheKey, data)
|
|
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func silences(w http.ResponseWriter, r *http.Request) {
|
|
noCache(w)
|
|
|
|
cacheKey := r.RequestURI
|
|
|
|
data, found := apiCache.Get(cacheKey)
|
|
if found {
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
return
|
|
}
|
|
|
|
dedupedSilences := []models.ManagedSilence{}
|
|
|
|
showExpired := false
|
|
showExpiredValue, found := lookupQueryString(r, "showExpired")
|
|
if found && showExpiredValue == "1" {
|
|
showExpired = true
|
|
}
|
|
|
|
searchTerm := ""
|
|
searchTermValue, found := lookupQueryString(r, "searchTerm")
|
|
if found && searchTermValue != "" {
|
|
searchTerm = strings.ToLower(searchTermValue)
|
|
}
|
|
|
|
clusters := []string{}
|
|
if searchTerm != "" {
|
|
upstreams := getUpstreams()
|
|
for _, u := range upstreams.Instances {
|
|
if strings.ToLower(u.Name) == searchTerm || strings.ToLower(u.Cluster) == searchTerm {
|
|
if !slices.Contains(clusters, u.Cluster) {
|
|
clusters = append(clusters, strings.ToLower(u.Cluster))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, silence := range alertmanager.DedupSilences() {
|
|
if silence.IsExpired && !showExpired {
|
|
continue
|
|
}
|
|
if searchTerm != "" {
|
|
isMatch := false
|
|
switch {
|
|
case strings.ToLower(silence.Silence.ID) == searchTerm:
|
|
isMatch = true
|
|
case "@cluster="+strings.ToLower(silence.Cluster) == searchTerm:
|
|
isMatch = true
|
|
case slices.Contains(clusters, strings.ToLower(silence.Cluster)):
|
|
isMatch = true
|
|
case strings.Contains(strings.ToLower(silence.Silence.Comment), searchTerm):
|
|
isMatch = true
|
|
case strings.Contains(strings.ToLower(silence.Silence.CreatedBy), searchTerm):
|
|
isMatch = true
|
|
default:
|
|
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 strings.Contains(strings.ToLower(fmt.Sprintf("%s%s%s", match.Name, eq, match.Value)), searchTerm) {
|
|
isMatch = true
|
|
}
|
|
}
|
|
}
|
|
if !isMatch {
|
|
continue
|
|
}
|
|
}
|
|
dedupedSilences = append(dedupedSilences, silence)
|
|
}
|
|
|
|
recentFirst := true
|
|
sortReverse, found := lookupQueryString(r, "sortReverse")
|
|
if found && sortReverse == "1" {
|
|
recentFirst = false
|
|
}
|
|
|
|
sort.Slice(dedupedSilences, func(i, 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
|
|
})
|
|
|
|
silenceCounters := make(map[string]int, len(dedupedSilences))
|
|
for _, silence := range dedupedSilences {
|
|
silenceCounters[silence.Silence.ID] = 0
|
|
}
|
|
for _, alertGroup := range alertmanager.DedupAlerts() {
|
|
for _, alert := range alertGroup.Alerts {
|
|
sidDone := map[string]struct{}{}
|
|
for _, am := range alert.Alertmanager {
|
|
for _, sID := range am.SilencedBy {
|
|
if _, found := sidDone[sID]; !found {
|
|
if _, ok := silenceCounters[sID]; ok {
|
|
silenceCounters[sID]++
|
|
sidDone[sID] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for i := range dedupedSilences {
|
|
if counter, ok := silenceCounters[dedupedSilences[i].Silence.ID]; ok {
|
|
dedupedSilences[i].AlertCount = counter
|
|
}
|
|
}
|
|
|
|
data, _ = marshalJSON(dedupedSilences)
|
|
_ = apiCache.Add(cacheKey, data)
|
|
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
type AlertList struct {
|
|
Alerts []models.OrderedLabels `json:"alerts"`
|
|
}
|
|
|
|
func alertList(w http.ResponseWriter, r *http.Request) {
|
|
noCache(w)
|
|
|
|
// use full URI (including query args) as cache key
|
|
cacheKey := r.RequestURI
|
|
|
|
d, found := apiCache.Get(cacheKey)
|
|
if found {
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(d)
|
|
return
|
|
}
|
|
|
|
q, _ := lookupQueryStringSlice(r, "q")
|
|
matchFilters := getFiltersFromQuery(q)
|
|
dedupedAlerts := alertmanager.DedupAlerts()
|
|
filtered := filterAlerts(dedupedAlerts, matchFilters)
|
|
|
|
labelMap := map[string]map[string]string{}
|
|
for _, ag := range filtered {
|
|
for _, alert := range ag.Alerts {
|
|
lm := ag.Labels.Map()
|
|
alert.Labels.Range(func(l promlabels.Label) {
|
|
lm[l.Name] = l.Value
|
|
})
|
|
h := hex.EncodeToString(structhash.Sha1(lm, 1))
|
|
labelMap[h] = lm
|
|
}
|
|
}
|
|
|
|
sortMap := map[string]struct{}{}
|
|
for _, k := range labelMap {
|
|
for v := range k {
|
|
sortMap[v] = struct{}{}
|
|
}
|
|
}
|
|
sortKeys := make([]string, 0, len(sortMap))
|
|
for k := range sortMap {
|
|
sortKeys = append(sortKeys, k)
|
|
}
|
|
sort.Strings(sortKeys)
|
|
|
|
al := AlertList{
|
|
Alerts: []models.OrderedLabels{},
|
|
}
|
|
for _, lm := range labelMap {
|
|
al.Alerts = append(al.Alerts, models.LabelsToOrderedLabels(models.LabelsFromMap(lm)))
|
|
}
|
|
sortSliceOfLabels(al.Alerts, sortKeys, "alertname")
|
|
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
data, _ := marshalJSON(al)
|
|
_ = apiCache.Add(cacheKey, data)
|
|
_, _ = w.Write(data)
|
|
}
|
|
|
|
func sortSliceOfLabels(ls []models.OrderedLabels, sortKeys []string, fallback string) {
|
|
sort.SliceStable(ls, func(i, j int) bool {
|
|
for _, k := range sortKeys {
|
|
vi := ls[i].Get(k)
|
|
vj := ls[j].Get(k)
|
|
if vi != "" && vj == "" {
|
|
return true
|
|
}
|
|
if vi == "" && vj != "" {
|
|
return false
|
|
}
|
|
if vi != vj {
|
|
return sortorder.NaturalLess(vi, vj)
|
|
}
|
|
}
|
|
return sortorder.NaturalLess(ls[i].Get(fallback), ls[j].Get(fallback))
|
|
})
|
|
}
|
|
|
|
func counters(w http.ResponseWriter, r *http.Request) {
|
|
noCache(w)
|
|
|
|
// use full URI (including query args) as cache key
|
|
cacheKey := r.RequestURI
|
|
|
|
d, found := apiCache.Get(cacheKey)
|
|
if found {
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(d)
|
|
return
|
|
}
|
|
|
|
q, _ := lookupQueryStringSlice(r, "q")
|
|
matchFilters := getFiltersFromQuery(q)
|
|
dedupedAlerts := alertmanager.DedupAlerts()
|
|
filtered := filterAlerts(dedupedAlerts, matchFilters)
|
|
upstreams := getUpstreams()
|
|
counters := map[string]map[string]int{}
|
|
|
|
countLabel := func(key, val string) {
|
|
if _, found := counters[key]; !found {
|
|
counters[key] = make(map[string]int)
|
|
}
|
|
counters[key][val]++
|
|
}
|
|
|
|
var total int
|
|
for _, ag := range filtered {
|
|
total += len(ag.Alerts)
|
|
for _, alert := range ag.Alerts {
|
|
stateCount := newStateCount()
|
|
for _, am := range alert.Alertmanager {
|
|
stateCount[am.State.String()]++
|
|
}
|
|
alert.State = stateFromStateCount(stateCount)
|
|
|
|
if len(upstreams.Clusters) > 1 {
|
|
clusters := map[string]struct{}{}
|
|
for _, am := range alert.Alertmanager {
|
|
clusters[am.Cluster] = struct{}{}
|
|
}
|
|
for cluster := range clusters {
|
|
countLabel("@cluster", cluster)
|
|
}
|
|
}
|
|
countLabel("@state", alert.State.String())
|
|
countLabel("@receiver", alert.Receiver)
|
|
alert.Labels.Range(func(l promlabels.Label) {
|
|
countLabel(l.Name, l.Value)
|
|
})
|
|
}
|
|
}
|
|
|
|
resp := models.Counters{
|
|
Total: total,
|
|
Counters: countersToLabelStats(counters),
|
|
}
|
|
|
|
mimeJSON(w)
|
|
w.WriteHeader(http.StatusOK)
|
|
data, _ := marshalJSON(resp)
|
|
_ = apiCache.Add(cacheKey, data)
|
|
_, _ = w.Write(data)
|
|
}
|