-
Notifications
You must be signed in to change notification settings - Fork 9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rulegroups #2842
Rulegroups #2842
Changes from 11 commits
c843a0c
e8f5566
cea1e99
a48a018
2d1e925
1c08743
c884881
e893c89
8cca666
5ff283a
dc69645
6b70a4d
c472316
37e7b69
592cb00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,11 +20,16 @@ import ( | |
"path/filepath" | ||
"strings" | ||
|
||
yaml "gopkg.in/yaml.v2" | ||
|
||
"github.com/prometheus/common/model" | ||
"github.com/prometheus/common/version" | ||
"github.com/prometheus/prometheus/config" | ||
"github.com/prometheus/prometheus/pkg/rulefmt" | ||
"github.com/prometheus/prometheus/promql" | ||
"github.com/prometheus/prometheus/util/cli" | ||
"github.com/prometheus/prometheus/util/promlint" | ||
"github.com/prometheus/tsdb" | ||
) | ||
|
||
// CheckConfigCmd validates configuration files. | ||
|
@@ -171,16 +176,96 @@ func checkRules(t cli.Term, filename string) (int, error) { | |
return 0, fmt.Errorf("is a directory") | ||
} | ||
|
||
rgs, errs := rulefmt.ParseFile(filename) | ||
if errs != nil { | ||
return 0, tsdb.MultiError(errs) | ||
} | ||
|
||
numRules := 0 | ||
for _, rg := range rgs.Groups { | ||
numRules += len(rg.Rules) | ||
} | ||
|
||
return numRules, nil | ||
} | ||
|
||
// UpdateRulesCmd updates the rule files. | ||
func UpdateRulesCmd(t cli.Term, args ...string) int { | ||
if len(args) == 0 { | ||
t.Infof("usage: promtool update-rules <files>") | ||
return 2 | ||
} | ||
failed := false | ||
|
||
for _, arg := range args { | ||
if err := updateRules(t, arg); err != nil { | ||
t.Errorf(" FAILED: %s", err) | ||
failed = true | ||
} | ||
} | ||
|
||
if failed { | ||
return 1 | ||
} | ||
return 0 | ||
} | ||
|
||
func updateRules(t cli.Term, filename string) error { | ||
t.Infof("Updating %s", filename) | ||
|
||
if stat, err := os.Stat(filename); err != nil { | ||
return fmt.Errorf("cannot get file info") | ||
} else if stat.IsDir() { | ||
return fmt.Errorf("is a directory") | ||
} | ||
|
||
content, err := ioutil.ReadFile(filename) | ||
if err != nil { | ||
return 0, err | ||
return err | ||
} | ||
|
||
rules, err := promql.ParseStmts(string(content)) | ||
if err != nil { | ||
return 0, err | ||
return err | ||
} | ||
|
||
yamlRG := &rulefmt.RuleGroups{ | ||
Version: 1, | ||
Groups: []rulefmt.RuleGroup{{ | ||
Name: filename, | ||
}}, | ||
} | ||
|
||
yamlRules := make([]rulefmt.Rule, 0, len(rules)) | ||
|
||
for _, rule := range rules { | ||
switch r := rule.(type) { | ||
case *promql.AlertStmt: | ||
yamlRules = append(yamlRules, rulefmt.Rule{ | ||
Alert: r.Name, | ||
Expr: r.Expr.String(), | ||
For: model.Duration(r.Duration), | ||
Labels: r.Labels.Map(), | ||
Annotations: r.Annotations.Map(), | ||
}) | ||
case *promql.RecordStmt: | ||
yamlRules = append(yamlRules, rulefmt.Rule{ | ||
Record: r.Name, | ||
Expr: r.Expr.String(), | ||
Labels: r.Labels.Map(), | ||
}) | ||
default: | ||
panic("unknown statement type") | ||
} | ||
} | ||
|
||
yamlRG.Groups[0].Rules = yamlRules | ||
y, err := yaml.Marshal(yamlRG) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because we are now using the plain YAML lib again this will generate rule files with basically random key ordering right? @brian-brazil if yes, this is pretty bad UX just because we don't want to use a 100 LOC wrapper lib that's vendored already anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, let's move forward then. Somewhat sure I've seen things wildely mixed – but maybe I recall incorrectly. |
||
if err != nil { | ||
return err | ||
} | ||
return len(rules), nil | ||
|
||
return ioutil.WriteFile(filename+".yaml", y, 0777) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
var checkMetricsUsage = strings.TrimSpace(` | ||
|
@@ -243,6 +328,11 @@ func main() { | |
Run: CheckMetricsCmd, | ||
}) | ||
|
||
app.Register("update-rules", &cli.Command{ | ||
Desc: "update the rules to the new YAML format", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update rule files to... to be consistent with the above. |
||
Run: UpdateRulesCmd, | ||
}) | ||
|
||
app.Register("version", &cli.Command{ | ||
Desc: "print the version of this binary", | ||
Run: VersionCmd, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// Copyright 2017 The Prometheus Authors | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package rulefmt | ||
|
||
import ( | ||
"io/ioutil" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/prometheus/common/model" | ||
"github.com/prometheus/prometheus/promql" | ||
yaml "gopkg.in/yaml.v2" | ||
) | ||
|
||
// Error represents semantical errors on parsing rule groups. | ||
type Error struct { | ||
Group string | ||
Rule int | ||
Err error | ||
} | ||
|
||
func (err *Error) Error() string { | ||
return errors.Wrapf(err.Err, "group %q, rule %d", err.Group, err.Rule).Error() | ||
} | ||
|
||
// RuleGroups is a set of rule groups that are typically exposed in a file. | ||
type RuleGroups struct { | ||
Version int `yaml:"version"` | ||
Groups []RuleGroup `yaml:"groups"` | ||
} | ||
|
||
// Validate validates all rules in the rule groups. | ||
func (g *RuleGroups) Validate() (errs []error) { | ||
if g.Version != 1 { | ||
errs = append(errs, errors.Errorf("invalid rule group version %d", g.Version)) | ||
} | ||
set := map[string]struct{}{} | ||
|
||
for _, g := range g.Groups { | ||
if g.Name == "" { | ||
errs = append(errs, errors.Errorf("Groupname should not be empty")) | ||
} | ||
|
||
if _, ok := set[g.Name]; ok { | ||
errs = append( | ||
errs, | ||
errors.Errorf("groupname: \"%s\" is repeated in the same file", g.Name), | ||
) | ||
} | ||
|
||
set[g.Name] = struct{}{} | ||
|
||
for i, r := range g.Rules { | ||
for _, err := range r.Validate() { | ||
errs = append(errs, &Error{ | ||
Group: g.Name, | ||
Rule: i, | ||
Err: err, | ||
}) | ||
} | ||
} | ||
} | ||
return errs | ||
} | ||
|
||
// RuleGroup is a list of sequentially evaluated recording and alerting rules. | ||
type RuleGroup struct { | ||
Name string `yaml:"name"` | ||
Interval model.Duration `yaml:"interval,omitempty"` | ||
Rules []Rule `yaml:"rules"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should have the XXX checkOverflow stuff. |
||
} | ||
|
||
// Rule describes an alerting or recording rule. | ||
type Rule struct { | ||
Record string `yaml:"record,omitempty"` | ||
Alert string `yaml:"alert,omitempty"` | ||
Expr string `yaml:"expr"` | ||
For model.Duration `yaml:"for,omitempty"` | ||
Labels map[string]string `yaml:"labels,omitempty"` | ||
Annotations map[string]string `yaml:"annotations,omitempty"` | ||
} | ||
|
||
// Validate the rule and return a list of encountered errors. | ||
func (r *Rule) Validate() (errs []error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to validate that the labelnames/annotations names are valid, and for rules that label values are valid. |
||
if r.Record != "" && r.Alert != "" { | ||
errs = append(errs, errors.Errorf("only one of 'record' and 'alert' must be set")) | ||
} | ||
if r.Record == "" && r.Alert == "" { | ||
errs = append(errs, errors.Errorf("one of 'record' or 'alert' must be set")) | ||
} | ||
|
||
if r.Expr == "" { | ||
errs = append(errs, errors.Errorf("field 'expr' must be set in rule")) | ||
} else if _, err := promql.ParseExpr(r.Expr); err != nil { | ||
errs = append(errs, errors.Errorf("could not parse expression: %s", err)) | ||
} | ||
if r.Record != "" { | ||
if len(r.Annotations) > 0 { | ||
errs = append(errs, errors.Errorf("invalid field 'annotations' in recording rule")) | ||
} | ||
if r.For != 0 { | ||
errs = append(errs, errors.Errorf("invalid field 'for' in recording rule")) | ||
} | ||
} | ||
|
||
for k, v := range r.Labels { | ||
if !model.LabelName(k).IsValid() { | ||
errs = append(errs, errors.Errorf("invalid label name: %s", k)) | ||
} | ||
|
||
if !model.LabelValue(v).IsValid() { | ||
errs = append(errs, errors.Errorf("invalid label value: %s", v)) | ||
} | ||
} | ||
|
||
for k := range r.Annotations { | ||
if !model.LabelName(k).IsValid() { | ||
errs = append(errs, errors.Errorf("invalid annotation name: %s", k)) | ||
} | ||
} | ||
|
||
return errs | ||
} | ||
|
||
// ParseFile parses the rule file and validates it. | ||
func ParseFile(file string) (*RuleGroups, []error) { | ||
b, err := ioutil.ReadFile(file) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
var groups RuleGroups | ||
if err := yaml.Unmarshal(b, &groups); err != nil { | ||
return nil, []error{err} | ||
} | ||
return &groups, groups.Validate() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// Copyright 2017 The Prometheus Authors | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package rulefmt | ||
|
||
import ( | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestParseFileSuccess(t *testing.T) { | ||
if _, errs := ParseFile("testdata/test.yaml"); len(errs) > 0 { | ||
t.Errorf("unexpected errors parsing file") | ||
for _, err := range errs { | ||
t.Error(err) | ||
} | ||
} | ||
} | ||
|
||
func TestParseFileFailure(t *testing.T) { | ||
table := []struct { | ||
filename string | ||
errMsg string | ||
}{ | ||
{ | ||
filename: "duplicate_grp.bad.yaml", | ||
errMsg: "groupname: \"yolo\" is repeated in the same file", | ||
}, | ||
{ | ||
filename: "noversion.bad.yaml", | ||
errMsg: "invalid rule group version 0", | ||
}, | ||
{ | ||
filename: "bad_expr.bad.yaml", | ||
errMsg: "parse error", | ||
}, | ||
{ | ||
filename: "record_and_alert.bad.yaml", | ||
errMsg: "only one of 'record' and 'alert' must be set", | ||
}, | ||
{ | ||
filename: "no_rec_alert.bad.yaml", | ||
errMsg: "one of 'record' or 'alert' must be set", | ||
}, | ||
{ | ||
filename: "noexpr.bad.yaml", | ||
errMsg: "field 'expr' must be set in rule", | ||
}, | ||
{ | ||
filename: "bad_lname.bad.yaml", | ||
errMsg: "invalid label name", | ||
}, | ||
{ | ||
filename: "bad_annotation.bad.yaml", | ||
errMsg: "invalid annotation name", | ||
}, | ||
} | ||
|
||
for _, c := range table { | ||
_, errs := ParseFile(filepath.Join("testdata", c.filename)) | ||
if errs == nil { | ||
t.Errorf("Expected error parsing %s but got none", c.filename) | ||
continue | ||
} | ||
if !strings.Contains(errs[0].Error(), c.errMsg) { | ||
t.Errorf("Expected error for %s to contain %q but got: %s", c.filename, c.errMsg, errs) | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
rules: | ||
- alert: hola | ||
expr: 1 | ||
annotations: | ||
ins-tance: localhost |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
rules: | ||
- record: yolo | ||
expr: rate(hi) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
rules: | ||
- record: hola | ||
expr: 1 | ||
labels: | ||
ins-tance: localhost |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
- name: yolo |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
rules: | ||
- expr: 1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
version: 1 | ||
groups: | ||
- name: yolo | ||
rules: | ||
- record: ylo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same about import
tsdb
for this and probably better returning the actual slice so the errors can be logged as one per line.tsdb.MultiError
will just concat them with;
which will be impossible to debug with.