Extend web responses for alerts: (#411)

vmalert: Extend web responses for alerts

* populate apiAlert object with additional fields
* return all active alerts, not only firing
* sort list of API alerts for deterministic output
* add helper for available path list
This commit is contained in:
Roman Khavronenko 2020-04-11 16:49:23 +01:00 committed by GitHub
parent 90de3086b3
commit 9f8cc8ae1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 60 deletions

View file

@ -33,8 +33,8 @@ Examples:
basicAuthUsername = flag.String("datasource.basicAuth.username", "", "Optional basic auth username to use for -datasource.url")
basicAuthPassword = flag.String("datasource.basicAuth.password", "", "Optional basic auth password to use for -datasource.url")
evaluationInterval = flag.Duration("evaluationInterval", 1*time.Minute, "How often to evaluate the rules. Default 1m")
providerURL = flag.String("provider.url", "", "Prometheus alertmanager url. Required parameter. e.g. http://127.0.0.1:9093")
externalURL = flag.String("external.url", "", "Reachable external url. URL is used to generate sharable alert url and in annotation templates")
notifierURL = flag.String("notifier.url", "", "Prometheus alertmanager URL. Required parameter. e.g. http://127.0.0.1:9093")
externalURL = flag.String("external.url", "", "URL is used to generate sharable alert URL")
)
// TODO: hot configuration reload
@ -60,7 +60,7 @@ func main() {
w := &watchdog{
storage: datasource.NewVMStorage(*datasourceURL, *basicAuthUsername, *basicAuthPassword, &http.Client{}),
alertProvider: notifier.NewAlertManager(*providerURL, func(group, name string) string {
alertProvider: notifier.NewAlertManager(*notifierURL, func(group, name string) string {
return fmt.Sprintf("%s/api/v1/%s/%s/status", eu, group, name)
}, &http.Client{}),
}
@ -132,9 +132,9 @@ func getExternalURL(externalURL, httpListenAddr string, isSecure bool) (*url.URL
}
func checkFlags() {
if *providerURL == "" {
if *notifierURL == "" {
flag.PrintDefaults()
logger.Fatalf("provider.url is empty")
logger.Fatalf("notifier.url is empty")
}
if *datasourceURL == "" {
flag.PrintDefaults()

View file

@ -6,6 +6,7 @@ import (
"fmt"
"hash/fnv"
"sort"
"strconv"
"sync"
"time"
@ -21,15 +22,6 @@ type Group struct {
Rules []*Rule
}
// ActiveAlerts returns list of active alert for all rules
func (g *Group) ActiveAlerts() []*notifier.Alert {
var list []*notifier.Alert
for i := range g.Rules {
list = append(list, g.Rules[i].listActiveAlerts()...)
}
return list
}
// Rule is basic alert entity
type Rule struct {
Name string `yaml:"alert"`
@ -41,7 +33,7 @@ type Rule struct {
group *Group
// guard status fields
mu sync.Mutex
mu sync.RWMutex
// stores list of active alerts
alerts map[uint64]*notifier.Alert
// stores last moment of time Exec was called
@ -188,22 +180,38 @@ func (r *Rule) newAlert(m datasource.Metric) (*notifier.Alert, error) {
return a, err
}
func (r *Rule) listActiveAlerts() []*notifier.Alert {
r.mu.Lock()
defer r.mu.Unlock()
var list []*notifier.Alert
for _, a := range r.alerts {
a := a
if a.State == notifier.StateFiring {
list = append(list, a)
}
// AlertAPI generates apiAlert object from alert by its id(hash)
func (r *Rule) AlertAPI(id uint64) *apiAlert {
r.mu.RLock()
defer r.mu.RUnlock()
a, ok := r.alerts[id]
if !ok {
return nil
}
return list
return r.newAlertAPI(*a)
}
// Alert returns single alert by its id(hash)
func (r *Rule) Alert(id uint64) *notifier.Alert {
r.mu.Lock()
defer r.mu.Unlock()
return r.alerts[id]
// AlertAPI generates list of apiAlert objects from existing alerts
func (r *Rule) AlertsAPI() []*apiAlert {
var alerts []*apiAlert
r.mu.RLock()
for _, a := range r.alerts {
alerts = append(alerts, r.newAlertAPI(*a))
}
r.mu.RUnlock()
return alerts
}
func (r *Rule) newAlertAPI(a notifier.Alert) *apiAlert {
return &apiAlert{
ID: a.ID,
Name: a.Name,
Group: a.Group,
Expression: r.Expr,
Labels: a.Labels,
Annotations: a.Annotations,
State: a.State.String(),
ActiveAt: a.Start,
Value: strconv.FormatFloat(a.Value, 'e', -1, 64),
}
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
@ -13,54 +14,67 @@ import (
// apiAlert has info for an alert.
type apiAlert struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Group string `json:"group"`
Expression string `json:"expression"`
State string `json:"state"`
Value string `json:"value"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
State string `json:"state"`
ActiveAt time.Time `json:"activeAt"`
Value string `json:"value"`
}
type requestHandler struct {
groups []Group
}
var pathList = [][]string{
{"/api/v1/alerts", "list all active alerts"},
{"/api/v1/groupName/alertID/status", "get alert status by ID"},
}
func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
resph := responseHandler{w}
switch r.URL.Path {
case "/":
for _, path := range pathList {
p, doc := path[0], path[1]
fmt.Fprintf(w, "<a href='%s'>%q</a> - %s<br/>", p, p, doc)
}
return true
case "/api/v1/alerts":
resph.handle(rh.list())
return true
default:
// /api/v1/<groupName>/<alertID>/status
if strings.HasSuffix(r.URL.Path, "/status") {
resph.handle(rh.alert(r.URL.Path))
return true
}
return false
case "/api/v1/alerts":
resph.handle(rh.listActiveAlerts())
return true
}
}
func (rh *requestHandler) listActiveAlerts() ([]byte, error) {
func (rh *requestHandler) list() ([]byte, error) {
type listAlertsResponse struct {
Data struct {
Alerts []apiAlert `json:"alerts"`
Alerts []*apiAlert `json:"alerts"`
} `json:"data"`
Status string `json:"status"`
}
lr := listAlertsResponse{Status: "success"}
for _, g := range rh.groups {
alerts := g.ActiveAlerts()
for i := range alerts {
alert := alerts[i]
lr.Data.Alerts = append(lr.Data.Alerts, apiAlert{
Labels: alert.Labels,
Annotations: alert.Annotations,
State: alert.State.String(),
ActiveAt: alert.Start,
Value: strconv.FormatFloat(alert.Value, 'e', -1, 64),
})
for _, r := range g.Rules {
lr.Data.Alerts = append(lr.Data.Alerts, r.AlertsAPI()...)
}
}
// sort list of alerts for deterministic output
sort.Slice(lr.Data.Alerts, func(i, j int) bool {
return lr.Data.Alerts[i].Name < lr.Data.Alerts[j].Name
})
b, err := json.Marshal(lr)
if err != nil {
return nil, &httpserver.ErrorWithStatusCode{
@ -84,27 +98,22 @@ func (rh *requestHandler) alert(path string) ([]byte, error) {
id, err := strconv.ParseUint(idStr, 10, 0)
if err != nil {
return nil, &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf(`cannot parse int from %s"`, idStr),
Err: fmt.Errorf(`cannot parse int from %q`, idStr),
StatusCode: http.StatusBadRequest,
}
}
for _, g := range rh.groups {
if g.Name == group {
for i := range g.Rules {
if alert := g.Rules[i].Alert(id); alert != nil {
return json.Marshal(apiAlert{
Labels: alert.Labels,
Annotations: alert.Annotations,
State: alert.State.String(),
ActiveAt: alert.Start,
Value: strconv.FormatFloat(alert.Value, 'e', -1, 64),
})
}
if g.Name != group {
continue
}
for i := range g.Rules {
if apiAlert := g.Rules[i].AlertAPI(id); apiAlert != nil {
return json.Marshal(apiAlert)
}
}
}
return nil, &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf(`cannot find alert %s in %s"`, idStr, group),
Err: fmt.Errorf(`cannot find alert %s in %q`, idStr, group),
StatusCode: http.StatusNotFound,
}
}