mirror of https://github.com/grafana/loki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
7.6 KiB
274 lines
7.6 KiB
package rules
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/prometheus/common/model"
|
|
"github.com/prometheus/prometheus/model/rulefmt"
|
|
"github.com/prometheus/prometheus/promql/parser"
|
|
log "github.com/sirupsen/logrus"
|
|
"go.yaml.in/yaml/v3"
|
|
|
|
logql "github.com/grafana/loki/v3/pkg/logql/syntax"
|
|
|
|
"github.com/grafana/loki/v3/pkg/tool/rules/rwrulefmt"
|
|
)
|
|
|
|
// RuleNamespace is used to parse a slightly modified prometheus
|
|
// rule file format, if no namespace is set, the default namespace
|
|
// is used. Namespace is functionally the same as a file name.
|
|
type RuleNamespace struct {
|
|
// Namespace field only exists for setting namespace in namespace body instead of file name
|
|
Namespace string `yaml:"namespace,omitempty"`
|
|
Filepath string `yaml:"-"`
|
|
|
|
Groups []rwrulefmt.RuleGroup `yaml:"groups"`
|
|
}
|
|
|
|
// LintExpressions runs the `expr` from a rule through the PromQL or LogQL parser and
|
|
// compares its output. If it differs from the parser, it uses the parser's instead.
|
|
func (r RuleNamespace) LintExpressions() (int, int, error) {
|
|
var parseFn func(string) (fmt.Stringer, error)
|
|
queryLanguage := "LogQL"
|
|
|
|
parseFn = func(s string) (fmt.Stringer, error) {
|
|
return logql.ParseExpr(s)
|
|
}
|
|
|
|
// `count` represents the number of rules we evalated.
|
|
// `mod` represents the number of rules linted.
|
|
var count, mod int
|
|
for i, group := range r.Groups {
|
|
for j, rule := range group.Rules {
|
|
log.WithFields(log.Fields{"rule": getRuleName(rule)}).Debugf("linting %s", queryLanguage)
|
|
exp, err := parseFn(rule.Expr)
|
|
if err != nil {
|
|
return count, mod, err
|
|
}
|
|
|
|
count++
|
|
if rule.Expr != exp.String() {
|
|
log.WithFields(log.Fields{
|
|
"rule": getRuleName(rule),
|
|
"currentExpr": rule.Expr,
|
|
"afterExpr": exp.String(),
|
|
}).Debugf("expression differs")
|
|
|
|
mod++
|
|
r.Groups[i].Rules[j].Expr = exp.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
return count, mod, nil
|
|
}
|
|
|
|
// CheckRecordingRules checks that recording rules have at least one colon in their name, this is based
|
|
// on the recording rules best practices here: https://prometheus.io/docs/practices/rules/
|
|
// Returns the number of rules that don't match the requirements.
|
|
func (r RuleNamespace) CheckRecordingRules(strict bool) int {
|
|
var name string
|
|
var count int
|
|
reqChunks := 2
|
|
if strict {
|
|
reqChunks = 3
|
|
}
|
|
for _, group := range r.Groups {
|
|
for _, rule := range group.Rules {
|
|
// Assume if there is a rule.Record that this is a recording rule.
|
|
if rule.Record == "" {
|
|
continue
|
|
}
|
|
name = rule.Record
|
|
log.WithFields(log.Fields{"rule": name}).Debugf("linting recording rule name")
|
|
chunks := strings.Split(name, ":")
|
|
if len(chunks) < reqChunks {
|
|
count++
|
|
log.WithFields(log.Fields{
|
|
"rule": getRuleName(rule),
|
|
"ruleGroup": group.Name,
|
|
"file": r.Filepath,
|
|
"error": "recording rule name does not match level:metric:operation format, must contain at least one colon",
|
|
}).Errorf("bad recording rule name")
|
|
}
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// AggregateBy modifies the aggregation rules in groups to include a given Label.
|
|
// If the applyTo function is provided, the aggregation is applied only to rules
|
|
// for which the applyTo function returns true.
|
|
func (r RuleNamespace) AggregateBy(label string, applyTo func(group rwrulefmt.RuleGroup, rule rulefmt.Rule) bool) (int, int, error) {
|
|
// `count` represents the number of rules we evaluated.
|
|
// `mod` represents the number of rules we modified - a modification can either be a lint or adding the
|
|
// label in the aggregation.
|
|
var count, mod int
|
|
|
|
for i, group := range r.Groups {
|
|
for j, rule := range group.Rules {
|
|
// Skip it if the applyTo function returns false.
|
|
if applyTo != nil && !applyTo(group, rule) {
|
|
log.WithFields(log.Fields{
|
|
"group": group.Name,
|
|
"rule": getRuleName(rule),
|
|
}).Debugf("skipped")
|
|
|
|
count++
|
|
continue
|
|
}
|
|
|
|
log.WithFields(log.Fields{"rule": getRuleName(rule)}).Debugf("evaluating...")
|
|
exp, err := parser.NewParser(parser.Options{}).ParseExpr(rule.Expr)
|
|
if err != nil {
|
|
return count, mod, err
|
|
}
|
|
|
|
count++
|
|
// Given inspect will help us traverse every node in the AST, Let's create the
|
|
// function that will modify the labels.
|
|
f := exprNodeInspectorFunc(rule, label)
|
|
parser.Inspect(exp, f)
|
|
|
|
// Only modify the ones that actually changed.
|
|
if rule.Expr != exp.String() {
|
|
log.WithFields(log.Fields{
|
|
"rule": getRuleName(rule),
|
|
"currentExpr": rule.Expr,
|
|
"afterExpr": exp.String(),
|
|
}).Debugf("expression differs")
|
|
mod++
|
|
r.Groups[i].Rules[j].Expr = exp.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
return count, mod, nil
|
|
}
|
|
|
|
// exprNodeInspectorFunc returns a PromQL inspector.
|
|
// It modifies most PromQL expressions to include a given label.
|
|
func exprNodeInspectorFunc(rule rulefmt.Rule, label string) func(node parser.Node, path []parser.Node) error {
|
|
return func(node parser.Node, _ []parser.Node) error {
|
|
var err error
|
|
switch n := node.(type) {
|
|
case *parser.AggregateExpr:
|
|
err = prepareAggregationExpr(n, label, getRuleName(rule))
|
|
case *parser.BinaryExpr:
|
|
err = prepareBinaryExpr(n, label, getRuleName(rule))
|
|
default:
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
func prepareAggregationExpr(e *parser.AggregateExpr, label string, ruleName string) error {
|
|
// If the aggregation is about dropping labels (e.g. without), we don't want to modify
|
|
// this expression. Omission as long as it is not the cluster label will include it.
|
|
// TODO: We probably want to check whenever the label we're trying to include is included in the omission.
|
|
if e.Without {
|
|
return nil
|
|
}
|
|
|
|
for _, lbl := range e.Grouping {
|
|
// It already has the label we want to aggregate by.
|
|
if lbl == label {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
log.WithFields(
|
|
log.Fields{"rule": ruleName, "lbls": strings.Join(e.Grouping, ", ")},
|
|
).Debugf("aggregation without '%s' label, adding.", label)
|
|
|
|
e.Grouping = append(e.Grouping, label)
|
|
return nil
|
|
}
|
|
|
|
func prepareBinaryExpr(e *parser.BinaryExpr, label string, rule string) error {
|
|
if e.VectorMatching == nil {
|
|
return nil
|
|
}
|
|
|
|
if !e.VectorMatching.On {
|
|
return nil
|
|
}
|
|
|
|
for _, lbl := range e.VectorMatching.MatchingLabels {
|
|
// It already has the label we want to add in the expression.
|
|
if lbl == label {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
log.WithFields(
|
|
log.Fields{"rule": rule, "lbls": strings.Join(e.VectorMatching.MatchingLabels, ", ")},
|
|
).Debugf("binary expression without '%s' label, adding.", label)
|
|
|
|
e.VectorMatching.MatchingLabels = append(e.VectorMatching.MatchingLabels, label)
|
|
return nil
|
|
}
|
|
|
|
// Validate each rule in the rule namespace is valid
|
|
func (r RuleNamespace) Validate() []error {
|
|
set := map[string]struct{}{}
|
|
var errs []error
|
|
|
|
for _, g := range r.Groups {
|
|
if g.Name == "" {
|
|
errs = append(errs, fmt.Errorf("groupname should not be empty"))
|
|
}
|
|
|
|
if _, ok := set[g.Name]; ok {
|
|
errs = append(
|
|
errs,
|
|
fmt.Errorf("groupname: \"%s\" is repeated in the same namespace", g.Name),
|
|
)
|
|
}
|
|
|
|
set[g.Name] = struct{}{}
|
|
|
|
errs = append(errs, ValidateRuleGroup(g)...)
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// ValidateRuleGroup validates a rulegroup
|
|
func ValidateRuleGroup(g rwrulefmt.RuleGroup) []error {
|
|
var errs []error
|
|
for i, r := range g.Rules {
|
|
ruleNode := rulefmt.RuleNode{
|
|
Record: yaml.Node{Value: r.Record},
|
|
Alert: yaml.Node{Value: r.Alert},
|
|
Expr: yaml.Node{Value: r.Expr},
|
|
}
|
|
for _, err := range r.Validate(ruleNode, model.UTF8Validation, parser.NewParser(parser.Options{})) {
|
|
var ruleName string
|
|
if r.Alert != "" {
|
|
ruleName = r.Alert
|
|
} else {
|
|
ruleName = r.Record
|
|
}
|
|
errs = append(errs, &rulefmt.Error{
|
|
Group: g.Name,
|
|
Rule: i,
|
|
RuleName: ruleName,
|
|
Err: err,
|
|
})
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func getRuleName(r rulefmt.Rule) string {
|
|
if r.Record != "" {
|
|
return r.Record
|
|
}
|
|
|
|
return r.Alert
|
|
}
|
|
|