Skip to content

Commit df41862

Browse files
authored
Adding parser for Xfunctions in Rule validation (#7111)
1 parent 93fd1c5 commit df41862

File tree

3 files changed

+127
-11
lines changed

3 files changed

+127
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* [ENHANCEMENT] Distributor: Add a label references validation for remote write v2 request. #7074
99
* [ENHANCEMENT] Distributor: Add count, spans, and buckets validations for native histogram. #7072
1010
* [BUGFIX] Ring: Change DynamoDB KV to retry indefinitely for WatchKey. #7088
11+
* [BUGFIX] Ruler: Add XFunctions validation support. #7111
1112

1213
## 1.20.0 2025-11-10
1314

pkg/ruler/manager.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net/http"
88
"os"
9+
"strings"
910
"sync"
1011
"time"
1112

@@ -24,6 +25,7 @@ import (
2425
"github.com/weaveworks/common/user"
2526
"golang.org/x/net/context/ctxhttp"
2627

28+
"github.com/cortexproject/cortex/pkg/parser"
2729
"github.com/cortexproject/cortex/pkg/ring/client"
2830
"github.com/cortexproject/cortex/pkg/ruler/rulespb"
2931
)
@@ -457,19 +459,41 @@ func (*DefaultMultiTenantManager) ValidateRuleGroup(g rulefmt.RuleGroup) []error
457459
}
458460

459461
for i, r := range g.Rules {
462+
// Use Cortex parser for expression validation (supports XFunctions)
463+
if r.Expr != "" {
464+
if _, err := parser.ParseExpr(r.Expr); err != nil {
465+
var ruleName string
466+
if r.Alert != "" {
467+
ruleName = r.Alert
468+
} else {
469+
ruleName = r.Record
470+
}
471+
errs = append(errs, &rulefmt.Error{
472+
Group: g.Name,
473+
Rule: i,
474+
RuleName: ruleName,
475+
Err: rulefmt.WrappedError{},
476+
})
477+
}
478+
}
479+
480+
// Validate other rule fields using Prometheus validation
460481
for _, err := range r.Validate(rulefmt.RuleNode{}) {
461-
var ruleName string
462-
if r.Alert != "" {
463-
ruleName = r.Alert
464-
} else {
465-
ruleName = r.Record
482+
// Skip expression validation errors since we handle them above
483+
if !strings.Contains(err.Error(), "could not parse expression") {
484+
var ruleName string
485+
if r.Alert != "" {
486+
ruleName = r.Alert
487+
} else {
488+
ruleName = r.Record
489+
}
490+
errs = append(errs, &rulefmt.Error{
491+
Group: g.Name,
492+
Rule: i,
493+
RuleName: ruleName,
494+
Err: err,
495+
})
466496
}
467-
errs = append(errs, &rulefmt.Error{
468-
Group: g.Name,
469-
Rule: i,
470-
RuleName: ruleName,
471-
Err: err,
472-
})
473497
}
474498
}
475499

pkg/ruler/manager_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/go-kit/log"
1010
"github.com/prometheus/client_golang/prometheus"
1111
"github.com/prometheus/prometheus/model/labels"
12+
"github.com/prometheus/prometheus/model/rulefmt"
1213
"github.com/prometheus/prometheus/notifier"
1314
promRules "github.com/prometheus/prometheus/rules"
1415
"github.com/stretchr/testify/require"
@@ -361,3 +362,93 @@ func (m *mockRulesManager) Stop() {
361362
m.running.Store(false)
362363
close(m.done)
363364
}
365+
366+
func TestValidateRuleGroup_AcceptsXFunctions(t *testing.T) {
367+
manager := &DefaultMultiTenantManager{}
368+
369+
// Test rule with XFunction
370+
ruleGroupWithXFunc := rulefmt.RuleGroup{
371+
Name: "test_group",
372+
Rules: []rulefmt.Rule{
373+
{
374+
Alert: "TestAlert",
375+
Expr: "xrate(cpu_usage[5m]) > 0.8", // XFunction
376+
},
377+
},
378+
}
379+
380+
errs := manager.ValidateRuleGroup(ruleGroupWithXFunc)
381+
382+
// Should not have validation errors
383+
if len(errs) != 0 {
384+
t.Fatalf("Expected no validation errors for XFunction after fix, got: %v", errs)
385+
}
386+
}
387+
388+
func TestValidateRuleGroup_AcceptsStandardFunctions(t *testing.T) {
389+
manager := &DefaultMultiTenantManager{}
390+
391+
// Test rule with standard function (should pass)
392+
ruleGroupStandard := rulefmt.RuleGroup{
393+
Name: "test_group",
394+
Rules: []rulefmt.Rule{
395+
{
396+
Alert: "TestAlert",
397+
Expr: "rate(cpu_usage[5m]) > 0.8", // Standard function
398+
},
399+
},
400+
}
401+
402+
errs := manager.ValidateRuleGroup(ruleGroupStandard)
403+
404+
// Should have no validation errors
405+
if len(errs) != 0 {
406+
t.Fatalf("Expected no validation errors for standard function, got: %v", errs)
407+
}
408+
}
409+
410+
func TestValidateRuleGroup_RejectsInvalidRules(t *testing.T) {
411+
manager := &DefaultMultiTenantManager{}
412+
413+
// Test rule with invalid expression syntax
414+
ruleGroupInvalid := rulefmt.RuleGroup{
415+
Name: "test_group",
416+
Rules: []rulefmt.Rule{
417+
{
418+
Alert: "TestAlert",
419+
Expr: "invalid_syntax_here >", // Invalid expression
420+
},
421+
},
422+
}
423+
424+
errs := manager.ValidateRuleGroup(ruleGroupInvalid)
425+
426+
// Should have validation errors and they should be properly propagated
427+
require.NotEmpty(t, errs, "Expected validation errors for invalid expression")
428+
// Verify the error is a rulefmt.Error with proper group information
429+
ruleErr, ok := errs[0].(*rulefmt.Error)
430+
require.True(t, ok, "Error should be of type *rulefmt.Error")
431+
require.Equal(t, "test_group", ruleErr.Group, "Error should contain correct group name")
432+
require.Equal(t, "TestAlert", ruleErr.RuleName, "Error should contain correct rule name")
433+
}
434+
435+
func TestValidateRuleGroup_RejectsEmptyGroupName(t *testing.T) {
436+
manager := &DefaultMultiTenantManager{}
437+
438+
// Test rule group with empty name
439+
ruleGroupEmptyName := rulefmt.RuleGroup{
440+
Name: "", // Empty name
441+
Rules: []rulefmt.Rule{
442+
{
443+
Alert: "TestAlert",
444+
Expr: "rate(cpu_usage[5m]) > 0.8",
445+
},
446+
},
447+
}
448+
449+
errs := manager.ValidateRuleGroup(ruleGroupEmptyName)
450+
451+
// Should have validation errors
452+
require.NotEmpty(t, errs, "Expected validation errors for empty group name")
453+
require.Contains(t, errs[0].Error(), "rule group name must not be empty", "Error should mention empty group name")
454+
}

0 commit comments

Comments
 (0)