Commit 84e6459c authored by Harkishen Singh's avatar Harkishen Singh Committed by Brian Brazil
Browse files

Adds support for line-column numbers for invalid rules, promtool (#6533)


Signed-off-by: default avatarHarkishen Singh <harkishensingh@hotmail.com>
parent 3a204be6
...@@ -347,11 +347,11 @@ func checkDuplicates(groups []rulefmt.RuleGroup) []compareRuleType { ...@@ -347,11 +347,11 @@ func checkDuplicates(groups []rulefmt.RuleGroup) []compareRuleType {
return duplicates return duplicates
} }
func ruleMetric(rule rulefmt.Rule) string { func ruleMetric(rule rulefmt.RuleNode) string {
if rule.Alert != "" { if rule.Alert.Value != "" {
return rule.Alert return rule.Alert.Value
} }
return rule.Record return rule.Record.Value
} }
var checkMetricsUsage = strings.TrimSpace(` var checkMetricsUsage = strings.TrimSpace(`
......
...@@ -50,6 +50,7 @@ require ( ...@@ -50,6 +50,7 @@ require (
gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 gopkg.in/fsnotify/fsnotify.v1 v1.4.7
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
k8s.io/api v0.0.0-20190813020757-36bff7324fb7 k8s.io/api v0.0.0-20190813020757-36bff7324fb7
k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010 k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
......
...@@ -482,6 +482,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl ...@@ -482,6 +482,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
......
...@@ -21,7 +21,7 @@ import ( ...@@ -21,7 +21,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v3"
"github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/pkg/timestamp"
"github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql"
...@@ -33,11 +33,24 @@ type Error struct { ...@@ -33,11 +33,24 @@ type Error struct {
Group string Group string
Rule int Rule int
RuleName string RuleName string
Err error Err WrappedError
}
// WrappedError wraps error with the yaml node which can be used to represent
// the line and column numbers of the error.
type WrappedError struct {
err error
node *yaml.Node
nodeAlt *yaml.Node
} }
func (err *Error) Error() string { func (err *Error) Error() string {
return errors.Wrapf(err.Err, "group %q, rule %d, %q", err.Group, err.Rule, err.RuleName).Error() if err.Err.nodeAlt != nil {
return errors.Wrapf(err.Err.err, "%d:%d: %d:%d: group %q, rule %d, %q", err.Err.node.Line, err.Err.node.Column, err.Err.nodeAlt.Line, err.Err.nodeAlt.Column, err.Group, err.Rule, err.RuleName).Error()
} else if err.Err.node != nil {
return errors.Wrapf(err.Err.err, "%d:%d: group %q, rule %d, %q", err.Err.node.Line, err.Err.node.Column, err.Group, err.Rule, err.RuleName).Error()
}
return errors.Wrapf(err.Err.err, "group %q, rule %d, %q", err.Group, err.Rule, err.RuleName).Error()
} }
// RuleGroups is a set of rule groups that are typically exposed in a file. // RuleGroups is a set of rule groups that are typically exposed in a file.
...@@ -45,28 +58,32 @@ type RuleGroups struct { ...@@ -45,28 +58,32 @@ type RuleGroups struct {
Groups []RuleGroup `yaml:"groups"` Groups []RuleGroup `yaml:"groups"`
} }
type ruleGroups struct {
Groups []yaml.Node `yaml:"groups"`
}
// Validate validates all rules in the rule groups. // Validate validates all rules in the rule groups.
func (g *RuleGroups) Validate() (errs []error) { func (g *RuleGroups) Validate(node ruleGroups) (errs []error) {
set := map[string]struct{}{} set := map[string]struct{}{}
for _, g := range g.Groups { for j, g := range g.Groups {
if g.Name == "" { if g.Name == "" {
errs = append(errs, errors.Errorf("Groupname should not be empty")) errs = append(errs, errors.Errorf("%d:%d: Groupname should not be empty", node.Groups[j].Line, node.Groups[j].Column))
} }
if _, ok := set[g.Name]; ok { if _, ok := set[g.Name]; ok {
errs = append( errs = append(
errs, errs,
errors.Errorf("groupname: \"%s\" is repeated in the same file", g.Name), errors.Errorf("%d:%d: groupname: \"%s\" is repeated in the same file", node.Groups[j].Line, node.Groups[j].Column, g.Name),
) )
} }
set[g.Name] = struct{}{} set[g.Name] = struct{}{}
for i, r := range g.Rules { for i, r := range g.Rules {
for _, err := range r.Validate() { for _, node := range r.Validate() {
var ruleName string var ruleName yaml.Node
if r.Alert != "" { if r.Alert.Value != "" {
ruleName = r.Alert ruleName = r.Alert
} else { } else {
ruleName = r.Record ruleName = r.Record
...@@ -74,8 +91,8 @@ func (g *RuleGroups) Validate() (errs []error) { ...@@ -74,8 +91,8 @@ func (g *RuleGroups) Validate() (errs []error) {
errs = append(errs, &Error{ errs = append(errs, &Error{
Group: g.Name, Group: g.Name,
Rule: i, Rule: i,
RuleName: ruleName, RuleName: ruleName.Value,
Err: err, Err: node,
}) })
} }
} }
...@@ -88,7 +105,7 @@ func (g *RuleGroups) Validate() (errs []error) { ...@@ -88,7 +105,7 @@ func (g *RuleGroups) Validate() (errs []error) {
type RuleGroup struct { type RuleGroup struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Interval model.Duration `yaml:"interval,omitempty"` Interval model.Duration `yaml:"interval,omitempty"`
Rules []Rule `yaml:"rules"` Rules []RuleNode `yaml:"rules"`
} }
// Rule describes an alerting or recording rule. // Rule describes an alerting or recording rule.
...@@ -101,55 +118,104 @@ type Rule struct { ...@@ -101,55 +118,104 @@ type Rule struct {
Annotations map[string]string `yaml:"annotations,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty"`
} }
// RuleNode adds yaml.v3 layer to support line and column outputs for invalid rules.
type RuleNode struct {
Record yaml.Node `yaml:"record,omitempty"`
Alert yaml.Node `yaml:"alert,omitempty"`
Expr yaml.Node `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. // Validate the rule and return a list of encountered errors.
func (r *Rule) Validate() (errs []error) { func (r *RuleNode) Validate() (nodes []WrappedError) {
if r.Record != "" && r.Alert != "" { if r.Record.Value != "" && r.Alert.Value != "" {
errs = append(errs, errors.Errorf("only one of 'record' and 'alert' must be set")) nodes = append(nodes, WrappedError{
err: errors.Errorf("only one of 'record' and 'alert' must be set"),
node: &r.Record,
nodeAlt: &r.Alert,
})
} }
if r.Record == "" && r.Alert == "" { if r.Record.Value == "" && r.Alert.Value == "" {
errs = append(errs, errors.Errorf("one of 'record' or 'alert' must be set")) if r.Record.Value == "0" {
nodes = append(nodes, WrappedError{
err: errors.Errorf("one of 'record' or 'alert' must be set"),
node: &r.Alert,
})
} else {
nodes = append(nodes, WrappedError{
err: errors.Errorf("one of 'record' or 'alert' must be set"),
node: &r.Record,
})
}
} }
if r.Expr == "" { if r.Expr.Value == "" {
errs = append(errs, errors.Errorf("field 'expr' must be set in rule")) nodes = append(nodes, WrappedError{
} else if _, err := promql.ParseExpr(r.Expr); err != nil { err: errors.Errorf("field 'expr' must be set in rule"),
errs = append(errs, errors.Wrap(err, "could not parse expression")) node: &r.Expr,
})
} else if _, err := promql.ParseExpr(r.Expr.Value); err != nil {
nodes = append(nodes, WrappedError{
err: errors.Wrapf(err, "could not parse expression"),
node: &r.Expr,
})
} }
if r.Record != "" { if r.Record.Value != "" {
if len(r.Annotations) > 0 { if len(r.Annotations) > 0 {
errs = append(errs, errors.Errorf("invalid field 'annotations' in recording rule")) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid field 'annotations' in recording rule"),
node: &r.Record,
})
} }
if r.For != 0 { if r.For != 0 {
errs = append(errs, errors.Errorf("invalid field 'for' in recording rule")) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid field 'for' in recording rule"),
node: &r.Record,
})
} }
if !model.IsValidMetricName(model.LabelValue(r.Record)) { if !model.IsValidMetricName(model.LabelValue(r.Record.Value)) {
errs = append(errs, errors.Errorf("invalid recording rule name: %s", r.Record)) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid recording rule name: %s", r.Record.Value),
node: &r.Record,
})
} }
} }
for k, v := range r.Labels { for k, v := range r.Labels {
if !model.LabelName(k).IsValid() { if !model.LabelName(k).IsValid() {
errs = append(errs, errors.Errorf("invalid label name: %s", k)) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid label name: %s", k),
})
} }
if !model.LabelValue(v).IsValid() { if !model.LabelValue(v).IsValid() {
errs = append(errs, errors.Errorf("invalid label value: %s", v)) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid label value: %s", v),
})
} }
} }
for k := range r.Annotations { for k := range r.Annotations {
if !model.LabelName(k).IsValid() { if !model.LabelName(k).IsValid() {
errs = append(errs, errors.Errorf("invalid annotation name: %s", k)) nodes = append(nodes, WrappedError{
err: errors.Errorf("invalid annotation name: %s", k),
})
} }
} }
return append(errs, testTemplateParsing(r)...) for _, err := range testTemplateParsing(r) {
nodes = append(nodes, WrappedError{err: err})
}
return
} }
// testTemplateParsing checks if the templates used in labels and annotations // testTemplateParsing checks if the templates used in labels and annotations
// of the alerting rules are parsed correctly. // of the alerting rules are parsed correctly.
func testTemplateParsing(rl *Rule) (errs []error) { func testTemplateParsing(rl *RuleNode) (errs []error) {
if rl.Alert == "" { if rl.Alert.Value == "" {
// Not an alerting rule. // Not an alerting rule.
return errs return errs
} }
...@@ -165,7 +231,7 @@ func testTemplateParsing(rl *Rule) (errs []error) { ...@@ -165,7 +231,7 @@ func testTemplateParsing(rl *Rule) (errs []error) {
tmpl := template.NewTemplateExpander( tmpl := template.NewTemplateExpander(
context.TODO(), context.TODO(),
strings.Join(append(defs, text), ""), strings.Join(append(defs, text), ""),
"__alert_"+rl.Alert, "__alert_"+rl.Alert.Value,
tmplData, tmplData,
model.Time(timestamp.FromTime(time.Now())), model.Time(timestamp.FromTime(time.Now())),
nil, nil,
...@@ -195,11 +261,16 @@ func testTemplateParsing(rl *Rule) (errs []error) { ...@@ -195,11 +261,16 @@ func testTemplateParsing(rl *Rule) (errs []error) {
// Parse parses and validates a set of rules. // Parse parses and validates a set of rules.
func Parse(content []byte) (*RuleGroups, []error) { func Parse(content []byte) (*RuleGroups, []error) {
var groups RuleGroups var (
if err := yaml.UnmarshalStrict(content, &groups); err != nil { groups RuleGroups
node ruleGroups
)
err := yaml.Unmarshal(content, &groups)
_err := yaml.Unmarshal(content, &node)
if err != nil || _err != nil {
return nil, []error{err} return nil, []error{err}
} }
return &groups, groups.Validate() return &groups, groups.Validate(node)
} }
// ParseFile reads and parses rules from a file. // ParseFile reads and parses rules from a file.
......
...@@ -931,14 +931,14 @@ func (m *Manager) LoadGroups( ...@@ -931,14 +931,14 @@ func (m *Manager) LoadGroups(
rules := make([]Rule, 0, len(rg.Rules)) rules := make([]Rule, 0, len(rg.Rules))
for _, r := range rg.Rules { for _, r := range rg.Rules {
expr, err := promql.ParseExpr(r.Expr) expr, err := promql.ParseExpr(r.Expr.Value)
if err != nil { if err != nil {
return nil, []error{errors.Wrap(err, fn)} return nil, []error{errors.Wrap(err, fn)}
} }
if r.Alert != "" { if r.Alert.Value != "" {
rules = append(rules, NewAlertingRule( rules = append(rules, NewAlertingRule(
r.Alert, r.Alert.Value,
expr, expr,
time.Duration(r.For), time.Duration(r.For),
labels.FromMap(r.Labels), labels.FromMap(r.Labels),
...@@ -950,7 +950,7 @@ func (m *Manager) LoadGroups( ...@@ -950,7 +950,7 @@ func (m *Manager) LoadGroups(
continue continue
} }
rules = append(rules, NewRecordingRule( rules = append(rules, NewRecordingRule(
r.Record, r.Record.Value,
expr, expr,
labels.FromMap(r.Labels), labels.FromMap(r.Labels),
)) ))
......
...@@ -756,14 +756,52 @@ func TestUpdate(t *testing.T) { ...@@ -756,14 +756,52 @@ func TestUpdate(t *testing.T) {
// Change group rules and reload. // Change group rules and reload.
for i, g := range rgs.Groups { for i, g := range rgs.Groups {
for j, r := range g.Rules { for j, r := range g.Rules {
rgs.Groups[i].Rules[j].Expr = fmt.Sprintf("%s * 0", r.Expr) rgs.Groups[i].Rules[j].Expr.SetString(fmt.Sprintf("%s * 0", r.Expr.Value))
} }
} }
reloadAndValidate(rgs, t, tmpFile, ruleManager, expected, ogs) reloadAndValidate(rgs, t, tmpFile, ruleManager, expected, ogs)
} }
// ruleGroupsTest for running tests over rules.
type ruleGroupsTest struct {
Groups []ruleGroupTest `yaml:"groups"`
}
// ruleGroupTest forms a testing struct for running tests over rules.
type ruleGroupTest struct {
Name string `yaml:"name"`
Interval model.Duration `yaml:"interval,omitempty"`
Rules []rulefmt.Rule `yaml:"rules"`
}
func formatRules(r *rulefmt.RuleGroups) ruleGroupsTest {
grps := r.Groups
tmp := []ruleGroupTest{}
for _, g := range grps {
rtmp := []rulefmt.Rule{}
for _, r := range g.Rules {
rtmp = append(rtmp, rulefmt.Rule{
Record: r.Record.Value,
Alert: r.Alert.Value,
Expr: r.Expr.Value,
For: r.For,
Labels: r.Labels,
Annotations: r.Annotations,
})
}
tmp = append(tmp, ruleGroupTest{
Name: g.Name,
Interval: g.Interval,
Rules: rtmp,
})
}
return ruleGroupsTest{
Groups: tmp,
}
}
func reloadAndValidate(rgs *rulefmt.RuleGroups, t *testing.T, tmpFile *os.File, ruleManager *Manager, expected map[string]labels.Labels, ogs map[string]*Group) { func reloadAndValidate(rgs *rulefmt.RuleGroups, t *testing.T, tmpFile *os.File, ruleManager *Manager, expected map[string]labels.Labels, ogs map[string]*Group) {
bs, err := yaml.Marshal(rgs) bs, err := yaml.Marshal(formatRules(rgs))
testutil.Ok(t, err) testutil.Ok(t, err)
tmpFile.Seek(0, 0) tmpFile.Seek(0, 0)
_, err = tmpFile.Write(bs) _, err = tmpFile.Write(bs)
......
language: go
go:
- "1.4.x"
- "1.5.x"
- "1.6.x"
- "1.7.x"
- "1.8.x"
- "1.9.x"
- "1.10.x"
- "1.11.x"
- "1.12.x"
- "1.13.x"
- "tip"
go_import_path: gopkg.in/yaml.v3
This project is covered by two different licenses: MIT and Apache.
#### MIT License ####
The following files were ported to Go from C files of libyaml, and thus
are still covered by their original MIT license, with the additional
copyright staring in 2011 when the project was ported over:
apic.go emitterc.go parserc.go readerc.go scannerc.go
writerc.go yamlh.go yamlprivateh.go
Copyright (c) 2006-2010 Kirill Simonov
Copyright (c) 2006-2011 Kirill Simonov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### Apache License ###
All the remaining project files are covered by the Apache license:
Copyright (c) 2011-2019 Canonical Ltd
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.
Copyright 2011-2016 Canonical Ltd.
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.
# YAML support for the Go language
Introduction
------------
The yaml package enables Go programs to comfortably encode and decode YAML
values. It was developed within [Canonical](https://www.canonical.com) as
part of the [juju](https://juju.ubuntu.com) project, and is based on a
pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML)
C library to parse and generate YAML data quickly and reliably.
Compatibility
-------------
The yaml package supports most of YAML 1.2, but preserves some behavior
from 1.1 for backwards compatibility.
Specifically, as of v3 of the yaml package:
- YAML 1.1 bools (_yes/no, on/off_) are supported as long as they are being
decoded into a typed bool value. Otherwise they behave as a string. Booleans
in YAML 1.2 are _true/false_ only.
- Octals encode and decode as _0777_ per YAML 1.1, rather than _0o777_
as specified in YAML 1.2, because most parsers still use the old format.
Octals in the _0o777_ format are supported though, so new files work.
- Does not support base-60 floats. These are gone from YAML 1.2, and were
actually never supported by this package as it's clearly a poor choice.
and offers backwards
compatibility with YAML 1.1 in some cases.
1.2, including support for
anchors, tags, map merging, etc. Multi-document unmarshalling is not yet
implemented, and base-60 floats from YAML 1.1 are purposefully not
supported since they're a poor design and are gone in YAML 1.2.
Installation and usage
----------------------
The import path for the package is *gopkg.in/yaml.v3*.
To install it, run:
go get gopkg.in/yaml.v3
API documentation
-----------------
If opened in a browser, the import path itself leads to the API documentation:
- [https://gopkg.in/yaml.v3](https://gopkg.in/yaml.v3)
API stability
-------------
The package API for yaml v3 will remain stable as described in [gopkg.in](https://gopkg.in).