From 31f8ca0dfb497f12404aba3108440f79b3015659 Mon Sep 17 00:00:00 2001 From: mg03 Date: Sun, 25 Mar 2018 09:50:34 -0700 Subject: [PATCH] api v1 alerts/rules json endpoint Signed-off-by: mg03 --- rules/alerting.go | 35 +++++++++++ web/api/v1/api.go | 129 +++++++++++++++++++++++++++++++++---- web/api/v1/api_test.go | 140 ++++++++++++++++++++++++++++++++++++++--- web/web.go | 1 + 4 files changed, 285 insertions(+), 20 deletions(-) diff --git a/rules/alerting.go b/rules/alerting.go index d7c39904a..2a94e2b0a 100644 --- a/rules/alerting.go +++ b/rules/alerting.go @@ -131,6 +131,41 @@ func (r *AlertingRule) Name() string { return r.name } +// Query returns the query expression of the alert. +func (r *AlertingRule) Query() promql.Expr { + return r.vector +} + +// Duration returns the hold duration of the alert. +func (r *AlertingRule) Duration() time.Duration { + return r.holdDuration +} + +// Labels returns the labels of the alert. +func (r *AlertingRule) Labels() labels.Labels { + return r.labels +} + +// Annotations returns the annotations of the alert. +func (r *AlertingRule) Annotations() labels.Labels { + return r.annotations +} + +// Alertinfo return an array of alerts +func (r *AlertingRule) Alertinfo() []*Alert { + activealerts := &r.active + alertsarr := make([]*Alert, 0) + if len(*activealerts) > 0 { + for _, a := range *activealerts { + if a.ResolvedAt.IsZero() { + alertsarr = append(alertsarr, a) + } + } + return alertsarr + } + return nil +} + func (r *AlertingRule) equal(o *AlertingRule) bool { return r.name == o.name && labels.Equal(r.labels, o.labels) } diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 4d990eb64..5cd3ce0c6 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -41,6 +41,7 @@ import ( "github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage/remote" @@ -95,6 +96,19 @@ type alertmanagerRetriever interface { DroppedAlertmanagers() []*url.URL } +type alertRetreiver interface { + AlertingRules() []*rules.AlertingRule +} + +type rulesRetreiver interface { + RuleGroups() []*rules.Group +} + +type alertsrulesRetreiver interface { + alertRetreiver + rulesRetreiver +} + type response struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` @@ -119,11 +133,11 @@ type API struct { targetRetriever targetRetriever alertmanagerRetriever alertmanagerRetriever - - now func() time.Time - config func() config.Config - flagsMap map[string]string - ready func(http.HandlerFunc) http.HandlerFunc + alertsrulesRetreiver alertsrulesRetreiver + now func() time.Time + config func() config.Config + flagsMap map[string]string + ready func(http.HandlerFunc) http.HandlerFunc db func() *tsdb.DB enableAdmin bool @@ -142,18 +156,20 @@ func NewAPI( db func() *tsdb.DB, enableAdmin bool, logger log.Logger, + al alertsrulesRetreiver, ) *API { return &API{ QueryEngine: qe, Queryable: q, targetRetriever: tr, alertmanagerRetriever: ar, - now: time.Now, - config: configFunc, - flagsMap: flagsMap, - ready: readyFunc, - db: db, - enableAdmin: enableAdmin, + now: time.Now, + config: configFunc, + flagsMap: flagsMap, + ready: readyFunc, + db: db, + enableAdmin: enableAdmin, + alertsrulesRetreiver: al, } } @@ -199,6 +215,9 @@ func (api *API) Register(r *route.Router) { r.Get("/status/flags", wrap(api.serveFlags)) r.Post("/read", api.ready(http.HandlerFunc(api.remoteRead))) + r.Get("/alerts", wrap(api.alerts)) + r.Get("/rules", wrap(api.rules)) + // Admin APIs r.Post("/admin/tsdb/delete_series", wrap(api.deleteSeries)) r.Post("/admin/tsdb/clean_tombstones", wrap(api.cleanTombstones)) @@ -578,6 +597,94 @@ func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError, func()) return ams, nil, nil } +// AlertDiscovery has info for all alerts +type AlertDiscovery struct { + Alertgrps []*Alertgrp `json:"alertgrp"` +} + +// Alert has info for a alert +type Alert struct { + Labels labels.Labels `json:"labels"` + Status string `json:"status"` + Activesince *time.Time `json:"activesince,omitempty"` +} + +// Alertgrp has info for alerts part of a group +type Alertgrp struct { + Name string `json:"name"` + Query string `json:"query"` + Duration string `json:"duration"` + Annotations labels.Labels `json:"annotations,omitempty"` + Alerts []*Alert `json:"alerts"` +} + +func (api *API) alerts(r *http.Request) (interface{}, *apiError) { + alertingrules := api.alertsrulesRetreiver.AlertingRules() + var alertgrps []*Alertgrp + res := &AlertDiscovery{Alertgrps: alertgrps} + for _, activerule := range alertingrules { + t := &Alertgrp{ + Name: activerule.Name(), + Query: fmt.Sprintf("%v", activerule.Query()), + Duration: activerule.Duration().String(), + Annotations: activerule.Annotations(), + } + alerts := activerule.Alertinfo() + var activealerts []*Alert + for _, alert := range alerts { + q := &Alert{ + Labels: alert.Labels, + Status: alert.State.String(), + Activesince: &alert.ActiveAt, + } + + activealerts = append(activealerts, q) + } + t.Alerts = activealerts + res.Alertgrps = append(res.Alertgrps, t) + } + + return res, nil +} + +// GroupDiscovery has info for all rules +type GroupDiscovery struct { + Rulegrps []*Rulegrp `json:"groups"` +} + +// Rulegrp has info for rules which are part of a group +type Rulegrp struct { + Name string `json:"name"` + File string `json:"file"` + Rules []*Ruleinfo `json:"rules"` +} + +// Ruleinfo has rule in human readable format using \n as line separators +type Ruleinfo struct { + Rule string `json:"rule"` +} + +func (api *API) rules(r *http.Request) (interface{}, *apiError) { + grps := api.alertsrulesRetreiver.RuleGroups() + res := &GroupDiscovery{Rulegrps: make([]*Rulegrp, len(grps))} + for i, grp := range grps { + t := &Rulegrp{ + Name: grp.Name(), + File: grp.File(), + } + var rulearr []*Ruleinfo + for _, rule := range grp.Rules() { + q := &Ruleinfo{ + Rule: rule.String(), + } + rulearr = append(rulearr, q) + } + t.Rules = rulearr + res.Rulegrps[i] = t + } + return res, nil +} + type prometheusConfig struct { YAML string `json:"yaml"` } diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index a4807daf2..5082b03bc 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -19,7 +19,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/go-kit/kit/log" "io/ioutil" + stdlog "log" "math" "net/http" "net/http/httptest" @@ -41,9 +43,11 @@ import ( "github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/rules" "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage/remote" + "github.com/prometheus/prometheus/util/testutil" ) type testTargetRetriever struct{} @@ -98,6 +102,68 @@ func (t testAlertmanagerRetriever) DroppedAlertmanagers() []*url.URL { } } +type testalertsrulesfunc struct { + test *testing.T +} + +func (t testalertsrulesfunc) AlertingRules() []*rules.AlertingRule { + expr1, err := promql.ParseExpr(`absent(test_metric3) != 1`) + if err != nil { + stdlog.Fatalf("Unable to parse alert expression: %s", err) + } + expr2, err := promql.ParseExpr(`up == 1`) + if err != nil { + stdlog.Fatalf("Unable to parse alert expression: %s", err) + } + + rule1 := rules.NewAlertingRule( + "test_metric3", + expr1, + time.Second, + labels.Labels{}, + labels.Labels{}, + log.NewNopLogger(), + ) + rule2 := rules.NewAlertingRule( + "test_metric4", + expr2, + time.Second, + labels.Labels{}, + labels.Labels{}, + log.NewNopLogger(), + ) + var r []*rules.AlertingRule + r = append(r, rule1) + r = append(r, rule2) + return r +} + +func (t testalertsrulesfunc) RuleGroups() []*rules.Group { + var ar testalertsrulesfunc + arules := ar.AlertingRules() + storage := testutil.NewStorage(t.test) + defer storage.Close() + + engine := promql.NewEngine(nil, nil, 10, 10*time.Second) + opts := &rules.ManagerOptions{ + QueryFunc: rules.EngineQueryFunc(engine, storage), + Appendable: storage, + Context: context.Background(), + Logger: log.NewNopLogger(), + } + + var r []rules.Rule + + for _, alertrule := range arules { + r = append(r, alertrule) + } + + group := rules.NewGroup("grp", "/path/to/file", time.Second, r, opts) + fmt.Println(group) + return []*rules.Group{group} + +} + var samplePrometheusCfg = config.Config{ GlobalConfig: config.GlobalConfig{}, AlertingConfig: config.AlertingConfig{}, @@ -131,15 +197,24 @@ func TestEndpoints(t *testing.T) { now := time.Now() t.Run("local", func(t *testing.T) { + + var algr testalertsrulesfunc + algr.test = t + + algr.AlertingRules() + + algr.RuleGroups() + api := &API{ Queryable: suite.Storage(), QueryEngine: suite.QueryEngine(), targetRetriever: testTargetRetriever{}, alertmanagerRetriever: testAlertmanagerRetriever{}, - now: func() time.Time { return now }, - config: func() config.Config { return samplePrometheusCfg }, - flagsMap: sampleFlagMap, - ready: func(f http.HandlerFunc) http.HandlerFunc { return f }, + now: func() time.Time { return now }, + config: func() config.Config { return samplePrometheusCfg }, + flagsMap: sampleFlagMap, + ready: func(f http.HandlerFunc) http.HandlerFunc { return f }, + alertsrulesRetreiver: algr, } testEndpoints(t, api, true) @@ -176,15 +251,23 @@ func TestEndpoints(t *testing.T) { t.Fatal(err) } + var algr testalertsrulesfunc + algr.test = t + + algr.AlertingRules() + + algr.RuleGroups() + api := &API{ Queryable: remote, QueryEngine: suite.QueryEngine(), targetRetriever: testTargetRetriever{}, alertmanagerRetriever: testAlertmanagerRetriever{}, - now: func() time.Time { return now }, - config: func() config.Config { return samplePrometheusCfg }, - flagsMap: sampleFlagMap, - ready: func(f http.HandlerFunc) http.HandlerFunc { return f }, + now: func() time.Time { return now }, + config: func() config.Config { return samplePrometheusCfg }, + flagsMap: sampleFlagMap, + ready: func(f http.HandlerFunc) http.HandlerFunc { return f }, + alertsrulesRetreiver: algr, } testEndpoints(t, api, false) @@ -237,7 +320,6 @@ func setupRemote(s storage.Storage) *httptest.Server { } func testEndpoints(t *testing.T, api *API, testLabelAPI bool) { - start := time.Unix(0, 0) type test struct { @@ -567,6 +649,46 @@ func testEndpoints(t *testing.T, api *API, testLabelAPI bool) { endpoint: api.serveFlags, response: sampleFlagMap, }, + { + endpoint: api.alerts, + response: &AlertDiscovery{ + Alertgrps: []*Alertgrp{ + { + Name: "test_metric3", + Query: "absent(test_metric3) != 1", + Duration: "1s", + Alerts: nil, + Annotations: labels.Labels{}, + }, + { + Name: "test_metric4", + Query: "up == 1", + Duration: "1s", + Alerts: nil, + Annotations: labels.Labels{}, + }, + }, + }, + }, + { + endpoint: api.rules, + response: &GroupDiscovery{ + Rulegrps: []*Rulegrp{ + { + Name: "grp", + File: "/path/to/file", + Rules: []*Ruleinfo{ + { + Rule: "alert: test_metric3\nexpr: absent(test_metric3) != 1\nfor: 1s\n", + }, + { + Rule: "alert: test_metric4\nexpr: up == 1\nfor: 1s\n", + }, + }, + }, + }, + }, + }, } if testLabelAPI { diff --git a/web/web.go b/web/web.go index af75a0827..5fa6309ab 100644 --- a/web/web.go +++ b/web/web.go @@ -228,6 +228,7 @@ func New(logger log.Logger, o *Options) *Handler { h.options.TSDB, h.options.EnableAdminAPI, logger, + h.ruleManager, ) if o.RoutePrefix != "/" {