Implement initial no-op alert parsing and rule parsing tests.

This commit is contained in:
Julius Volz 2013-04-23 00:26:59 +02:00 committed by Julius Volz
parent 955708e8db
commit c0601abf46
15 changed files with 406 additions and 155 deletions

1
.gitignore vendored
View file

@ -8,7 +8,6 @@
*.orig *.orig
*.pyc *.pyc
*.rej *.rej
*.rules
*.so *.so
*~ *~
.*.swp .*.swp

View file

@ -54,30 +54,30 @@ var configTests = []struct {
} }
func TestConfigs(t *testing.T) { func TestConfigs(t *testing.T) {
for _, configTest := range configTests { for i, configTest := range configTests {
testConfig, err := LoadFromFile(path.Join(fixturesPath, configTest.inputFile)) testConfig, err := LoadFromFile(path.Join(fixturesPath, configTest.inputFile))
if err != nil { if err != nil {
if !configTest.shouldFail { if !configTest.shouldFail {
t.Errorf("Error parsing config %v: %v", configTest.inputFile, err) t.Fatalf("%d. Error parsing config %v: %v", i, configTest.inputFile, err)
} else { } else {
if !strings.Contains(err.Error(), configTest.errContains) { if !strings.Contains(err.Error(), configTest.errContains) {
t.Errorf("Expected error containing '%v', got: %v", configTest.errContains, err) t.Fatalf("%d. Expected error containing '%v', got: %v", i, configTest.errContains, err)
} }
} }
} else { } else {
printedConfig, err := ioutil.ReadFile(path.Join(fixturesPath, configTest.printedFile)) printedConfig, err := ioutil.ReadFile(path.Join(fixturesPath, configTest.printedFile))
if err != nil { if err != nil {
t.Errorf("Error reading config %v: %v", configTest.inputFile, err) t.Fatalf("%d. Error reading config %v: %v", i, configTest.inputFile, err)
continue continue
} }
expected := string(printedConfig) expected := string(printedConfig)
actual := testConfig.ToString(0) actual := testConfig.ToString(0)
if actual != expected { if actual != expected {
t.Errorf("%v: printed config doesn't match expected output", configTest.inputFile) t.Errorf("%d. %v: printed config doesn't match expected output", i, configTest.inputFile)
t.Errorf("Expected:\n%v\n\nActual:\n%v\n", expected, actual) t.Errorf("Expected:\n%v\n\nActual:\n%v\n", expected, actual)
t.Errorf("Writing expected and actual printed configs to /tmp for diffing (see test source for paths)") t.Fatalf("Writing expected and actual printed configs to /tmp for diffing (see test source for paths)")
ioutil.WriteFile(fmt.Sprintf("/tmp/%s.expected", configTest.printedFile), []byte(expected), 0600) ioutil.WriteFile(fmt.Sprintf("/tmp/%s.expected", configTest.printedFile), []byte(expected), 0600)
ioutil.WriteFile(fmt.Sprintf("/tmp/%s.actual", configTest.printedFile), []byte(actual), 0600) ioutil.WriteFile(fmt.Sprintf("/tmp/%s.actual", configTest.printedFile), []byte(actual), 0600)
} }

View file

View file

@ -0,0 +1,13 @@
// A simple test recording rule.
dc_http_request_rate5m = sum(rate(http_request_count[5m])) by (dc)
// A simple test alerting rule.
ALERT GlobalRequestRateLow IF(dc_http_request_rate5m < 10000) FOR 5m WITH {
description = "Global HTTP request rate low!",
summary = "Request rate low"
/* ... more fields here ... */
}
foo = bar{label1="value1"}
ALERT BazAlert IF(foo > 10) WITH {}

View file

@ -0,0 +1 @@
now = time()

View file

@ -0,0 +1,13 @@
// A simple test recording rule.
dc_http_request_rate5m = sum(rate(http_request_count[5m])) by (dc)
// A simple test alerting rule with a syntax error (invalid duration string "5").
ALERT GlobalRequestRateLow IF(dc_http_request_rate5m < 10000) FOR 5 WITH {
description = "Global HTTP request rate low!",
summary = "Request rate low"
/* ... more fields here ... */
}
foo = bar{label1="value1"}
ALERT BazAlert IF(foo > 10) WITH {}

View file

@ -20,11 +20,22 @@ import (
"github.com/prometheus/prometheus/utility" "github.com/prometheus/prometheus/utility"
) )
func CreateRule(name string, labels model.LabelSet, root ast.Node, permanent bool) (*Rule, error) { func CreateRecordingRule(name string, labels model.LabelSet, expr ast.Node, permanent bool) (*RecordingRule, error) {
if root.Type() != ast.VECTOR { if _, ok := expr.(ast.VectorNode); !ok {
return nil, fmt.Errorf("Rule %v does not evaluate to vector type", name) return nil, fmt.Errorf("Recording rule expression %v does not evaluate to vector type", expr)
} }
return NewRule(name, labels, root.(ast.VectorNode), permanent), nil return NewRecordingRule(name, labels, expr.(ast.VectorNode), permanent), nil
}
func CreateAlertingRule(name string, expr ast.Node, holdDurationStr string, labels model.LabelSet) (*AlertingRule, error) {
if _, ok := expr.(ast.VectorNode); !ok {
return nil, fmt.Errorf("Alert rule expression %v does not evaluate to vector type", expr)
}
holdDuration, err := utility.StringToDuration(holdDurationStr)
if err != nil {
return nil, err
}
return NewAlertingRule(name, expr.(ast.VectorNode), holdDuration, labels), nil
} }
func NewFunctionCall(name string, args []ast.Node) (ast.Node, error) { func NewFunctionCall(name string, args []ast.Node) (ast.Node, error) {
@ -40,7 +51,7 @@ func NewFunctionCall(name string, args []ast.Node) (ast.Node, error) {
} }
func NewVectorAggregation(aggrTypeStr string, vector ast.Node, groupBy []model.LabelName) (*ast.VectorAggregation, error) { func NewVectorAggregation(aggrTypeStr string, vector ast.Node, groupBy []model.LabelName) (*ast.VectorAggregation, error) {
if vector.Type() != ast.VECTOR { if _, ok := vector.(ast.VectorNode); !ok {
return nil, fmt.Errorf("Operand of %v aggregation must be of vector type", aggrTypeStr) return nil, fmt.Errorf("Operand of %v aggregation must be of vector type", aggrTypeStr)
} }
var aggrTypes = map[string]ast.AggrType{ var aggrTypes = map[string]ast.AggrType{

View file

@ -37,11 +37,16 @@ U [smhdwy]
\/\/[^\r\n]*\n { /* gobble up one-line comments */ } \/\/[^\r\n]*\n { /* gobble up one-line comments */ }
permanent { return PERMANENT } ALERT|alert { return ALERT }
IF|if { return IF }
FOR|for { return FOR }
WITH|with { return WITH }
PERMANENT|permanent { return PERMANENT }
BY|by { return GROUP_OP } BY|by { return GROUP_OP }
AVG|SUM|MAX|MIN { yylval.str = yytext; return AGGR_OP } AVG|SUM|MAX|MIN { yylval.str = yytext; return AGGR_OP }
avg|sum|max|min { yylval.str = strings.ToUpper(yytext); return AGGR_OP } avg|sum|max|min { yylval.str = strings.ToUpper(yytext); return AGGR_OP }
\<|>|AND|OR { yylval.str = yytext; return CMP_OP } \<|>|AND|OR|and|or { yylval.str = strings.ToUpper(yytext); return CMP_OP }
==|!=|>=|<= { yylval.str = yytext; return CMP_OP } ==|!=|>=|<= { yylval.str = yytext; return CMP_OP }
[+\-] { yylval.str = yytext; return ADDITIVE_OP } [+\-] { yylval.str = yytext; return ADDITIVE_OP }
[*/%] { yylval.str = yytext; return MULT_OP } [*/%] { yylval.str = yytext; return MULT_OP }
@ -49,7 +54,7 @@ avg|sum|max|min { yylval.str = strings.ToUpper(yytext); return AGGR_OP
{D}+{U} { yylval.str = yytext; return DURATION } {D}+{U} { yylval.str = yytext; return DURATION }
{L}({L}|{D})* { yylval.str = yytext; return IDENTIFIER } {L}({L}|{D})* { yylval.str = yytext; return IDENTIFIER }
\-?{D}+(\.{D}*)? { num, err := strconv.ParseFloat(yytext, 32); \-?{D}+(\.{D}*)? { num, err := strconv.ParseFloat(yytext, 64);
if (err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax) { if (err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax) {
panic("Invalid float") panic("Invalid float")
} }

View file

@ -302,7 +302,59 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon
{ {
} }
return yyactionreturn{0, yyRT_FALLTHROUGH} return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("permanent"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) { }}, {regexp.MustCompile("ALERT|alert"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() {
if r := recover(); r != nil {
if r != "yyREJECT" {
panic(r)
}
yyar.returnType = yyRT_REJECT
}
}()
{
return yyactionreturn{ALERT, yyRT_USER_RETURN}
}
return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("IF|if"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() {
if r := recover(); r != nil {
if r != "yyREJECT" {
panic(r)
}
yyar.returnType = yyRT_REJECT
}
}()
{
return yyactionreturn{IF, yyRT_USER_RETURN}
}
return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("FOR|for"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() {
if r := recover(); r != nil {
if r != "yyREJECT" {
panic(r)
}
yyar.returnType = yyRT_REJECT
}
}()
{
return yyactionreturn{FOR, yyRT_USER_RETURN}
}
return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("WITH|with"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() {
if r := recover(); r != nil {
if r != "yyREJECT" {
panic(r)
}
yyar.returnType = yyRT_REJECT
}
}()
{
return yyactionreturn{WITH, yyRT_USER_RETURN}
}
return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("PERMANENT|permanent"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if r != "yyREJECT" { if r != "yyREJECT" {
@ -356,7 +408,7 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon
return yyactionreturn{AGGR_OP, yyRT_USER_RETURN} return yyactionreturn{AGGR_OP, yyRT_USER_RETURN}
} }
return yyactionreturn{0, yyRT_FALLTHROUGH} return yyactionreturn{0, yyRT_FALLTHROUGH}
}}, {regexp.MustCompile("\\<|>|AND|OR"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) { }}, {regexp.MustCompile("\\<|>|AND|OR|and|or"), nil, []yystartcondition{}, false, func() (yyar yyactionreturn) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if r != "yyREJECT" { if r != "yyREJECT" {
@ -366,7 +418,7 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon
} }
}() }()
{ {
yylval.str = yytext yylval.str = strings.ToUpper(yytext)
return yyactionreturn{CMP_OP, yyRT_USER_RETURN} return yyactionreturn{CMP_OP, yyRT_USER_RETURN}
} }
return yyactionreturn{0, yyRT_FALLTHROUGH} return yyactionreturn{0, yyRT_FALLTHROUGH}
@ -450,7 +502,7 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon
} }
}() }()
{ {
num, err := strconv.ParseFloat(yytext, 32) num, err := strconv.ParseFloat(yytext, 64)
if err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax { if err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax {
panic("Invalid float") panic("Invalid float")
} }

View file

@ -34,7 +34,7 @@ var (
type RulesLexer struct { type RulesLexer struct {
errors []string // Errors encountered during parsing. errors []string // Errors encountered during parsing.
startToken int // Dummy token to simulate multiple start symbols (see below). startToken int // Dummy token to simulate multiple start symbols (see below).
parsedRules []*Rule // Parsed full rules. parsedRules []Rule // Parsed full rules.
parsedExpr ast.Node // Parsed single expression. parsedExpr ast.Node // Parsed single expression.
} }
@ -95,23 +95,23 @@ func LoadFromReader(rulesReader io.Reader, singleExpr bool) (interface{}, error)
panic("") panic("")
} }
func LoadRulesFromReader(rulesReader io.Reader) ([]*Rule, error) { func LoadRulesFromReader(rulesReader io.Reader) ([]Rule, error) {
expr, err := LoadFromReader(rulesReader, false) expr, err := LoadFromReader(rulesReader, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return expr.([]*Rule), err return expr.([]Rule), err
} }
func LoadRulesFromString(rulesString string) ([]*Rule, error) { func LoadRulesFromString(rulesString string) ([]Rule, error) {
rulesReader := strings.NewReader(rulesString) rulesReader := strings.NewReader(rulesString)
return LoadRulesFromReader(rulesReader) return LoadRulesFromReader(rulesReader)
} }
func LoadRulesFromFile(fileName string) ([]*Rule, error) { func LoadRulesFromFile(fileName string) ([]Rule, error) {
rulesReader, err := os.Open(fileName) rulesReader, err := os.Open(fileName)
if err != nil { if err != nil {
return []*Rule{}, err return []Rule{}, err
} }
defer rulesReader.Close() defer rulesReader.Close()
return LoadRulesFromReader(rulesReader) return LoadRulesFromReader(rulesReader)

View file

@ -31,7 +31,7 @@ type RuleManager interface {
} }
type ruleManager struct { type ruleManager struct {
rules []*Rule rules []Rule
results chan *Result results chan *Result
done chan bool done chan bool
interval time.Duration interval time.Duration
@ -40,7 +40,8 @@ type ruleManager struct {
func NewRuleManager(results chan *Result, interval time.Duration) RuleManager { func NewRuleManager(results chan *Result, interval time.Duration) RuleManager {
manager := &ruleManager{ manager := &ruleManager{
results: results, results: results,
rules: []*Rule{}, rules: []Rule{},
done: make(chan bool),
interval: interval, interval: interval,
} }
go manager.run(results) go manager.run(results)
@ -70,7 +71,7 @@ func (m *ruleManager) runIteration(results chan *Result) {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
for _, rule := range m.rules { for _, rule := range m.rules {
wg.Add(1) wg.Add(1)
go func(rule *Rule) { go func(rule Rule) {
vector, err := rule.Eval(&now) vector, err := rule.Eval(&now)
m.results <- &Result{ m.results <- &Result{
Samples: vector, Samples: vector,

View file

@ -39,12 +39,14 @@
%token <num> NUMBER %token <num> NUMBER
%token PERMANENT GROUP_OP %token PERMANENT GROUP_OP
%token <str> AGGR_OP CMP_OP ADDITIVE_OP MULT_OP %token <str> AGGR_OP CMP_OP ADDITIVE_OP MULT_OP
%token ALERT IF FOR WITH
%type <ruleNodeSlice> func_arg_list %type <ruleNodeSlice> func_arg_list
%type <labelNameSlice> label_list grouping_opts %type <labelNameSlice> label_list grouping_opts
%type <labelSet> label_assign label_assign_list rule_labels %type <labelSet> label_assign label_assign_list rule_labels
%type <ruleNode> rule_expr func_arg %type <ruleNode> rule_expr func_arg
%type <boolean> qualifier %type <boolean> qualifier
%type <str> for_duration
%right '=' %right '='
%left CMP_OP %left CMP_OP
@ -67,10 +69,22 @@ saved_rule_expr : rule_expr
rules_stat : qualifier IDENTIFIER rule_labels '=' rule_expr rules_stat : qualifier IDENTIFIER rule_labels '=' rule_expr
{ {
rule, err := CreateRule($2, $3, $5, $1) rule, err := CreateRecordingRule($2, $3, $5, $1)
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule) yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
} }
| ALERT IDENTIFIER IF rule_expr for_duration WITH rule_labels
{
rule, err := CreateAlertingRule($2, $4, $5, $7)
if err != nil { yylex.Error(err.Error()); return 1 }
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
}
;
for_duration : /* empty */
{ $$ = "0s" }
| FOR DURATION
{ $$ = $2 }
; ;
qualifier : /* empty */ qualifier : /* empty */

View file

@ -30,6 +30,10 @@ const AGGR_OP = 57354
const CMP_OP = 57355 const CMP_OP = 57355
const ADDITIVE_OP = 57356 const ADDITIVE_OP = 57356
const MULT_OP = 57357 const MULT_OP = 57357
const ALERT = 57358
const IF = 57359
const FOR = 57360
const WITH = 57361
var yyToknames = []string{ var yyToknames = []string{
"START_RULES", "START_RULES",
@ -44,6 +48,10 @@ var yyToknames = []string{
"CMP_OP", "CMP_OP",
"ADDITIVE_OP", "ADDITIVE_OP",
"MULT_OP", "MULT_OP",
"ALERT",
"IF",
"FOR",
"WITH",
" =", " =",
} }
var yyStatenames = []string{} var yyStatenames = []string{}
@ -52,7 +60,7 @@ const yyEofCode = 1
const yyErrCode = 2 const yyErrCode = 2
const yyMaxDepth = 200 const yyMaxDepth = 200
//line parser.y:175 //line parser.y:189
//line yacctab:1 //line yacctab:1
@ -61,75 +69,79 @@ var yyExca = []int{
1, -1, 1, -1,
-2, 0, -2, 0,
-1, 4, -1, 4,
6, 7, 6, 10,
-2, 1, -2, 1,
} }
const yyNprod = 33 const yyNprod = 36
const yyPrivate = 57344 const yyPrivate = 57344
var yyTokenNames []string var yyTokenNames []string
var yyStates []string var yyStates []string
const yyLast = 84 const yyLast = 97
var yyAct = []int{ var yyAct = []int{
32, 36, 31, 16, 6, 17, 15, 16, 18, 40, 20, 38, 34, 17, 33, 43, 6, 67, 15, 66,
14, 14, 21, 46, 14, 20, 25, 26, 27, 8, 19, 18, 16, 17, 15, 45, 59, 44, 60, 27,
33, 54, 10, 38, 22, 9, 19, 17, 15, 16, 28, 29, 16, 17, 15, 41, 40, 18, 16, 17,
17, 15, 16, 7, 30, 28, 14, 8, 33, 14, 18, 16, 17, 22, 15, 23, 21, 46, 47, 49,
10, 15, 16, 9, 47, 48, 49, 21, 53, 14, 15, 8, 35, 15, 10, 51, 22, 9, 50, 53,
39, 7, 8, 37, 58, 10, 57, 42, 9, 41, 52, 48, 61, 57, 18, 16, 17, 39, 8, 7,
43, 44, 52, 13, 45, 35, 7, 24, 50, 59, 32, 10, 65, 42, 9, 56, 30, 15, 8, 35,
56, 37, 23, 2, 3, 11, 5, 4, 1, 12, 54, 10, 62, 37, 9, 14, 7, 26, 68, 64,
34, 51, 55, 29, 39, 13, 25, 24, 2, 3, 7, 11, 5, 4,
1, 58, 12, 36, 55, 63, 31,
} }
var yyPact = []int{ var yyPact = []int{
69, -1000, -1000, 46, 53, -1000, 17, 46, -5, 4, 80, -1000, -1000, 52, 65, -1000, 17, 52, 12, 11,
-1000, -1000, 66, -1000, 59, 46, 46, 46, 14, -1000, -1000, -1000, 77, 76, -1000, 69, 52, 52, 52, 41,
13, 47, 46, 30, -14, -12, -11, 27, -1000, 38, -1000, 35, 51, 52, 25, 46, -22, -12, -18, 8,
-1000, -1000, 17, -1000, 42, -1000, -1000, 48, -8, 28, -1000, -8, -1000, -1000, 17, -1000, 15, -1000, -1000, 31,
-1000, -1000, 31, -1000, 65, 61, 51, 46, -1000, -1000, 14, 28, 52, -1000, -1000, 62, -1000, 74, 63, 54,
-1000, -1000, 1, 17, 64, 35, -1000, -1000, 63, -1000, 52, -2, -1000, -1000, -1000, -1000, -6, 17, 33, 64,
73, 25, -1000, -16, -1000, -1000, -1000, 72, -1000,
} }
var yyPgo = []int{ var yyPgo = []int{
0, 83, 82, 81, 1, 80, 26, 0, 2, 79, 0, 96, 95, 94, 1, 93, 0, 2, 4, 92,
78, 77, 76, 75, 91, 90, 89, 88, 87,
} }
var yyR1 = []int{ var yyR1 = []int{
0, 10, 10, 11, 11, 12, 13, 9, 9, 6, 0, 11, 11, 12, 12, 13, 14, 14, 10, 10,
6, 6, 5, 5, 4, 7, 7, 7, 7, 7, 9, 9, 6, 6, 6, 5, 5, 4, 7, 7,
7, 7, 7, 7, 7, 3, 3, 2, 2, 1, 7, 7, 7, 7, 7, 7, 7, 7, 3, 3,
1, 8, 8, 2, 2, 1, 1, 8, 8,
} }
var yyR2 = []int{ var yyR2 = []int{
0, 2, 2, 0, 2, 1, 5, 0, 1, 0, 0, 2, 2, 0, 2, 1, 5, 7, 0, 2,
3, 2, 1, 3, 3, 3, 2, 4, 3, 4, 0, 1, 0, 3, 2, 1, 3, 3, 3, 2,
5, 3, 3, 3, 1, 0, 4, 1, 3, 1, 4, 3, 4, 5, 3, 3, 3, 1, 0, 4,
3, 1, 1, 1, 3, 1, 3, 1, 1,
} }
var yyChk = []int{ var yyChk = []int{
-1000, -10, 4, 5, -11, -12, -7, 20, 6, 12, -1000, -11, 4, 5, -12, -13, -7, 24, 6, 12,
9, -13, -9, 10, 22, 14, 15, 13, -7, -6, 9, -14, -9, 16, 10, 26, 14, 15, 13, -7,
20, 17, 20, 6, 8, -7, -7, -7, 21, -1, -6, 24, 21, 24, 6, 6, 8, -7, -7, -7,
21, -8, -7, 7, -5, 18, -4, 6, -7, -6, 25, -1, 25, -8, -7, 7, -5, 22, -4, 6,
23, 21, 19, 18, 19, 16, 21, 16, -8, -4, -7, -6, 17, 27, 25, 23, 22, 23, 20, 25,
7, -3, 11, -7, 20, -2, 6, 21, 19, 6, 20, -7, -8, -4, 7, -3, 11, -7, -10, 18,
24, 19, 8, -2, 6, -6, 25, 23, 6,
} }
var yyDef = []int{ var yyDef = []int{
0, -2, 3, 0, -2, 2, 5, 0, 9, 0, 0, -2, 3, 0, -2, 2, 5, 0, 12, 0,
24, 4, 0, 8, 0, 0, 0, 0, 0, 16, 27, 4, 0, 0, 11, 0, 0, 0, 0, 0,
0, 0, 0, 9, 0, 21, 22, 23, 15, 0, 19, 0, 0, 0, 12, 0, 0, 24, 25, 26,
18, 29, 31, 32, 0, 11, 12, 0, 0, 0, 18, 0, 21, 32, 34, 35, 0, 14, 15, 0,
19, 17, 0, 10, 0, 0, 25, 0, 30, 13, 0, 0, 0, 22, 20, 0, 13, 0, 0, 28,
14, 20, 0, 6, 0, 0, 27, 26, 0, 28, 0, 8, 33, 16, 17, 23, 0, 6, 0, 0,
0, 12, 9, 0, 30, 7, 29, 0, 31,
} }
var yyTok1 = []int{ var yyTok1 = []int{
@ -137,20 +149,20 @@ var yyTok1 = []int{
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
20, 21, 3, 3, 19, 3, 3, 3, 3, 3, 24, 25, 3, 3, 23, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 16, 3, 3, 3, 3, 3, 3, 3, 3, 3, 20, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 22, 3, 23, 3, 3, 3, 3, 3, 3, 3, 26, 3, 27, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 17, 3, 18, 3, 3, 3, 21, 3, 22,
} }
var yyTok2 = []int{ var yyTok2 = []int{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 12, 13, 14, 15, 16, 17, 18, 19,
} }
var yyTok3 = []int{ var yyTok3 = []int{
0, 0,
@ -381,120 +393,133 @@ yydefault:
switch yynt { switch yynt {
case 5: case 5:
//line parser.y:65 //line parser.y:67
{ yylex.(*RulesLexer).parsedExpr = yyS[yypt-0].ruleNode } { yylex.(*RulesLexer).parsedExpr = yyS[yypt-0].ruleNode }
case 6: case 6:
//line parser.y:69 //line parser.y:71
{ {
rule, err := CreateRule(yyS[yypt-3].str, yyS[yypt-2].labelSet, yyS[yypt-0].ruleNode, yyS[yypt-4].boolean) rule, err := CreateRecordingRule(yyS[yypt-3].str, yyS[yypt-2].labelSet, yyS[yypt-0].ruleNode, yyS[yypt-4].boolean)
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule) yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
} }
case 7: case 7:
//line parser.y:77 //line parser.y:77
{ yyVAL.boolean = false } {
rule, err := CreateAlertingRule(yyS[yypt-5].str, yyS[yypt-3].ruleNode, yyS[yypt-2].str, yyS[yypt-0].labelSet)
if err != nil { yylex.Error(err.Error()); return 1 }
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
}
case 8: case 8:
//line parser.y:79
{ yyVAL.boolean = true }
case 9:
//line parser.y:83
{ yyVAL.labelSet = model.LabelSet{} }
case 10:
//line parser.y:85 //line parser.y:85
{ yyVAL.labelSet = yyS[yypt-1].labelSet } { yyVAL.str = "0s" }
case 11: case 9:
//line parser.y:87 //line parser.y:87
{ yyVAL.labelSet = model.LabelSet{} } { yyVAL.str = yyS[yypt-0].str }
case 10:
//line parser.y:91
{ yyVAL.boolean = false }
case 11:
//line parser.y:93
{ yyVAL.boolean = true }
case 12: case 12:
//line parser.y:90 //line parser.y:97
{ yyVAL.labelSet = yyS[yypt-0].labelSet } { yyVAL.labelSet = model.LabelSet{} }
case 13: case 13:
//line parser.y:92 //line parser.y:99
{ for k, v := range yyS[yypt-0].labelSet { yyVAL.labelSet[k] = v } } { yyVAL.labelSet = yyS[yypt-1].labelSet }
case 14: case 14:
//line parser.y:96
{ yyVAL.labelSet = model.LabelSet{ model.LabelName(yyS[yypt-2].str): model.LabelValue(yyS[yypt-0].str) } }
case 15:
//line parser.y:101 //line parser.y:101
{ yyVAL.ruleNode = yyS[yypt-1].ruleNode } { yyVAL.labelSet = model.LabelSet{} }
case 15:
//line parser.y:104
{ yyVAL.labelSet = yyS[yypt-0].labelSet }
case 16: case 16:
//line parser.y:103 //line parser.y:106
{ yyS[yypt-0].labelSet[model.MetricNameLabel] = model.LabelValue(yyS[yypt-1].str); yyVAL.ruleNode = ast.NewVectorLiteral(yyS[yypt-0].labelSet) } { for k, v := range yyS[yypt-0].labelSet { yyVAL.labelSet[k] = v } }
case 17: case 17:
//line parser.y:105 //line parser.y:110
{ yyVAL.labelSet = model.LabelSet{ model.LabelName(yyS[yypt-2].str): model.LabelValue(yyS[yypt-0].str) } }
case 18:
//line parser.y:115
{ yyVAL.ruleNode = yyS[yypt-1].ruleNode }
case 19:
//line parser.y:117
{ yyS[yypt-0].labelSet[model.MetricNameLabel] = model.LabelValue(yyS[yypt-1].str); yyVAL.ruleNode = ast.NewVectorLiteral(yyS[yypt-0].labelSet) }
case 20:
//line parser.y:119
{ {
var err error var err error
yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-3].str, yyS[yypt-1].ruleNodeSlice) yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-3].str, yyS[yypt-1].ruleNodeSlice)
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
} }
case 18: case 21:
//line parser.y:111 //line parser.y:125
{ {
var err error var err error
yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-2].str, []ast.Node{}) yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-2].str, []ast.Node{})
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
} }
case 19: case 22:
//line parser.y:117 //line parser.y:131
{ {
var err error var err error
yyVAL.ruleNode, err = NewMatrix(yyS[yypt-3].ruleNode, yyS[yypt-1].str) yyVAL.ruleNode, err = NewMatrix(yyS[yypt-3].ruleNode, yyS[yypt-1].str)
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
} }
case 20: case 23:
//line parser.y:123 //line parser.y:137
{ {
var err error var err error
yyVAL.ruleNode, err = NewVectorAggregation(yyS[yypt-4].str, yyS[yypt-2].ruleNode, yyS[yypt-0].labelNameSlice) yyVAL.ruleNode, err = NewVectorAggregation(yyS[yypt-4].str, yyS[yypt-2].ruleNode, yyS[yypt-0].labelNameSlice)
if err != nil { yylex.Error(err.Error()); return 1 } if err != nil { yylex.Error(err.Error()); return 1 }
} }
case 21:
//line parser.y:131
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 22:
//line parser.y:137
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 23:
//line parser.y:143
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 24: case 24:
//line parser.y:149 //line parser.y:145
{ yyVAL.ruleNode = ast.NewScalarLiteral(yyS[yypt-0].num)} {
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 25: case 25:
//line parser.y:153 //line parser.y:151
{ yyVAL.labelNameSlice = []model.LabelName{} } {
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 26: case 26:
//line parser.y:155 //line parser.y:157
{ yyVAL.labelNameSlice = yyS[yypt-1].labelNameSlice } {
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
if err != nil { yylex.Error(err.Error()); return 1 }
}
case 27: case 27:
//line parser.y:159 //line parser.y:163
{ yyVAL.labelNameSlice = []model.LabelName{model.LabelName(yyS[yypt-0].str)} } { yyVAL.ruleNode = ast.NewScalarLiteral(yyS[yypt-0].num)}
case 28: case 28:
//line parser.y:161
{ yyVAL.labelNameSlice = append(yyVAL.labelNameSlice, model.LabelName(yyS[yypt-0].str)) }
case 29:
//line parser.y:165
{ yyVAL.ruleNodeSlice = []ast.Node{yyS[yypt-0].ruleNode} }
case 30:
//line parser.y:167 //line parser.y:167
{ yyVAL.ruleNodeSlice = append(yyVAL.ruleNodeSlice, yyS[yypt-0].ruleNode) } { yyVAL.labelNameSlice = []model.LabelName{} }
case 31: case 29:
//line parser.y:171 //line parser.y:169
{ yyVAL.ruleNode = yyS[yypt-0].ruleNode } { yyVAL.labelNameSlice = yyS[yypt-1].labelNameSlice }
case 32: case 30:
//line parser.y:173 //line parser.y:173
{ yyVAL.labelNameSlice = []model.LabelName{model.LabelName(yyS[yypt-0].str)} }
case 31:
//line parser.y:175
{ yyVAL.labelNameSlice = append(yyVAL.labelNameSlice, model.LabelName(yyS[yypt-0].str)) }
case 32:
//line parser.y:179
{ yyVAL.ruleNodeSlice = []ast.Node{yyS[yypt-0].ruleNode} }
case 33:
//line parser.y:181
{ yyVAL.ruleNodeSlice = append(yyVAL.ruleNodeSlice, yyS[yypt-0].ruleNode) }
case 34:
//line parser.y:185
{ yyVAL.ruleNode = yyS[yypt-0].ruleNode }
case 35:
//line parser.y:187
{ yyVAL.ruleNode = ast.NewStringLiteral(yyS[yypt-0].str) } { yyVAL.ruleNode = ast.NewStringLiteral(yyS[yypt-0].str) }
} }
goto yystack /* stack new state and value */ goto yystack /* stack new state and value */

View file

@ -20,21 +20,41 @@ import (
"time" "time"
) )
// A recorded rule. // A Rule encapsulates a vector expression which is evaluated at a specified
type Rule struct { // interval and acted upon (currently either recorded or used for alerting).
type Rule interface {
// Name returns the name of the rule.
Name() string
// EvalRaw evaluates the rule's vector expression without triggering any
// other actions, like recording or alerting.
EvalRaw(timestamp *time.Time) (vector ast.Vector, err error)
// Eval evaluates the rule, including any associated recording or alerting actions.
Eval(timestamp *time.Time) (vector ast.Vector, err error)
}
// A RecordingRule records its vector expression into new timeseries.
type RecordingRule struct {
name string name string
vector ast.VectorNode vector ast.VectorNode
labels model.LabelSet labels model.LabelSet
permanent bool permanent bool
} }
func (rule *Rule) Name() string { return rule.name } // An alerting rule generates alerts from its vector expression.
type AlertingRule struct {
name string
vector ast.VectorNode
holdDuration time.Duration
labels model.LabelSet
}
func (rule *Rule) EvalRaw(timestamp *time.Time) (vector ast.Vector, err error) { func (rule RecordingRule) Name() string { return rule.name }
func (rule RecordingRule) EvalRaw(timestamp *time.Time) (vector ast.Vector, err error) {
return ast.EvalVectorInstant(rule.vector, *timestamp) return ast.EvalVectorInstant(rule.vector, *timestamp)
} }
func (rule *Rule) Eval(timestamp *time.Time) (vector ast.Vector, err error) { func (rule RecordingRule) Eval(timestamp *time.Time) (vector ast.Vector, err error) {
// Get the raw value of the rule expression. // Get the raw value of the rule expression.
vector, err = rule.EvalRaw(timestamp) vector, err = rule.EvalRaw(timestamp)
if err != nil { if err != nil {
@ -55,20 +75,46 @@ func (rule *Rule) Eval(timestamp *time.Time) (vector ast.Vector, err error) {
return return
} }
func (rule *Rule) RuleToDotGraph() string { func (rule RecordingRule) RuleToDotGraph() string {
graph := "digraph \"Rules\" {\n" graph := "digraph \"Rules\" {\n"
graph += fmt.Sprintf("%#p[shape=\"box\",label=\"%v = \"];\n", rule, rule.name) graph += fmt.Sprintf("%#p[shape=\"box\",label=\"%v = \"];\n", rule, rule.name)
graph += fmt.Sprintf("%#p -> %#p;\n", rule, rule.vector) graph += fmt.Sprintf("%#p -> %#p;\n", &rule, rule.vector)
graph += rule.vector.NodeTreeToDotGraph() graph += rule.vector.NodeTreeToDotGraph()
graph += "}\n" graph += "}\n"
return graph return graph
} }
func NewRule(name string, labels model.LabelSet, vector ast.VectorNode, permanent bool) *Rule { func (rule AlertingRule) Name() string { return rule.name }
return &Rule{
func (rule AlertingRule) EvalRaw(timestamp *time.Time) (vector ast.Vector, err error) {
return ast.EvalVectorInstant(rule.vector, *timestamp)
}
func (rule AlertingRule) Eval(timestamp *time.Time) (vector ast.Vector, err error) {
// Get the raw value of the rule expression.
vector, err = rule.EvalRaw(timestamp)
if err != nil {
return
}
// TODO(julius): handle alerting.
return
}
func NewRecordingRule(name string, labels model.LabelSet, vector ast.VectorNode, permanent bool) *RecordingRule {
return &RecordingRule{
name: name, name: name,
labels: labels, labels: labels,
vector: vector, vector: vector,
permanent: permanent, permanent: permanent,
} }
} }
func NewAlertingRule(name string, vector ast.VectorNode, holdDuration time.Duration, labels model.LabelSet) *AlertingRule {
return &AlertingRule{
name: name,
vector: vector,
holdDuration: holdDuration,
labels: labels,
}
}

View file

@ -18,12 +18,16 @@ import (
"github.com/prometheus/prometheus/rules/ast" "github.com/prometheus/prometheus/rules/ast"
"github.com/prometheus/prometheus/storage/metric" "github.com/prometheus/prometheus/storage/metric"
"github.com/prometheus/prometheus/utility/test" "github.com/prometheus/prometheus/utility/test"
"path"
"strings" "strings"
"testing" "testing"
"time" "time"
) )
var testEvalTime = testStartTime.Add(testDuration5m * 10) var (
testEvalTime = testStartTime.Add(testDuration5m * 10)
fixturesPath = "fixtures"
)
// Labels in expected output need to be alphabetically sorted. // Labels in expected output need to be alphabetically sorted.
var expressionTests = []struct { var expressionTests = []struct {
@ -349,3 +353,70 @@ func TestExpressions(t *testing.T) {
} }
} }
} }
var ruleTests = []struct {
inputFile string
shouldFail bool
errContains string
numRecordingRules int
numAlertingRules int
}{
{
inputFile: "empty.rules",
numRecordingRules: 0,
numAlertingRules: 0,
}, {
inputFile: "mixed.rules",
numRecordingRules: 2,
numAlertingRules: 2,
},
{
inputFile: "syntax_error.rules",
shouldFail: true,
errContains: "Error parsing rules at line 3",
},
{
inputFile: "non_vector.rules",
shouldFail: true,
errContains: "does not evaluate to vector type",
},
}
func TestRules(t *testing.T) {
for i, ruleTest := range ruleTests {
testRules, err := LoadRulesFromFile(path.Join(fixturesPath, ruleTest.inputFile))
if err != nil {
if !ruleTest.shouldFail {
t.Fatalf("%d. Error parsing rules file %v: %v", i, ruleTest.inputFile, err)
} else {
if !strings.Contains(err.Error(), ruleTest.errContains) {
t.Fatalf("%d. Expected error containing '%v', got: %v", i, ruleTest.errContains, err)
}
}
} else {
numRecordingRules := 0
numAlertingRules := 0
for j, rule := range testRules {
switch rule.(type) {
case *RecordingRule:
numRecordingRules++
case *AlertingRule:
numAlertingRules++
default:
t.Fatalf("%d.%d. Unknown rule type!", i, j)
}
}
if numRecordingRules != ruleTest.numRecordingRules {
t.Fatalf("%d. Expected %d recording rules, got %d", i, ruleTest.numRecordingRules, numRecordingRules)
}
if numAlertingRules != ruleTest.numAlertingRules {
t.Fatalf("%d. Expected %d alerting rules, got %d", i, ruleTest.numAlertingRules, numAlertingRules)
}
// TODO(julius): add more complex checks on the parsed rules here.
}
}
}