Skip to content

Commit

Permalink
feat: add new validators alertNameMatchesRegexp, groupNameMatchesRege…
Browse files Browse the repository at this point in the history
…xp, recordedMetricNameMatchesRegexp (#81)

Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
  • Loading branch information
FUSAKLA authored Jul 21, 2024
1 parent 4b94538 commit 0f7359f
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added: new config options to the prometheus section of config:
- `queryOffset`: Specify offset(delay) of the query (useful for consistency if using remote write for example).
- `queryLookback`: How long into the past to look in queries supporting time range (just metadata queries for now).
- Added: New validator `alertNameMatchesRegexp` to check if the alert name matches the regexp.
- Added: New validator `groupNameMatchesRegexp` to check if the rule group name matches the regexp.
- Added: New validator `recordedMetricNameMatchesRegexp` to check if the recorded metric name matches the regexp.
- Fixed: Loading glob patterns in the file paths to rules
- Fixed: Params of the `expressionCanBeEvaluated` validator were ignored, this is now fixed.
- Updated: Prometheus and other dependencies
Expand Down
40 changes: 40 additions & 0 deletions docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ All the supported validations are listed here. The validations are grouped by th
- [`hasValidPartialResponseStrategy`](#hasvalidpartialresponsestrategy)
- [`maxRulesPerGroup`](#maxrulespergroup)
- [`hasValidLimit`](#hasvalidlimit)
- [`groupNameMatchesRegexp`](#groupnamematchesregexp)
- [`hasAllowedQueryOffset`](#hasallowedqueryoffset)
- [Universal rule validators](#universal-rule-validators)
- [Labels](#labels)
- [`hasLabels`](#haslabels)
Expand Down Expand Up @@ -53,7 +55,9 @@ All the supported validations are listed here. The validations are grouped by th
- [Other](#other-1)
- [`forIsNotLongerThan`](#forisnotlongerthan)
- [`keepFiringForIsNotLongerThan`](#keepfiringforisnotlongerthan)
- [`alertNameMatchesRegexp`](#alertnamematchesregexp)
- [Recording rules validators](#recording-rules-validators)
- [`recordedMetricNameMatchesRegexp`](#recordedmetricnamematchesregexp)



Expand Down Expand Up @@ -120,6 +124,24 @@ params:
limit: 10
```

### `groupNameMatchesRegexp`

Fails if the group name does not match the specified regular expression.

```yaml
params:
regexp: "[A-Z]\s+"
```

### `hasAllowedQueryOffset`

Fails if the rule group has the `query_offset` out of the configured range.

```yaml
params:
minimum: <duration>
maximum: <duration> # Optional, default is infinity
```

## Universal rule validators
Validators that can be used on `All rules`, `Recording rule` and `Alert` scopes.
Expand Down Expand Up @@ -464,5 +486,23 @@ params:
limit: "1h"
```

#### `alertNameMatchesRegexp`

Fails if the alert name does not match the specified regular expression.

```yaml
params:
regexp: "[A-Z]\s+"
```

## Recording rules validators
Validators that can be used on `Recording rule` scope.

#### `recordedMetricNameMatchesRegexp`

Fails if the name of the recorded metric does not match the specified regular expression.

```yaml
params:
regexp: "[^:]+:[^:]+:[^:]+"
```
36 changes: 36 additions & 0 deletions pkg/validator/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -100,3 +101,38 @@ func (h validateLabelTemplates) Validate(_ unmarshaler.RuleGroup, rule rulefmt.R
}
return errs
}

func newAlertNameMatchesRegexp(paramsConfig yaml.Node) (Validator, error) {
params := struct {
Regexp string `yaml:"regexp"`
}{}
if err := paramsConfig.Decode(&params); err != nil {
return nil, err
}
if params.Regexp == "" {
return nil, fmt.Errorf("missing pattern")
}
r, err := regexp.Compile(params.Regexp)
if err != nil {
return nil, fmt.Errorf("invalid pattern %s: %w", params.Regexp, err)
}
return &alertNameMatchesRegexp{
pattern: r,
}, nil
}

type alertNameMatchesRegexp struct {
pattern *regexp.Regexp
}

func (h alertNameMatchesRegexp) String() string {
return fmt.Sprintf("Alert name matches regexp: %s", h.pattern.String())
}

func (h alertNameMatchesRegexp) Validate(_ unmarshaler.RuleGroup, rule rulefmt.Rule, _ *prometheus.Client) []error {
var errs []error
if !h.pattern.MatchString(rule.Alert) {
errs = append(errs, fmt.Errorf("alert name %s does not match pattern %s", rule.Alert, h.pattern.String()))
}
return errs
}
7 changes: 6 additions & 1 deletion pkg/validator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ var registeredUniversalRuleValidators = map[string]validatorCreator{
"hasSourceTenantsForMetrics": newHasSourceTenantsForMetrics,
}

var registeredRecordingRuleValidators = map[string]validatorCreator{}
var registeredRecordingRuleValidators = map[string]validatorCreator{
"recordedMetricNameMatchesRegexp": newRecordedMetricNameMatchesRegexp,
}

var registeredAlertValidators = map[string]validatorCreator{
"forIsNotLongerThan": newForIsNotLongerThan,
"keepFiringForIsNotLongerThan": newKeepFiringForIsNotLongerThan,
"alertNameMatchesRegexp": newAlertNameMatchesRegexp,

"validateAnnotationTemplates": newValidateAnnotationTemplates,
"annotationIsValidPromQL": newAnnotationIsValidPromQL,
Expand All @@ -63,6 +66,8 @@ var registeredGroupValidators = map[string]validatorCreator{
"hasValidPartialResponseStrategy": newHasValidPartialResponseStrategy,
"maxRulesPerGroup": newMaxRulesPerGroup,
"hasAllowedLimit": newHasAllowedLimit,
"groupNameMatchesRegexp": newGroupNameMatchesRegexp,
"hasAllowedQueryOffset": newHasAllowedQueryOffset,
}

var (
Expand Down
75 changes: 75 additions & 0 deletions pkg/validator/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package validator

import (
"fmt"
"regexp"
"strings"

"github.com/fusakla/promruval/v2/pkg/prometheus"
Expand Down Expand Up @@ -185,3 +186,77 @@ func (h hasAllowedLimit) Validate(group unmarshaler.RuleGroup, _ rulefmt.Rule, _
}
return []error{}
}

func newHasAllowedQueryOffset(paramsConfig yaml.Node) (Validator, error) {
params := struct {
Minimum model.Duration `yaml:"minimum"`
Maximum model.Duration `yaml:"maximum"`
}{}
if err := paramsConfig.Decode(&params); err != nil {
return nil, err
}
if params.Minimum > params.Maximum {
return nil, fmt.Errorf("minimum is greater than maximum")
}
if params.Maximum == 0 && params.Minimum == 0 {
return nil, fmt.Errorf("minimum or maximum must be set")
}
if params.Maximum == 0 {
params.Maximum = model.Duration(1<<63 - 1)
}

return &hasAllowedQueryOffset{min: params.Minimum, max: params.Maximum}, nil
}

type hasAllowedQueryOffset struct {
min model.Duration
max model.Duration
}

func (h hasAllowedQueryOffset) String() string {
return fmt.Sprintf("group query_offset is higher than %s and lowed then %s", h.min, h.max)
}

func (h hasAllowedQueryOffset) Validate(group unmarshaler.RuleGroup, _ rulefmt.Rule, _ *prometheus.Client) []error {
if group.QueryOffset > h.max {
return []error{fmt.Errorf("group has query_offset %s, allowed maximum is %s", group.QueryOffset, h.max)}
} else if group.QueryOffset < h.min {
return []error{fmt.Errorf("group has query_offset %s, allowed minimum is %s", group.QueryOffset, h.min)}
}
return []error{}
}

func newGroupNameMatchesRegexp(paramsConfig yaml.Node) (Validator, error) {
params := struct {
Regexp string `yaml:"regexp"`
}{}
if err := paramsConfig.Decode(&params); err != nil {
return nil, err
}
if params.Regexp == "" {
return nil, fmt.Errorf("missing regexp")
}
r, err := regexp.Compile(params.Regexp)
if err != nil {
return nil, fmt.Errorf("invalid regexp %s: %w", params.Regexp, err)
}
return &groupNameMatchesRegexp{
pattern: r,
}, nil
}

type groupNameMatchesRegexp struct {
pattern *regexp.Regexp
}

func (h groupNameMatchesRegexp) String() string {
return fmt.Sprintf("Group name matches regexp: %s", h.pattern.String())
}

func (h groupNameMatchesRegexp) Validate(group unmarshaler.RuleGroup, _ rulefmt.Rule, _ *prometheus.Client) []error {
var errs []error
if !h.pattern.MatchString(group.Name) {
errs = append(errs, fmt.Errorf("group name %s does not match regexp %s", group.Name, h.pattern.String()))
}
return errs
}
46 changes: 46 additions & 0 deletions pkg/validator/recording_rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package validator

import (
"fmt"
"regexp"

"github.com/fusakla/promruval/v2/pkg/prometheus"
"github.com/fusakla/promruval/v2/pkg/unmarshaler"
"github.com/prometheus/prometheus/model/rulefmt"
"gopkg.in/yaml.v3"
)

func newRecordedMetricNameMatchesRegexp(paramsConfig yaml.Node) (Validator, error) {
params := struct {
Regexp string `yaml:"regexp"`
}{}
if err := paramsConfig.Decode(&params); err != nil {
return nil, err
}
if params.Regexp == "" {
return nil, fmt.Errorf("missing regexp")
}
r, err := regexp.Compile(params.Regexp)
if err != nil {
return nil, fmt.Errorf("invalid regexp %s: %w", params.Regexp, err)
}
return &recordedMetricNameMatchesRegexp{
pattern: r,
}, nil
}

type recordedMetricNameMatchesRegexp struct {
pattern *regexp.Regexp
}

func (h recordedMetricNameMatchesRegexp) String() string {
return fmt.Sprintf("Recorded metric name matches regexp: %s", h.pattern.String())
}

func (h recordedMetricNameMatchesRegexp) Validate(_ unmarshaler.RuleGroup, rule rulefmt.Rule, _ *prometheus.Client) []error {
var errs []error
if !h.pattern.MatchString(rule.Record) {
errs = append(errs, fmt.Errorf("recorded metric name %s does not match pattern %s", rule.Alert, h.pattern.String()))
}
return errs
}
17 changes: 17 additions & 0 deletions pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,23 @@ var testCases = []struct {
{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},

// alertNameMatchesRegexp
{name: "alertNameMatchesRegexp_Valid", validator: alertNameMatchesRegexp{pattern: regexp.MustCompile("Foo.*")}, rule: rulefmt.Rule{Alert: `FooBAr`}, expectedErrors: 0},
{name: "alertNameMatchesRegexp_NotMatch", validator: alertNameMatchesRegexp{pattern: regexp.MustCompile("Foo.*")}, rule: rulefmt.Rule{Alert: `Bar`}, expectedErrors: 1},

// recordedMetricNameMatchesRegexp
{name: "recordedMetricNameMatchesRegexp_Matches", validator: recordedMetricNameMatchesRegexp{pattern: regexp.MustCompile("[^:]+:[^:]+:[^:]+")}, rule: rulefmt.Rule{Record: `cluster:foo_bar:avg`}, expectedErrors: 0},
{name: "recordedMetricNameMatchesRegexp_notMatches", validator: recordedMetricNameMatchesRegexp{pattern: regexp.MustCompile("[^:]+:[^:]+:[^:]+")}, rule: rulefmt.Rule{Record: `foo_bar`}, expectedErrors: 1},

// hasAllowedQueryOffset
{name: "hasAllowedQueryOffset_valid", validator: hasAllowedQueryOffset{min: model.Duration(time.Second), max: model.Duration(time.Minute)}, group: unmarshaler.RuleGroup{QueryOffset: model.Duration(time.Second * 30)}, expectedErrors: 0},
{name: "hasAllowedQueryOffset_tooHigh", validator: hasAllowedQueryOffset{min: model.Duration(time.Second), max: model.Duration(time.Minute)}, group: unmarshaler.RuleGroup{QueryOffset: model.Duration(time.Minute * 2)}, expectedErrors: 1},
{name: "hasAllowedQueryOffset_tooLow", validator: hasAllowedQueryOffset{min: model.Duration(time.Minute), max: model.Duration(time.Hour)}, group: unmarshaler.RuleGroup{QueryOffset: model.Duration(time.Second)}, expectedErrors: 1},

// groupNameMatchesRegexp
{name: "groupNameMatchesRegexp_valid", validator: groupNameMatchesRegexp{pattern: regexp.MustCompile(`^[A-Z]\S+$`)}, group: unmarshaler.RuleGroup{Name: "TestGroup"}, expectedErrors: 0},
{name: "groupNameMatchesRegexp_invalid", validator: groupNameMatchesRegexp{pattern: regexp.MustCompile(`^[A-Z]\S+$`)}, group: unmarshaler.RuleGroup{Name: "Test Group"}, expectedErrors: 1},
}

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

0 comments on commit 0f7359f

Please sign in to comment.