Skip to content

Commit 4b4631f

Browse files
authored
Merge pull request #37 from grafana/prepare-rules
Introduce the `rules prepare` command
2 parents deb558b + 8dca5ef commit 4b4631f

37 files changed

+9618
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## unreleased / master
44

5+
* [FEATURE] Add `rules prepare` command. It allows you add a label to PromQL aggregations and lint your expressions in rule files.
6+
57
## v0.1.4 / 2020-03-10
68

79
* [CHANGE] Ensure 404 deletes do not trigger an error for `rules` and `alertmanager` commands #28

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ This command will load each rule group in the specified files and load them into
6060

6161
cortextool rules load ./example_rules_one.yaml ./example_rules_two.yaml ...
6262

63+
#### Rules Prepare
64+
65+
This command prepares a rules file for upload to Cortex. It lints all your PromQL expressions and adds an specific label to your PromQL query aggregations in the file. Unlike, the previous command this one does not interact with your Cortex cluster.
66+
67+
cortextool rules prepare -i ./example_rules_one.yaml ./example_rules_two.yaml ...
68+
69+
There are two flags of note for this command:
70+
- `-i` which allows you to edit in place, otherwise a a new file with a `.output` extension is created with the results of the run.
71+
- `-l` which allows you specify the label you want you add for your aggregations, it is `cluster` by default.
72+
73+
At the end of the run, the command tells you whenever the operation was a success in the form of
74+
75+
INFO[0000] SUCESS: 194 rules found, 0 modified expressions
76+
77+
It is important to note that a modification can be a PromQL expression lint or a label add to your aggregation.
78+
6379
# chunktool
6480

6581
This repo also contains the `chunktool`. A client meant to interact with chunks stored and indexed in cortex backends.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/prometheus/common v0.7.0
2121
github.com/prometheus/prometheus v1.8.2-0.20190918104050-8744afdd1ea0
2222
github.com/sirupsen/logrus v1.4.2
23+
github.com/stretchr/testify v1.4.0
2324
google.golang.org/api v0.8.0
2425
gopkg.in/alecthomas/kingpin.v2 v2.2.6
2526
gopkg.in/yaml.v2 v2.2.2

pkg/commands/rules.go

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"context"
55
"fmt"
6+
"io/ioutil"
67
"os"
78
"path/filepath"
89
"strings"
@@ -16,6 +17,11 @@ import (
1617
"github.com/grafana/cortextool/pkg/rules"
1718
log "github.com/sirupsen/logrus"
1819
"gopkg.in/alecthomas/kingpin.v2"
20+
"gopkg.in/yaml.v2"
21+
)
22+
23+
const (
24+
defaultPrepareAggregationLabel = "cluster"
1925
)
2026

2127
var (
@@ -31,7 +37,7 @@ var (
3137
})
3238
)
3339

34-
// RuleCommand configures and executes rule related cortex api operations
40+
// RuleCommand configures and executes rule related cortex operations
3541
type RuleCommand struct {
3642
ClientConfig client.Config
3743

@@ -41,7 +47,7 @@ type RuleCommand struct {
4147
Namespace string
4248
RuleGroup string
4349

44-
// Load Rules Configs
50+
// Load Rules Config
4551
RuleFilesList []string
4652
RuleFiles string
4753
RuleFilesPath string
@@ -50,38 +56,73 @@ type RuleCommand struct {
5056
IgnoredNamespaces string
5157
ignoredNamespacesMap map[string]struct{}
5258

59+
// Prepare Rules Config
60+
InPlaceEdit bool
61+
AggregationLabel string
62+
5363
DisableColor bool
5464
}
5565

5666
// Register rule related commands and flags with the kingpin application
5767
func (r *RuleCommand) Register(app *kingpin.Application) {
5868
rulesCmd := app.Command("rules", "View & edit rules stored in cortex.").PreAction(r.setup)
59-
rulesCmd.Flag("address", "Address of the cortex cluster, alternatively set CORTEX_ADDRESS.").Envar("CORTEX_ADDRESS").Required().StringVar(&r.ClientConfig.Address)
60-
rulesCmd.Flag("id", "Cortex tenant id, alternatively set CORTEX_TENANT_ID.").Envar("CORTEX_TENANT_ID").Required().StringVar(&r.ClientConfig.ID)
6169
rulesCmd.Flag("key", "Api key to use when contacting cortex, alternatively set $CORTEX_API_KEY.").Default("").Envar("CORTEX_API_KEY").StringVar(&r.ClientConfig.Key)
6270

63-
// List Rules Command
64-
rulesCmd.Command("list", "List the rules currently in the cortex ruler.").Action(r.listRules)
71+
// Register rule commands
72+
listCmd := rulesCmd.
73+
Command("list", "List the rules currently in the cortex ruler.").
74+
Action(r.listRules)
75+
printRulesCmd := rulesCmd.
76+
Command("print", "Print the rules currently in the cortex ruler.").
77+
Action(r.printRules)
78+
getRuleGroupCmd := rulesCmd.
79+
Command("get", "Retreive a rulegroup from the ruler.").
80+
Action(r.getRuleGroup)
81+
deleteRuleGroupCmd := rulesCmd.
82+
Command("delete", "Delete a rulegroup from the ruler.").
83+
Action(r.deleteRuleGroup)
84+
loadRulesCmd := rulesCmd.
85+
Command("load", "load a set of rules to a designated cortex endpoint").
86+
Action(r.loadRules)
87+
diffRulesCmd := rulesCmd.
88+
Command("diff", "diff a set of rules to a designated cortex endpoint").
89+
Action(r.diffRules)
90+
syncRulesCmd := rulesCmd.
91+
Command("sync", "sync a set of rules to a designated cortex endpoint").
92+
Action(r.syncRules)
93+
prepareCmd := rulesCmd.
94+
Command("prepare", "modifies a set of rules by including an specific label in aggregations.").
95+
Action(r.prepare)
96+
97+
// Require Cortex cluster address and tentant ID on all these commands
98+
for _, c := range []*kingpin.CmdClause{listCmd, printRulesCmd, getRuleGroupCmd, deleteRuleGroupCmd, loadRulesCmd, diffRulesCmd, syncRulesCmd} {
99+
c.Flag("address", "Address of the cortex cluster, alternatively set CORTEX_ADDRESS.").
100+
Envar("CORTEX_ADDRESS").
101+
Required().
102+
StringVar(&r.ClientConfig.Address)
103+
104+
c.Flag("id", "Cortex tenant id, alternatively set CORTEX_TENANT_ID.").
105+
Envar("CORTEX_TENANT_ID").
106+
Required().
107+
StringVar(&r.ClientConfig.ID)
108+
}
65109

66110
// Print Rules Command
67-
printRulesCmd := rulesCmd.Command("print", "Print the rules currently in the cortex ruler.").Action(r.printRules)
68111
printRulesCmd.Flag("disable-color", "disable colored output").BoolVar(&r.DisableColor)
69112

70113
// Get RuleGroup Command
71-
getRuleGroupCmd := rulesCmd.Command("get", "Retreive a rulegroup from the ruler.").Action(r.getRuleGroup)
72114
getRuleGroupCmd.Arg("namespace", "Namespace of the rulegroup to retrieve.").Required().StringVar(&r.Namespace)
73115
getRuleGroupCmd.Arg("group", "Name of the rulegroup ot retrieve.").Required().StringVar(&r.RuleGroup)
74116
getRuleGroupCmd.Flag("disable-color", "disable colored output").BoolVar(&r.DisableColor)
75117

76118
// Delete RuleGroup Command
77-
deleteRuleGroupCmd := rulesCmd.Command("delete", "Delete a rulegroup from the ruler.").Action(r.deleteRuleGroup)
78119
deleteRuleGroupCmd.Arg("namespace", "Namespace of the rulegroup to delete.").Required().StringVar(&r.Namespace)
79120
deleteRuleGroupCmd.Arg("group", "Name of the rulegroup ot delete.").Required().StringVar(&r.RuleGroup)
80121

81-
loadRulesCmd := rulesCmd.Command("load", "load a set of rules to a designated cortex endpoint").Action(r.loadRules)
122+
// Load Rules Command
82123
loadRulesCmd.Arg("rule-files", "The rule files to check.").Required().ExistingFilesVar(&r.RuleFilesList)
83124

84-
diffRulesCmd := rulesCmd.Command("diff", "diff a set of rules to a designated cortex endpoint").Action(r.diffRules)
125+
// Diff Command
85126
diffRulesCmd.Flag("ignored-namespaces", "comma-separated list of namespaces to ignore during a diff.").StringVar(&r.IgnoredNamespaces)
86127
diffRulesCmd.Flag("rule-files", "The rule files to check. Flag can be reused to load multiple files.").StringVar(&r.RuleFiles)
87128
diffRulesCmd.Flag(
@@ -90,13 +131,26 @@ func (r *RuleCommand) Register(app *kingpin.Application) {
90131
).StringVar(&r.RuleFilesPath)
91132
diffRulesCmd.Flag("disable-color", "disable colored output").BoolVar(&r.DisableColor)
92133

93-
syncRulesCmd := rulesCmd.Command("sync", "sync a set of rules to a designated cortex endpoint").Action(r.syncRules)
134+
// Sync Command
94135
syncRulesCmd.Flag("ignored-namespaces", "comma-separated list of namespaces to ignore during a sync.").StringVar(&r.IgnoredNamespaces)
95136
syncRulesCmd.Flag("rule-files", "The rule files to check. Flag can be reused to load multiple files.").StringVar(&r.RuleFiles)
96137
syncRulesCmd.Flag(
97138
"rule-dirs",
98139
"Comma seperated list of paths to directories containing rules yaml files. Each file in a directory with a .yml or .yaml suffix will be parsed.",
99140
).StringVar(&r.RuleFilesPath)
141+
142+
// Prepare Command
143+
prepareCmd.Arg("rule-files", "The rule files to check.").Required().ExistingFilesVar(&r.RuleFilesList)
144+
prepareCmd.Flag("rule-files", "The rule files to check. Flag can be reused to load multiple files.").StringVar(&r.RuleFiles)
145+
prepareCmd.Flag(
146+
"rule-dirs",
147+
"Comma seperated list of paths to directories containing rules yaml files. Each file in a directory with a .yml or .yaml suffix will be parsed.",
148+
).StringVar(&r.RuleFilesPath)
149+
prepareCmd.Flag(
150+
"in-place",
151+
"edits the rule file in place",
152+
).Short('i').BoolVar(&r.InPlaceEdit)
153+
prepareCmd.Flag("label", "label to include as part of the aggregations.").Default(defaultPrepareAggregationLabel).Short('l').StringVar(&r.AggregationLabel)
100154
}
101155

102156
func (r *RuleCommand) setup(k *kingpin.ParseContext) error {
@@ -420,3 +474,48 @@ func (r *RuleCommand) executeChanges(ctx context.Context, changes []rules.Namesp
420474
fmt.Printf("Sync Summary: %v Groups Created, %v Groups Updated, %v Groups Deleted\n", created, updated, deleted)
421475
return nil
422476
}
477+
478+
func (r *RuleCommand) prepare(k *kingpin.ParseContext) error {
479+
err := r.setupFiles()
480+
if err != nil {
481+
return errors.Wrap(err, "prepare operation unsuccessful, unable to load rules files")
482+
}
483+
484+
namespaces, err := rules.ParseFiles(r.RuleFilesList)
485+
if err != nil {
486+
return errors.Wrap(err, "prepare operation unsuccessful, unable to parse rules files")
487+
}
488+
489+
var count, mod int
490+
for _, ruleNamespace := range namespaces {
491+
c, m, err := ruleNamespace.AggregateBy(r.AggregationLabel)
492+
if err != nil {
493+
return err
494+
}
495+
496+
count += c
497+
mod += m
498+
}
499+
500+
// now, save all the files
501+
for _, ns := range namespaces {
502+
payload, err := yaml.Marshal(ns)
503+
if err != nil {
504+
return err
505+
}
506+
507+
filepath := ns.Filepath
508+
if !r.InPlaceEdit {
509+
filepath = filepath + ".result"
510+
}
511+
512+
err = ioutil.WriteFile(filepath, payload, 0644)
513+
if err != nil {
514+
return err
515+
}
516+
}
517+
518+
log.Infof("SUCESS: %d rules found, %d modified expressions", count, mod)
519+
520+
return nil
521+
}

pkg/rules/parser.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func ParseFiles(files []string) (map[string]RuleNamespace, error) {
3030
return nil, errFileReadError
3131
}
3232

33+
ns.Filepath = f
34+
3335
// Determine if the namespace is explicitly set. If not
3436
// the file name without the extension is used.
3537
namespace := ns.Namespace

pkg/rules/rules.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package rules
22

33
import (
44
"fmt"
5+
"strings"
56

67
"github.com/prometheus/prometheus/pkg/rulefmt"
8+
"github.com/prometheus/prometheus/promql"
9+
log "github.com/sirupsen/logrus"
710
)
811

912
// RuleNamespace is used to parse a slightly modified prometheus
@@ -12,10 +15,74 @@ import (
1215
type RuleNamespace struct {
1316
// Namespace field only exists for setting namespace in namespace body instead of file name
1417
Namespace string `yaml:"namespace,omitempty"`
18+
Filepath string `yaml:"-"`
1519

1620
Groups []rulefmt.RuleGroup `yaml:"groups"`
1721
}
1822

23+
// AggregateBy Modifies the aggregation rules in groups to include a given Label.
24+
func (r RuleNamespace) AggregateBy(label string) (int, int, error) {
25+
// `count` represents the number of rules we evalated.
26+
// `mod` represents the number of rules we modified - a modification can either be a lint or adding the
27+
// label in the aggregation.
28+
var count, mod int
29+
30+
for i, group := range r.Groups {
31+
for j, rule := range group.Rules {
32+
log.WithFields(log.Fields{"rule": getRuleName(rule)}).Debugf("evaluating...")
33+
exp, err := promql.ParseExpr(rule.Expr)
34+
if err != nil {
35+
return count, mod, err
36+
}
37+
38+
count++
39+
// Given inspect will help us traverse every node in the AST, Let's create the
40+
// function that will modify the labels.
41+
f := exprNodeInspectorFunc(rule, label)
42+
promql.Inspect(exp, f)
43+
44+
// Only modify the ones that actually changed.
45+
if r.Groups[i].Rules[j].Expr != exp.String() {
46+
mod++
47+
r.Groups[i].Rules[j].Expr = exp.String()
48+
}
49+
}
50+
}
51+
52+
return count, mod, nil
53+
}
54+
55+
// exprNodeInspectorFunc returns a PromQL inspector.
56+
// It modifies most PromQL aggregations to include a given label.
57+
func exprNodeInspectorFunc(rule rulefmt.Rule, label string) func(node promql.Node, path []promql.Node) error {
58+
return func(node promql.Node, path []promql.Node) error {
59+
aggregation, ok := node.(*promql.AggregateExpr)
60+
if !ok {
61+
return nil
62+
}
63+
64+
// If the aggregation is about dropping labels (e.g. without), we don't want to modify
65+
// this expression. Omission as long as it is not the cluster label will include it.
66+
// TODO: We probably want to check whenever the label we're trying to include is included in the omission.
67+
if aggregation.Without {
68+
return nil
69+
}
70+
71+
for _, lbl := range aggregation.Grouping {
72+
if lbl == label {
73+
return nil
74+
}
75+
}
76+
77+
log.WithFields(
78+
log.Fields{"rule": getRuleName(rule), "lbls": strings.Join(aggregation.Grouping, ", ")},
79+
).Debugf("aggregation without '%s' label, adding.", label)
80+
81+
aggregation.Grouping = append(aggregation.Grouping, label)
82+
return nil
83+
}
84+
}
85+
1986
// Validate each rule in the rule namespace is valid
2087
func (r RuleNamespace) Validate() []error {
2188
set := map[string]struct{}{}
@@ -63,3 +130,11 @@ func ValidateRuleGroup(g rulefmt.RuleGroup) []error {
63130

64131
return errs
65132
}
133+
134+
func getRuleName(r rulefmt.Rule) string {
135+
if r.Record != "" {
136+
return r.Record
137+
}
138+
139+
return r.Alert
140+
}

0 commit comments

Comments
 (0)