Skip to content

Commit

Permalink
feat: add logQlExpressionUsesFiltersFirst validator (#78)
Browse files Browse the repository at this point in the history
* feat: add logQlExpressionUsesFiltersFirst validator

Signed-off-by: Martin Chodur <m.chodur@seznam.cz>

* Update CHANGELOG.md

---------

Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
  • Loading branch information
FUSAKLA committed Jul 17, 2024
1 parent 8da15be commit 768a47a
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added: :tada: **Support for validation of Loki rules!** Now you can validate Loki rules as well. First two validators are:
- `expressionIsValidLogQL` to check if the expression is a valid LogQL query
- `logQlExpressionUsesRangeAggregation` to check if the LogQL expression uses range aggregation
- `logQlExpressionUsesFiltersFirst` to check if the LogQL expression uses filters first in the query since it is more efficient
- Added: support for alert field `keep_firing_for`
- Added: support for the `query_offset` field in the rule group
- Added: new validator `expressionIsValidPromQL` to check if the expression is a valid PromQL query
Expand Down
8 changes: 8 additions & 0 deletions examples/loki/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ groups:
- name: group1
remote_write:
- url: http://localhost:1234
rules:
- alert: HighRequestLatency
expr: 'sum(rate({job="myjob"} |= "error" | logfmt [5m])) > 0.1'
for: 10m
labels:
severity: page
annotations:
summary: High error rate in logs
1 change: 1 addition & 0 deletions examples/loki/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ validationRules:
validations:
- type: expressionIsValidLogQL
- type: logQlExpressionUsesRangeAggregation
- type: logQlExpressionUsesFiltersFirst
1 change: 1 addition & 0 deletions pkg/validator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var registeredUniversalRuleValidators = map[string]validatorCreator{

"expressionIsValidLogQL": newExpressionIsValidLogQL,
"logQlExpressionUsesRangeAggregation": newLogQLExpressionUsesRangeAggregation,
"logQlExpressionUsesFiltersFirst": newlogQlExpressionUsesFiltersFirst,

"hasSourceTenantsForMetrics": newHasSourceTenantsForMetrics,
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/validator/logql_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,45 @@ func (h logQLExpressionUsesRangeAggregation) Validate(_ unmarshaler.RuleGroup, r
}
return []error{fmt.Errorf("expression %s does not use any of the range aggregation which is required in rules, see https://grafana.com/docs/loki/latest/query/metric_queries/#log-range-aggregations", rule.Expr)}
}

func newlogQlExpressionUsesFiltersFirst(_ yaml.Node) (Validator, error) {
return &logQlExpressionUsesFiltersFirst{}, nil
}

type logQlExpressionUsesFiltersFirst struct{}

func (h logQlExpressionUsesFiltersFirst) String() string {
return "LogQL expression should use a line filter expressions as first in the pipeline to optimize the query efficiency, see https://grafana.com/docs/loki/latest/query/log_queries/#line-filter-expression"
}

func (h logQlExpressionUsesFiltersFirst) Validate(_ unmarshaler.RuleGroup, rule rulefmt.Rule, _ *prometheus.Client) []error {
expr, err := syntax.ParseExpr(rule.Expr)
if err != nil {
return []error{fmt.Errorf("expression %s is not a valid LogQL query: %w", rule.Expr, err)}
}

filterAfterNonFilter := false
expr.Walk(func(e syntax.Expr) {
if _, ok := e.(*syntax.PipelineExpr); ok {
nonFilterEncountered := false
e.Walk(func(e syntax.Expr) {
switch e.(type) {
case *syntax.PipelineExpr:
return
case *syntax.MatchersExpr:
return
case *syntax.LineFilterExpr:
if nonFilterEncountered {
filterAfterNonFilter = true
}
return
}
nonFilterEncountered = true
})
}
})
if !filterAfterNonFilter {
return []error{}
}
return []error{fmt.Errorf("LogQL expression should use a line filter expressions as first in the pipeline to optimize the query efficiency: %s", rule.Expr)}
}
5 changes: 5 additions & 0 deletions pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ var testCases = []struct {
// logQlExpressionUsesRangeAggregation
{name: "logQlExpressionUsesRangeAggregation_OK", validator: logQLExpressionUsesRangeAggregation{}, rule: rulefmt.Rule{Expr: `sum(rate({job="foo"} |= "foo"[1m]))`}, expectedErrors: 0},
{name: "logQlExpressionUsesRangeAggregation_Invalid", validator: logQLExpressionUsesRangeAggregation{}, rule: rulefmt.Rule{Expr: `{job="foo"} |= "foo"`}, expectedErrors: 1},

// logQlExpressionUsesFiltersFirst
{name: "logQlExpressionUsesFiltersFirst_OK", validator: logQlExpressionUsesFiltersFirst{}, rule: rulefmt.Rule{Expr: `{job="foo"} |= "foo" | logfmt`}, expectedErrors: 0},
{name: "logQlExpressionUsesFiltersFirst_Invalid", validator: logQlExpressionUsesFiltersFirst{}, rule: rulefmt.Rule{Expr: `{job="foo"} | logfmt |= "foo"`}, expectedErrors: 1},
{name: "logQlExpressionUsesFiltersFirst_Invalid", validator: logQlExpressionUsesFiltersFirst{}, rule: rulefmt.Rule{Expr: `{job="foo"} |= "foo" | logfmt |= "bar"`}, expectedErrors: 1},
}

func Test(t *testing.T) {
Expand Down

0 comments on commit 768a47a

Please sign in to comment.