mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
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:
parent
90de3086b3
commit
9f8cc8ae1b
3 changed files with 77 additions and 60 deletions
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue