Skip to content

Commit 556f276

Browse files
committed
Introduce the rules prepare command
This commit introduces a new command for the cortextool: `rules prepare`. It allows users to "lint" and modify their rules files to include a specific cluster in their aggregations. Cortex users writing from multiple systems to a single tenant is not uncommon when you want to have a global view. In this setup, you'd typically use a label to differentiate identical series coming from different systems. Cortex users transitioning from Prometheus rule evaluation to the Cortex ruler based under this setup could do with having a tool that helps them aggregate by an specific label. This is exactly what this command is for. Signed-off-by: gotjosh <[email protected]>
1 parent deb558b commit 556f276

File tree

3 files changed

+192
-12
lines changed

3 files changed

+192
-12
lines changed

pkg/commands/rules.go

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@ package commands
33
import (
44
"context"
55
"fmt"
6+
"io/ioutil"
67
"os"
78
"path/filepath"
89
"strings"
910
"text/tabwriter"
1011

1112
"github.com/pkg/errors"
1213
"github.com/prometheus/client_golang/prometheus"
14+
"github.com/prometheus/prometheus/pkg/rulefmt"
1315

1416
"github.com/grafana/cortextool/pkg/client"
1517
"github.com/grafana/cortextool/pkg/printer"
1618
"github.com/grafana/cortextool/pkg/rules"
1719
log "github.com/sirupsen/logrus"
1820
"gopkg.in/alecthomas/kingpin.v2"
21+
"gopkg.in/yaml.v2"
22+
)
23+
24+
const (
25+
defaultPrepareAggregationLabel = "cluster"
1926
)
2027

2128
var (
@@ -31,7 +38,7 @@ var (
3138
})
3239
)
3340

34-
// RuleCommand configures and executes rule related cortex api operations
41+
// RuleCommand configures and executes rule related cortex operations
3542
type RuleCommand struct {
3643
ClientConfig client.Config
3744

@@ -41,7 +48,7 @@ type RuleCommand struct {
4148
Namespace string
4249
RuleGroup string
4350

44-
// Load Rules Configs
51+
// Load Rules Config
4552
RuleFilesList []string
4653
RuleFiles string
4754
RuleFilesPath string
@@ -50,38 +57,73 @@ type RuleCommand struct {
5057
IgnoredNamespaces string
5158
ignoredNamespacesMap map[string]struct{}
5259

60+
// Prepare Rules Config
61+
InPlaceEdit bool
62+
AggregationLabel string
63+
5364
DisableColor bool
5465
}
5566

5667
// Register rule related commands and flags with the kingpin application
5768
func (r *RuleCommand) Register(app *kingpin.Application) {
5869
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)
6170
rulesCmd.Flag("key", "Api key to use when contacting cortex, alternatively set $CORTEX_API_KEY.").Default("").Envar("CORTEX_API_KEY").StringVar(&r.ClientConfig.Key)
6271

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

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

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

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

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

84-
diffRulesCmd := rulesCmd.Command("diff", "diff a set of rules to a designated cortex endpoint").Action(r.diffRules)
126+
// Diff Command
85127
diffRulesCmd.Flag("ignored-namespaces", "comma-separated list of namespaces to ignore during a diff.").StringVar(&r.IgnoredNamespaces)
86128
diffRulesCmd.Flag("rule-files", "The rule files to check. Flag can be reused to load multiple files.").StringVar(&r.RuleFiles)
87129
diffRulesCmd.Flag(
@@ -90,13 +132,25 @@ func (r *RuleCommand) Register(app *kingpin.Application) {
90132
).StringVar(&r.RuleFilesPath)
91133
diffRulesCmd.Flag("disable-color", "disable colored output").BoolVar(&r.DisableColor)
92134

93-
syncRulesCmd := rulesCmd.Command("sync", "sync a set of rules to a designated cortex endpoint").Action(r.syncRules)
135+
// Sync Command
94136
syncRulesCmd.Flag("ignored-namespaces", "comma-separated list of namespaces to ignore during a sync.").StringVar(&r.IgnoredNamespaces)
95137
syncRulesCmd.Flag("rule-files", "The rule files to check. Flag can be reused to load multiple files.").StringVar(&r.RuleFiles)
96138
syncRulesCmd.Flag(
97139
"rule-dirs",
98140
"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.",
99141
).StringVar(&r.RuleFilesPath)
142+
143+
// Prepare Command
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,56 @@ 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+
}
522+
523+
func getRuleName(r rulefmt.Rule) string {
524+
if r.Record != "" {
525+
return r.Record
526+
}
527+
528+
return r.Alert
529+
}

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

64127
return errs
65128
}
129+
130+
func getRuleName(r rulefmt.Rule) string {
131+
if r.Record != "" {
132+
return r.Record
133+
}
134+
135+
return r.Alert
136+
}

0 commit comments

Comments
 (0)