diff --git a/.gitignore b/.gitignore index 3993b00b4..8d48acc7c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ *.orig *.pyc *.rej -*.rules *.so *~ .*.swp diff --git a/config/config_test.go b/config/config_test.go index da70ff732..523b7bf72 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -54,30 +54,30 @@ var configTests = []struct { } func TestConfigs(t *testing.T) { - for _, configTest := range configTests { + for i, configTest := range configTests { testConfig, err := LoadFromFile(path.Join(fixturesPath, configTest.inputFile)) if err != nil { 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 { 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 { printedConfig, err := ioutil.ReadFile(path.Join(fixturesPath, configTest.printedFile)) 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 } expected := string(printedConfig) actual := testConfig.ToString(0) 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("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.actual", configTest.printedFile), []byte(actual), 0600) } diff --git a/rules/fixtures/empty.rules b/rules/fixtures/empty.rules new file mode 100644 index 000000000..e69de29bb diff --git a/rules/fixtures/mixed.rules b/rules/fixtures/mixed.rules new file mode 100644 index 000000000..ca50cd96f --- /dev/null +++ b/rules/fixtures/mixed.rules @@ -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 {} diff --git a/rules/fixtures/non_vector.rules b/rules/fixtures/non_vector.rules new file mode 100644 index 000000000..f3b1046f2 --- /dev/null +++ b/rules/fixtures/non_vector.rules @@ -0,0 +1 @@ +now = time() diff --git a/rules/fixtures/syntax_error.rules b/rules/fixtures/syntax_error.rules new file mode 100644 index 000000000..5e0aff861 --- /dev/null +++ b/rules/fixtures/syntax_error.rules @@ -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 {} diff --git a/rules/helpers.go b/rules/helpers.go index a5a224c44..6633c1132 100644 --- a/rules/helpers.go +++ b/rules/helpers.go @@ -20,11 +20,22 @@ import ( "github.com/prometheus/prometheus/utility" ) -func CreateRule(name string, labels model.LabelSet, root ast.Node, permanent bool) (*Rule, error) { - if root.Type() != ast.VECTOR { - return nil, fmt.Errorf("Rule %v does not evaluate to vector type", name) +func CreateRecordingRule(name string, labels model.LabelSet, expr ast.Node, permanent bool) (*RecordingRule, error) { + if _, ok := expr.(ast.VectorNode); !ok { + 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) { @@ -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) { - 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) } var aggrTypes = map[string]ast.AggrType{ diff --git a/rules/lexer.l b/rules/lexer.l index 0424d2e15..766120e6e 100644 --- a/rules/lexer.l +++ b/rules/lexer.l @@ -37,11 +37,16 @@ U [smhdwy] \/\/[^\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 } AVG|SUM|MAX|MIN { yylval.str = 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 ADDITIVE_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 } {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) { panic("Invalid float") } diff --git a/rules/lexer.l.go b/rules/lexer.l.go index 26907d708..da632e1fc 100644 --- a/rules/lexer.l.go +++ b/rules/lexer.l.go @@ -302,7 +302,59 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon { } 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() { if r := recover(); r != nil { 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{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() { if r := recover(); r != nil { 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{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 { panic("Invalid float") } diff --git a/rules/load.go b/rules/load.go index 36a08e1d6..690071c75 100644 --- a/rules/load.go +++ b/rules/load.go @@ -34,7 +34,7 @@ var ( type RulesLexer struct { errors []string // Errors encountered during parsing. 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. } @@ -95,23 +95,23 @@ func LoadFromReader(rulesReader io.Reader, singleExpr bool) (interface{}, error) panic("") } -func LoadRulesFromReader(rulesReader io.Reader) ([]*Rule, error) { +func LoadRulesFromReader(rulesReader io.Reader) ([]Rule, error) { expr, err := LoadFromReader(rulesReader, false) if err != nil { 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) return LoadRulesFromReader(rulesReader) } -func LoadRulesFromFile(fileName string) ([]*Rule, error) { +func LoadRulesFromFile(fileName string) ([]Rule, error) { rulesReader, err := os.Open(fileName) if err != nil { - return []*Rule{}, err + return []Rule{}, err } defer rulesReader.Close() return LoadRulesFromReader(rulesReader) diff --git a/rules/manager.go b/rules/manager.go index 2bd7c799c..ba86019ec 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -31,7 +31,7 @@ type RuleManager interface { } type ruleManager struct { - rules []*Rule + rules []Rule results chan *Result done chan bool interval time.Duration @@ -40,7 +40,8 @@ type ruleManager struct { func NewRuleManager(results chan *Result, interval time.Duration) RuleManager { manager := &ruleManager{ results: results, - rules: []*Rule{}, + rules: []Rule{}, + done: make(chan bool), interval: interval, } go manager.run(results) @@ -70,7 +71,7 @@ func (m *ruleManager) runIteration(results chan *Result) { wg := sync.WaitGroup{} for _, rule := range m.rules { wg.Add(1) - go func(rule *Rule) { + go func(rule Rule) { vector, err := rule.Eval(&now) m.results <- &Result{ Samples: vector, diff --git a/rules/parser.y b/rules/parser.y index 344871da3..741a01524 100644 --- a/rules/parser.y +++ b/rules/parser.y @@ -39,12 +39,14 @@ %token NUMBER %token PERMANENT GROUP_OP %token AGGR_OP CMP_OP ADDITIVE_OP MULT_OP +%token ALERT IF FOR WITH %type func_arg_list %type label_list grouping_opts %type label_assign label_assign_list rule_labels %type rule_expr func_arg %type qualifier +%type for_duration %right '=' %left CMP_OP @@ -67,10 +69,22 @@ saved_rule_expr : 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 } 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 */ diff --git a/rules/parser.y.go b/rules/parser.y.go index ea25e1889..4b549cc63 100644 --- a/rules/parser.y.go +++ b/rules/parser.y.go @@ -30,6 +30,10 @@ const AGGR_OP = 57354 const CMP_OP = 57355 const ADDITIVE_OP = 57356 const MULT_OP = 57357 +const ALERT = 57358 +const IF = 57359 +const FOR = 57360 +const WITH = 57361 var yyToknames = []string{ "START_RULES", @@ -44,6 +48,10 @@ var yyToknames = []string{ "CMP_OP", "ADDITIVE_OP", "MULT_OP", + "ALERT", + "IF", + "FOR", + "WITH", " =", } var yyStatenames = []string{} @@ -52,7 +60,7 @@ const yyEofCode = 1 const yyErrCode = 2 const yyMaxDepth = 200 -//line parser.y:175 +//line parser.y:189 //line yacctab:1 @@ -61,75 +69,79 @@ var yyExca = []int{ 1, -1, -2, 0, -1, 4, - 6, 7, + 6, 10, -2, 1, } -const yyNprod = 33 +const yyNprod = 36 const yyPrivate = 57344 var yyTokenNames []string var yyStates []string -const yyLast = 84 +const yyLast = 97 var yyAct = []int{ - 32, 36, 31, 16, 6, 17, 15, 16, 18, 40, - 14, 14, 21, 46, 14, 20, 25, 26, 27, 8, - 33, 54, 10, 38, 22, 9, 19, 17, 15, 16, - 17, 15, 16, 7, 30, 28, 14, 8, 33, 14, - 10, 15, 16, 9, 47, 48, 49, 21, 53, 14, - 39, 7, 8, 37, 58, 10, 57, 42, 9, 41, - 43, 44, 52, 13, 45, 35, 7, 24, 50, 59, - 56, 37, 23, 2, 3, 11, 5, 4, 1, 12, - 34, 51, 55, 29, + 20, 38, 34, 17, 33, 43, 6, 67, 15, 66, + 19, 18, 16, 17, 15, 45, 59, 44, 60, 27, + 28, 29, 16, 17, 15, 41, 40, 18, 16, 17, + 18, 16, 17, 22, 15, 23, 21, 46, 47, 49, + 15, 8, 35, 15, 10, 51, 22, 9, 50, 53, + 52, 48, 61, 57, 18, 16, 17, 39, 8, 7, + 32, 10, 65, 42, 9, 56, 30, 15, 8, 35, + 54, 10, 62, 37, 9, 14, 7, 26, 68, 64, + 39, 13, 25, 24, 2, 3, 7, 11, 5, 4, + 1, 58, 12, 36, 55, 63, 31, } var yyPact = []int{ - 69, -1000, -1000, 46, 53, -1000, 17, 46, -5, 4, - -1000, -1000, 66, -1000, 59, 46, 46, 46, 14, -1000, - 13, 47, 46, 30, -14, -12, -11, 27, -1000, 38, - -1000, -1000, 17, -1000, 42, -1000, -1000, 48, -8, 28, - -1000, -1000, 31, -1000, 65, 61, 51, 46, -1000, -1000, - -1000, -1000, 1, 17, 64, 35, -1000, -1000, 63, -1000, + 80, -1000, -1000, 52, 65, -1000, 17, 52, 12, 11, + -1000, -1000, 77, 76, -1000, 69, 52, 52, 52, 41, + -1000, 35, 51, 52, 25, 46, -22, -12, -18, 8, + -1000, -8, -1000, -1000, 17, -1000, 15, -1000, -1000, 31, + 14, 28, 52, -1000, -1000, 62, -1000, 74, 63, 54, + 52, -2, -1000, -1000, -1000, -1000, -6, 17, 33, 64, + 73, 25, -1000, -16, -1000, -1000, -1000, 72, -1000, } var yyPgo = []int{ - 0, 83, 82, 81, 1, 80, 26, 0, 2, 79, - 78, 77, 76, 75, + 0, 96, 95, 94, 1, 93, 0, 2, 4, 92, + 91, 90, 89, 88, 87, } var yyR1 = []int{ - 0, 10, 10, 11, 11, 12, 13, 9, 9, 6, - 6, 6, 5, 5, 4, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 3, 3, 2, 2, 1, - 1, 8, 8, + 0, 11, 11, 12, 12, 13, 14, 14, 10, 10, + 9, 9, 6, 6, 6, 5, 5, 4, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 3, 3, + 2, 2, 1, 1, 8, 8, } var yyR2 = []int{ - 0, 2, 2, 0, 2, 1, 5, 0, 1, 0, - 3, 2, 1, 3, 3, 3, 2, 4, 3, 4, - 5, 3, 3, 3, 1, 0, 4, 1, 3, 1, - 3, 1, 1, + 0, 2, 2, 0, 2, 1, 5, 7, 0, 2, + 0, 1, 0, 3, 2, 1, 3, 3, 3, 2, + 4, 3, 4, 5, 3, 3, 3, 1, 0, 4, + 1, 3, 1, 3, 1, 1, } var yyChk = []int{ - -1000, -10, 4, 5, -11, -12, -7, 20, 6, 12, - 9, -13, -9, 10, 22, 14, 15, 13, -7, -6, - 20, 17, 20, 6, 8, -7, -7, -7, 21, -1, - 21, -8, -7, 7, -5, 18, -4, 6, -7, -6, - 23, 21, 19, 18, 19, 16, 21, 16, -8, -4, - 7, -3, 11, -7, 20, -2, 6, 21, 19, 6, + -1000, -11, 4, 5, -12, -13, -7, 24, 6, 12, + 9, -14, -9, 16, 10, 26, 14, 15, 13, -7, + -6, 24, 21, 24, 6, 6, 8, -7, -7, -7, + 25, -1, 25, -8, -7, 7, -5, 22, -4, 6, + -7, -6, 17, 27, 25, 23, 22, 23, 20, 25, + 20, -7, -8, -4, 7, -3, 11, -7, -10, 18, + 24, 19, 8, -2, 6, -6, 25, 23, 6, } var yyDef = []int{ - 0, -2, 3, 0, -2, 2, 5, 0, 9, 0, - 24, 4, 0, 8, 0, 0, 0, 0, 0, 16, - 0, 0, 0, 9, 0, 21, 22, 23, 15, 0, - 18, 29, 31, 32, 0, 11, 12, 0, 0, 0, - 19, 17, 0, 10, 0, 0, 25, 0, 30, 13, - 14, 20, 0, 6, 0, 0, 27, 26, 0, 28, + 0, -2, 3, 0, -2, 2, 5, 0, 12, 0, + 27, 4, 0, 0, 11, 0, 0, 0, 0, 0, + 19, 0, 0, 0, 12, 0, 0, 24, 25, 26, + 18, 0, 21, 32, 34, 35, 0, 14, 15, 0, + 0, 0, 0, 22, 20, 0, 13, 0, 0, 28, + 0, 8, 33, 16, 17, 23, 0, 6, 0, 0, + 0, 12, 9, 0, 30, 7, 29, 0, 31, } 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, - 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, 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, 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, 17, 3, 18, + 3, 3, 3, 21, 3, 22, } var yyTok2 = []int{ 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{ 0, @@ -381,120 +393,133 @@ yydefault: switch yynt { case 5: - //line parser.y:65 + //line parser.y:67 { yylex.(*RulesLexer).parsedExpr = yyS[yypt-0].ruleNode } 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 } yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule) } case 7: //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: - //line parser.y:79 - { yyVAL.boolean = true } - case 9: - //line parser.y:83 - { yyVAL.labelSet = model.LabelSet{} } - case 10: //line parser.y:85 - { yyVAL.labelSet = yyS[yypt-1].labelSet } - case 11: + { yyVAL.str = "0s" } + case 9: //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: - //line parser.y:90 - { yyVAL.labelSet = yyS[yypt-0].labelSet } + //line parser.y:97 + { yyVAL.labelSet = model.LabelSet{} } case 13: - //line parser.y:92 - { for k, v := range yyS[yypt-0].labelSet { yyVAL.labelSet[k] = v } } + //line parser.y:99 + { yyVAL.labelSet = yyS[yypt-1].labelSet } 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 - { yyVAL.ruleNode = yyS[yypt-1].ruleNode } + { yyVAL.labelSet = model.LabelSet{} } + case 15: + //line parser.y:104 + { yyVAL.labelSet = yyS[yypt-0].labelSet } case 16: - //line parser.y:103 - { yyS[yypt-0].labelSet[model.MetricNameLabel] = model.LabelValue(yyS[yypt-1].str); yyVAL.ruleNode = ast.NewVectorLiteral(yyS[yypt-0].labelSet) } + //line parser.y:106 + { for k, v := range yyS[yypt-0].labelSet { yyVAL.labelSet[k] = v } } 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 yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-3].str, yyS[yypt-1].ruleNodeSlice) if err != nil { yylex.Error(err.Error()); return 1 } } - case 18: - //line parser.y:111 + case 21: + //line parser.y:125 { var err error yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-2].str, []ast.Node{}) if err != nil { yylex.Error(err.Error()); return 1 } } - case 19: - //line parser.y:117 + case 22: + //line parser.y:131 { var err error yyVAL.ruleNode, err = NewMatrix(yyS[yypt-3].ruleNode, yyS[yypt-1].str) if err != nil { yylex.Error(err.Error()); return 1 } } - case 20: - //line parser.y:123 + case 23: + //line parser.y:137 { var err error 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 } } - 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: - //line parser.y:149 - { yyVAL.ruleNode = ast.NewScalarLiteral(yyS[yypt-0].num)} + //line parser.y:145 + { + 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: - //line parser.y:153 - { yyVAL.labelNameSlice = []model.LabelName{} } + //line parser.y:151 + { + 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: - //line parser.y:155 - { yyVAL.labelNameSlice = yyS[yypt-1].labelNameSlice } + //line parser.y:157 + { + 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: - //line parser.y:159 - { yyVAL.labelNameSlice = []model.LabelName{model.LabelName(yyS[yypt-0].str)} } + //line parser.y:163 + { yyVAL.ruleNode = ast.NewScalarLiteral(yyS[yypt-0].num)} 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 - { yyVAL.ruleNodeSlice = append(yyVAL.ruleNodeSlice, yyS[yypt-0].ruleNode) } - case 31: - //line parser.y:171 - { yyVAL.ruleNode = yyS[yypt-0].ruleNode } - case 32: + { yyVAL.labelNameSlice = []model.LabelName{} } + case 29: + //line parser.y:169 + { yyVAL.labelNameSlice = yyS[yypt-1].labelNameSlice } + case 30: //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) } } goto yystack /* stack new state and value */ diff --git a/rules/rules.go b/rules/rules.go index ef4ffca1d..1f18b5dc0 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -20,21 +20,41 @@ import ( "time" ) -// A recorded rule. -type Rule struct { +// A Rule encapsulates a vector expression which is evaluated at a specified +// 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 vector ast.VectorNode labels model.LabelSet 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) } -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. vector, err = rule.EvalRaw(timestamp) if err != nil { @@ -55,20 +75,46 @@ func (rule *Rule) Eval(timestamp *time.Time) (vector ast.Vector, err error) { return } -func (rule *Rule) RuleToDotGraph() string { +func (rule RecordingRule) RuleToDotGraph() string { graph := "digraph \"Rules\" {\n" 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 += "}\n" return graph } -func NewRule(name string, labels model.LabelSet, vector ast.VectorNode, permanent bool) *Rule { - return &Rule{ +func (rule AlertingRule) Name() string { return rule.name } + +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, labels: labels, vector: vector, 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, + } +} diff --git a/rules/rules_test.go b/rules/rules_test.go index 8d9c8719d..48645e44c 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -18,12 +18,16 @@ import ( "github.com/prometheus/prometheus/rules/ast" "github.com/prometheus/prometheus/storage/metric" "github.com/prometheus/prometheus/utility/test" + "path" "strings" "testing" "time" ) -var testEvalTime = testStartTime.Add(testDuration5m * 10) +var ( + testEvalTime = testStartTime.Add(testDuration5m * 10) + fixturesPath = "fixtures" +) // Labels in expected output need to be alphabetically sorted. 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. + } + } +}