Skip to content
This repository was archived by the owner on Apr 2, 2024. It is now read-only.

Commit a973379

Browse files
Adds support for /api/v1/rules API
Signed-off-by: Harkishen-Singh <[email protected]> This commit implements the `/api/v1/rules` API. This implementation is done using the rule-manager in rules package along with reusing the structs that Prometheus uses in its API package. This ensures that our API response similar to that of Prometheus.
1 parent 3d7cd20 commit a973379

File tree

9 files changed

+263
-27
lines changed

9 files changed

+263
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ We use the following categories for changes:
2222
- Query Jaeger traces directly through Promscale [#1224]
2323
- Additional dataset configuration options via `-startup.dataset.config` flag. Read more [here](docs/dataset.md) [#1276, #1310]
2424
- Adds support for alerting and recording rules in Promscale [#1286, #1315]
25+
- Adds support for `/api/v1/rules` API [#1320]
2526

2627
### Changed
2728
- Enable tracing by default [#1213], [#1290]

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
12861286
github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
12871287
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
12881288
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
1289+
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
12891290
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
12901291
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
12911292
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=

pkg/api/common.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,14 @@ func respond(w http.ResponseWriter, status int, message interface{}) {
191191
w.Header().Set("Content-Type", "application/json")
192192
w.Header().Set("Cache-Control", "no-store")
193193
w.WriteHeader(status)
194+
195+
statusText := "success"
196+
if status != 200 {
197+
statusText = http.StatusText(status)
198+
}
199+
194200
_ = json.NewEncoder(w).Encode(&response{
195-
Status: http.StatusText(status),
201+
Status: statusText,
196202
Data: message,
197203
})
198204
}

pkg/api/provider.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// This file and its contents are licensed under the Apache License 2.0.
2+
// Please see the included NOTICE for copyright information and
3+
// LICENSE for a copy of the license.
4+
5+
package api
6+
7+
import (
8+
"sync"
9+
10+
"github.com/timescale/promscale/pkg/pgclient"
11+
"github.com/timescale/promscale/pkg/rules"
12+
)
13+
14+
// Provider provides the API, access to the application level implementations.
15+
// It must be created with its constructor so that none of its fields are nil.
16+
type provider struct {
17+
client *pgclient.Client
18+
rulesMux sync.RWMutex
19+
rules rules.Manager
20+
}
21+
22+
func NewProviderWith(client *pgclient.Client, manager rules.Manager) *provider {
23+
return &provider{
24+
client: client,
25+
rules: manager,
26+
}
27+
}
28+
29+
func (p *provider) UpdateRulesManager(m rules.Manager) {
30+
p.rulesMux.Lock()
31+
defer p.rulesMux.Unlock()
32+
p.rules = m
33+
}
34+
35+
func (p *provider) RulesManager() rules.Manager {
36+
p.rulesMux.RLock()
37+
defer p.rulesMux.RUnlock()
38+
return p.rules
39+
}

pkg/api/router.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,15 @@ import (
2222
haClient "github.com/timescale/promscale/pkg/ha/client"
2323
"github.com/timescale/promscale/pkg/jaeger"
2424
"github.com/timescale/promscale/pkg/log"
25-
"github.com/timescale/promscale/pkg/pgclient"
2625
pgMetrics "github.com/timescale/promscale/pkg/pgmodel/metrics"
2726
"github.com/timescale/promscale/pkg/query"
28-
"github.com/timescale/promscale/pkg/rules"
2927
"github.com/timescale/promscale/pkg/telemetry"
3028
)
3129

32-
// Provider provides the API, an access to the application level implementations.
33-
// None of its fields should be nil.
34-
type Provider struct {
35-
Client *pgclient.Client
36-
Rules rules.Manager
37-
}
38-
39-
func GenerateRouter(apiConf *Config, promqlConf *query.Config, provider Provider) (*mux.Router, error) {
30+
func GenerateRouter(apiConf *Config, promqlConf *query.Config, provider *provider) (*mux.Router, error) {
4031
var (
4132
writePreprocessors []parser.Preprocessor
42-
client = provider.Client
33+
client = provider.client
4334
)
4435
if apiConf.HighAvailability {
4536
service := ha.NewService(haClient.NewLeaseClient(client.Connection))
@@ -98,6 +89,9 @@ func GenerateRouter(apiConf *Config, promqlConf *query.Config, provider Provider
9889
metadataHandler := timeHandler(metrics.HTTPRequestDuration, "metadata", MetricMetadata(apiConf, client))
9990
apiV1.Path("/metadata").Methods(http.MethodGet, http.MethodPost).HandlerFunc(metadataHandler)
10091

92+
rulesHandler := timeHandler(metrics.HTTPRequestDuration, "rules", Rules(apiConf, provider, updateQueryMetrics))
93+
apiV1.Path("/rules").Methods(http.MethodGet).HandlerFunc(rulesHandler)
94+
10195
labelValuesHandler := timeHandler(metrics.HTTPRequestDuration, "label/:name/values", LabelValues(apiConf, queryable))
10296
apiV1.Path("/label/{name}/values").Methods(http.MethodGet).HandlerFunc(labelValuesHandler)
10397

pkg/api/rules.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// This file and its contents are licensed under the Apache License 2.0.
2+
// Please see the included NOTICE for copyright information and
3+
// LICENSE for a copy of the license.
4+
5+
package api
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"strconv"
11+
"strings"
12+
"time"
13+
14+
"github.com/NYTimes/gziphandler"
15+
16+
"github.com/prometheus/prometheus/model/labels"
17+
prom_rules "github.com/prometheus/prometheus/rules"
18+
19+
"github.com/timescale/promscale/pkg/log"
20+
)
21+
22+
func Rules(conf *Config, p *provider, updateMetrics func(handler, code string, duration float64)) http.Handler {
23+
hf := corsWrapper(conf, rulesHandler(p, updateMetrics))
24+
return gziphandler.GzipHandler(hf)
25+
}
26+
27+
// Below types and code is copied from prometheus/web/api.v1
28+
// https://github.com/prometheus/prometheus/blob/854d671b6bc5d3ae7960936103ff23863c8a3f91/web/api/v1/api.go#L1168
29+
//
30+
// Note: We cannot directly import prometheus/web/api/v1 package and use these exported structs from there.
31+
// This is because then we get duplicate enum registration in prompb, which is likely due to
32+
// some dependency in api/v1 package. Thus, copying them here avoids the enum issue.
33+
34+
// RuleDiscovery has info for all rules
35+
type RuleDiscovery struct {
36+
RuleGroups []*RuleGroup `json:"groups"`
37+
}
38+
39+
// RuleGroup has info for rules which are part of a group
40+
type RuleGroup struct {
41+
Name string `json:"name"`
42+
File string `json:"file"`
43+
// In order to preserve rule ordering, while exposing type (alerting or recording)
44+
// specific properties, both alerting and recording rules are exposed in the
45+
// same array.
46+
Rules []Rule `json:"rules"`
47+
Interval float64 `json:"interval"`
48+
Limit int `json:"limit"`
49+
EvaluationTime float64 `json:"evaluationTime"`
50+
LastEvaluation time.Time `json:"lastEvaluation"`
51+
}
52+
53+
type Rule interface{}
54+
55+
type AlertingRule struct {
56+
// State can be "pending", "firing", "inactive".
57+
State string `json:"state"`
58+
Name string `json:"name"`
59+
Query string `json:"query"`
60+
Duration float64 `json:"duration"`
61+
Labels labels.Labels `json:"labels"`
62+
Annotations labels.Labels `json:"annotations"`
63+
Alerts []*Alert `json:"alerts"`
64+
Health prom_rules.RuleHealth `json:"health"`
65+
LastError string `json:"lastError,omitempty"`
66+
EvaluationTime float64 `json:"evaluationTime"`
67+
LastEvaluation time.Time `json:"lastEvaluation"`
68+
// Type of an alertingRule is always "alerting".
69+
Type string `json:"type"`
70+
}
71+
72+
type RecordingRule struct {
73+
Name string `json:"name"`
74+
Query string `json:"query"`
75+
Labels labels.Labels `json:"labels,omitempty"`
76+
Health prom_rules.RuleHealth `json:"health"`
77+
LastError string `json:"lastError,omitempty"`
78+
EvaluationTime float64 `json:"evaluationTime"`
79+
LastEvaluation time.Time `json:"lastEvaluation"`
80+
// Type of a recordingRule is always "recording".
81+
Type string `json:"type"`
82+
}
83+
84+
// AlertDiscovery has info for all active alerts.
85+
type AlertDiscovery struct {
86+
Alerts []*Alert `json:"alerts"`
87+
}
88+
89+
// Alert has info for an alert.
90+
type Alert struct {
91+
Labels labels.Labels `json:"labels"`
92+
Annotations labels.Labels `json:"annotations"`
93+
State string `json:"state"`
94+
ActiveAt *time.Time `json:"activeAt,omitempty"`
95+
Value string `json:"value"`
96+
}
97+
98+
func rulesHandler(p *provider, updateMetrics func(handler, code string, duration float64)) http.HandlerFunc {
99+
return func(w http.ResponseWriter, r *http.Request) {
100+
statusCode := "400"
101+
begin := time.Now()
102+
defer func() {
103+
updateMetrics("/api/v1/rules", statusCode, time.Since(begin).Seconds())
104+
}()
105+
106+
typ := strings.ToLower(r.URL.Query().Get("type"))
107+
if typ != "" && typ != "alert" && typ != "record" {
108+
log.Error("msg", "unsupported type", "type", typ)
109+
respondError(w, http.StatusBadRequest, fmt.Errorf("unsupported type: type %s", typ), "bad_data")
110+
return
111+
}
112+
113+
returnAlerts := typ == "" || typ == "alert"
114+
returnRecording := typ == "" || typ == "record"
115+
116+
ruleGroups := p.RulesManager().RuleGroups()
117+
res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, len(ruleGroups))}
118+
119+
for i, grp := range ruleGroups {
120+
apiRuleGroup := &RuleGroup{
121+
Name: grp.Name(),
122+
File: grp.File(),
123+
Interval: grp.Interval().Seconds(),
124+
Limit: grp.Limit(),
125+
Rules: []Rule{},
126+
EvaluationTime: grp.GetEvaluationTime().Seconds(),
127+
LastEvaluation: grp.GetLastEvaluation(),
128+
}
129+
for _, r := range grp.Rules() {
130+
var enrichedRule Rule
131+
132+
lastError := ""
133+
if r.LastError() != nil {
134+
lastError = r.LastError().Error()
135+
}
136+
switch rule := r.(type) {
137+
case *prom_rules.AlertingRule:
138+
if !returnAlerts {
139+
break
140+
}
141+
enrichedRule = AlertingRule{
142+
State: rule.State().String(),
143+
Name: rule.Name(),
144+
Query: rule.Query().String(),
145+
Duration: rule.HoldDuration().Seconds(),
146+
Labels: rule.Labels(),
147+
Annotations: rule.Annotations(),
148+
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
149+
Health: rule.Health(),
150+
LastError: lastError,
151+
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
152+
LastEvaluation: rule.GetEvaluationTimestamp(),
153+
Type: "alerting",
154+
}
155+
case *prom_rules.RecordingRule:
156+
if !returnRecording {
157+
break
158+
}
159+
enrichedRule = RecordingRule{
160+
Name: rule.Name(),
161+
Query: rule.Query().String(),
162+
Labels: rule.Labels(),
163+
Health: rule.Health(),
164+
LastError: lastError,
165+
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
166+
LastEvaluation: rule.GetEvaluationTimestamp(),
167+
Type: "recording",
168+
}
169+
default:
170+
statusCode = "500"
171+
err := fmt.Errorf("failed to assert type of rule '%v'", rule.Name())
172+
log.Error("msg", err.Error())
173+
respondError(w, http.StatusInternalServerError, err, "type_conversion")
174+
return
175+
}
176+
if enrichedRule != nil {
177+
apiRuleGroup.Rules = append(apiRuleGroup.Rules, enrichedRule)
178+
}
179+
}
180+
res.RuleGroups[i] = apiRuleGroup
181+
}
182+
statusCode = "200"
183+
respond(w, 200, res)
184+
}
185+
}
186+
187+
func rulesAlertsToAPIAlerts(rulesAlerts []*prom_rules.Alert) []*Alert {
188+
apiAlerts := make([]*Alert, len(rulesAlerts))
189+
for i, ruleAlert := range rulesAlerts {
190+
apiAlerts[i] = &Alert{
191+
Labels: ruleAlert.Labels,
192+
Annotations: ruleAlert.Annotations,
193+
State: ruleAlert.State.String(),
194+
ActiveAt: &ruleAlert.ActiveAt,
195+
Value: strconv.FormatFloat(ruleAlert.Value, 'e', -1, 64),
196+
}
197+
}
198+
199+
return apiAlerts
200+
}

pkg/rules/rules.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ func (m *impleManager) Stop() {
146146

147147
type noopImple struct{}
148148

149-
func NewNoopManager() noopImple {
150-
return noopImple{}
149+
func NewNoopManager() *noopImple {
150+
return &noopImple{}
151151
}
152152

153153
func (noopImple) ApplyConfig(*prometheus_config.Config) error { return nil }

pkg/runner/runner.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,7 @@ func Run(cfg *Config) error {
137137
}
138138
}
139139

140-
provider := api.Provider{
141-
Client: client,
142-
Rules: rules.NewNoopManager(),
143-
}
140+
provider := api.NewProviderWith(client, rules.NewNoopManager())
144141
router, err := api.GenerateRouter(&cfg.APICfg, &cfg.PromQLCfg, provider)
145142
if err != nil {
146143
log.Error("msg", "aborting startup due to error", "err", fmt.Sprintf("generate router: %s", err.Error()))
@@ -227,13 +224,13 @@ func Run(cfg *Config) error {
227224
if cfg.RulesCfg.ContainsRules() {
228225
rulesCtx, stopRuler := context.WithCancel(context.Background())
229226
defer stopRuler()
227+
230228
manager, err := rules.NewManager(rulesCtx, prometheus.DefaultRegisterer, client, &cfg.RulesCfg)
231229
if err != nil {
232230
return fmt.Errorf("error creating rules manager: %w", err)
233231
}
234-
// This will not cause a race since its in the same routine as the api's Router,
235-
// plus Promscale hasn't started yet.
236-
provider.Rules = manager
232+
provider.UpdateRulesManager(manager)
233+
237234
group.Add(
238235
func() error {
239236
promCfg := cfg.RulesCfg.PrometheusConfig
@@ -248,10 +245,10 @@ func Run(cfg *Config) error {
248245
); err != nil {
249246
return fmt.Errorf("error updating rules manager: %w", err)
250247
}
251-
log.Info("msg", "Starting rule-manager ...")
248+
log.Info("msg", "Started Rule-Manager")
252249
return manager.Run()
253250
}, func(err error) {
254-
log.Info("msg", "Stopping rule-manager")
251+
log.Info("msg", "Stopping Rule-Manager")
255252
stopRuler() // Stops the discovery manager.
256253
manager.Stop() // Stops the internal group of rule-manager.
257254
},

pkg/tests/end_to_end_tests/promql_endpoint_integration_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,8 @@ func buildRouterWithAPIConfig(pool *pgxpool.Pool, cfg *api.Config) (*mux.Router,
322322
return nil, nil, fmt.Errorf("init promql engine: %w", err)
323323
}
324324

325-
router, err := api.GenerateRouter(cfg, qryCfg, api.Provider{
326-
Client: pgClient,
327-
Rules: rules.NewNoopManager(),
328-
})
325+
provider := api.NewProviderWith(pgClient, rules.NewNoopManager())
326+
router, err := api.GenerateRouter(cfg, qryCfg, provider)
329327
if err != nil {
330328
return nil, nil, fmt.Errorf("generate router: %w", err)
331329
}

0 commit comments

Comments
 (0)