diff --git a/autocomplete.go b/autocomplete.go new file mode 100644 index 000000000..9b5ed15d6 --- /dev/null +++ b/autocomplete.go @@ -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)) +} diff --git a/autocomplete_test.go b/autocomplete_test.go new file mode 100644 index 000000000..d512baad7 --- /dev/null +++ b/autocomplete_test.go @@ -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) + } + } + } +} diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index 6c6455ec6..5b0d92765 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -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 +} diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index ea0776c2f..57223b3fe 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -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() diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index c99173ed0..343b49285 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -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, diff --git a/main.go b/main.go index 1ff82378f..5e7b98464 100644 --- a/main.go +++ b/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) }