mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
feat(api): add /labelNames.json endpoint
This endpoint will be used for label name autocomplete in the silence form
This commit is contained in:
63
autocomplete.go
Normal file
63
autocomplete.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prymitive/unsee/internal/alertmanager"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// knownLabelNames allows querying known label names
|
||||
func knownLabelNames(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
|
||||
}
|
||||
|
||||
labels := alertmanager.DedupKnownLabels()
|
||||
acData := []string{}
|
||||
|
||||
term, found := c.GetQuery("term")
|
||||
if !found || term == "" {
|
||||
// return everything
|
||||
sort.Strings(labels)
|
||||
acData = labels
|
||||
} else {
|
||||
// return what matches
|
||||
for _, key := range labels {
|
||||
if strings.Contains(key, term) {
|
||||
acData = append(acData, key)
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
70
autocomplete_test.go
Normal file
70
autocomplete_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/prymitive/unsee/internal/mock"
|
||||
)
|
||||
|
||||
type labelTest struct {
|
||||
Term string
|
||||
Results []string
|
||||
}
|
||||
|
||||
var labelTests = []labelTest{
|
||||
{
|
||||
Term: "a",
|
||||
Results: []string{"alertname", "instance"},
|
||||
},
|
||||
{
|
||||
Term: "alertname",
|
||||
Results: []string{"alertname"},
|
||||
},
|
||||
{
|
||||
Term: "1234567890",
|
||||
Results: []string{},
|
||||
},
|
||||
{
|
||||
Term: "",
|
||||
Results: []string{"alertname", "cluster", "instance", "job"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestKnownLabels(t *testing.T) {
|
||||
mockConfig()
|
||||
for _, version := range mock.ListAllMocks() {
|
||||
t.Logf("Testing known labels using mock files from Alertmanager %s", version)
|
||||
mockAlerts(version)
|
||||
r := ginTestEngine()
|
||||
|
||||
req, _ := http.NewRequest("GET", "/labelNames.json", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("Invalid status code for request without any query: %d", resp.Code)
|
||||
}
|
||||
|
||||
for _, testCase := range labelTests {
|
||||
url := fmt.Sprintf("/labelNames.json?term=%s", testCase.Term)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Errorf("GET %s returned status %d", url, resp.Code)
|
||||
}
|
||||
|
||||
ur := []string{}
|
||||
json.Unmarshal(resp.Body.Bytes(), &ur)
|
||||
|
||||
if len(ur) != len(testCase.Results) {
|
||||
t.Errorf("Invalid number of label names for %s, got %d, expected %d", url, len(ur), len(testCase.Results))
|
||||
t.Errorf("Results: %s", ur)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,3 +162,21 @@ func DedupAutocomplete() []models.Autocomplete {
|
||||
|
||||
return dedupedAutocomplete
|
||||
}
|
||||
|
||||
// DedupKnownLabels returns a deduplicated slice of all known label names
|
||||
func DedupKnownLabels() []string {
|
||||
dedupedLabels := map[string]bool{}
|
||||
upstreams := GetAlertmanagers()
|
||||
|
||||
for _, am := range upstreams {
|
||||
for _, key := range am.KnownLabels() {
|
||||
dedupedLabels[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
flatLabels := []string{}
|
||||
for key := range dedupedLabels {
|
||||
flatLabels = append(flatLabels, key)
|
||||
}
|
||||
return flatLabels
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type Alertmanager struct {
|
||||
silences map[string]models.Silence
|
||||
colors models.LabelsColorMap
|
||||
autocomplete []models.Autocomplete
|
||||
knownLabels []string
|
||||
lastError string
|
||||
// metrics tracked per alertmanager instance
|
||||
metrics alertmanagerMetrics
|
||||
@@ -100,6 +101,7 @@ func (am *Alertmanager) clearData() {
|
||||
am.silences = map[string]models.Silence{}
|
||||
am.colors = models.LabelsColorMap{}
|
||||
am.autocomplete = []models.Autocomplete{}
|
||||
am.knownLabels = []string{}
|
||||
am.lock.Unlock()
|
||||
}
|
||||
|
||||
@@ -205,6 +207,7 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
log.Infof("[%s] Deduplicating alert groups (%d)", am.Name, len(groups))
|
||||
uniqueGroups := map[string]models.AlertGroup{}
|
||||
uniqueAlerts := map[string]map[string]models.Alert{}
|
||||
knownLabelsMap := map[string]bool{}
|
||||
for _, ag := range groups {
|
||||
agID := ag.LabelsFingerprint()
|
||||
if _, found := uniqueGroups[agID]; !found {
|
||||
@@ -222,6 +225,9 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
if _, found := uniqueAlerts[agID][alertCFP]; !found {
|
||||
uniqueAlerts[agID][alertCFP] = alert
|
||||
}
|
||||
for key := range alert.Labels {
|
||||
knownLabelsMap[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -284,10 +290,16 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
autocomplete = append(autocomplete, hint)
|
||||
}
|
||||
|
||||
knownLabels := []string{}
|
||||
for key := range knownLabelsMap {
|
||||
knownLabels = append(knownLabels, key)
|
||||
}
|
||||
|
||||
am.lock.Lock()
|
||||
am.alertGroups = dedupedGroups
|
||||
am.colors = colors
|
||||
am.autocomplete = autocomplete
|
||||
am.knownLabels = knownLabels
|
||||
am.lock.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -378,6 +390,17 @@ func (am *Alertmanager) Autocomplete() []models.Autocomplete {
|
||||
return autocomplete
|
||||
}
|
||||
|
||||
// KnownLabels returns a copy of a map with known labels
|
||||
func (am *Alertmanager) KnownLabels() []string {
|
||||
am.lock.RLock()
|
||||
defer am.lock.RUnlock()
|
||||
|
||||
knownLabels := make([]string, len(am.knownLabels))
|
||||
copy(knownLabels, am.knownLabels)
|
||||
|
||||
return knownLabels
|
||||
}
|
||||
|
||||
func (am *Alertmanager) setError(err string) {
|
||||
am.lock.Lock()
|
||||
defer am.lock.Unlock()
|
||||
|
||||
@@ -30,6 +30,7 @@ func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, e
|
||||
silences: map[string]models.Silence{},
|
||||
colors: models.LabelsColorMap{},
|
||||
autocomplete: []models.Autocomplete{},
|
||||
knownLabels: []string{},
|
||||
metrics: alertmanagerMetrics{
|
||||
errors: map[string]float64{
|
||||
labelValueErrorsAlerts: 0,
|
||||
|
||||
1
main.go
1
main.go
@@ -68,6 +68,7 @@ func setupRouter(router *gin.Engine) {
|
||||
router.GET(getViewURL("/"), index)
|
||||
router.GET(getViewURL("/alerts.json"), alerts)
|
||||
router.GET(getViewURL("/autocomplete.json"), autocomplete)
|
||||
router.GET(getViewURL("/labelNames.json"), knownLabelNames)
|
||||
router.NoRoute(notFound)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user