Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
7f02458ef8 fix(alerting): include queryFormulas in rule validation and evaluation 2026-04-08 18:19:03 +00:00
5 changed files with 111 additions and 8 deletions

View File

@@ -90,8 +90,9 @@ func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*q
},
NoCache: true,
}
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
queries := r.Condition().CompositeQuery.QueriesIncludingFormulas()
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(queries))
copy(req.CompositeQuery.Queries, queries)
return req, nil
}

View File

@@ -101,6 +101,75 @@ func TestThresholdRuleEvalWithoutRecoveryTarget(t *testing.T) {
}
}
func TestPrepareQueryRangeIncludesQueryFormulas(t *testing.T) {
target := 10.0
postableRule := ruletypes.PostableRule{
AlertName: "prepare query range includes formulas",
AlertType: ruletypes.AlertTypeMetric,
RuleType: ruletypes.RuleTypeThreshold,
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: valuer.MustParseTextDuration("5m"),
Frequency: valuer.MustParseTextDuration("1m"),
}},
RuleCondition: &ruletypes.RuleCondition{
CompositeQuery: &ruletypes.AlertCompositeQuery{
QueryType: ruletypes.QueryTypeBuilder,
Queries: []qbtypes.QueryEnvelope{
{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "A",
Signal: telemetrytypes.SignalMetrics,
StepInterval: qbtypes.Step{Duration: time.Minute},
Aggregations: []qbtypes.MetricAggregation{
{
MetricName: "signoz_calls_total",
Temporality: metrictypes.Delta,
TimeAggregation: metrictypes.TimeAggregationRate,
SpaceAggregation: metrictypes.SpaceAggregationSum,
},
},
},
},
},
QueryFormulas: []qbtypes.QueryBuilderFormula{
{
Name: "F1",
Expression: "A",
},
},
},
Target: &target,
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
Thresholds: &ruletypes.RuleThresholdData{
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{
{
Name: ruletypes.CriticalThresholdName,
TargetValue: &target,
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
},
},
},
},
Version: "v5",
}
logger := instrumentationtest.New().Logger()
rule, err := NewThresholdRule("69", valuer.GenerateUUID(), &postableRule, nil, logger)
require.NoError(t, err)
req, err := rule.prepareQueryRange(context.Background(), time.Now())
require.NoError(t, err)
require.Len(t, req.CompositeQuery.Queries, 2)
assert.Equal(t, "A", req.CompositeQuery.Queries[0].GetQueryName())
assert.Equal(t, "F1", req.CompositeQuery.Queries[1].GetQueryName())
assert.Equal(t, qbtypes.QueryTypeFormula, req.CompositeQuery.Queries[1].Type)
}
func TestNormalizeLabelName(t *testing.T) {
cases := []struct {
labelName string

View File

@@ -85,7 +85,8 @@ var (
)
type AlertCompositeQuery struct {
Queries []qbtypes.QueryEnvelope `json:"queries"`
Queries []qbtypes.QueryEnvelope `json:"queries"`
QueryFormulas []qbtypes.QueryBuilderFormula `json:"queryFormulas,omitempty"`
PanelType PanelType `json:"panelType"`
QueryType QueryType `json:"queryType"`
@@ -94,6 +95,22 @@ type AlertCompositeQuery struct {
Unit string `json:"unit,omitempty"`
}
func (acq *AlertCompositeQuery) QueriesIncludingFormulas() []qbtypes.QueryEnvelope {
if acq == nil {
return nil
}
queries := make([]qbtypes.QueryEnvelope, 0, len(acq.Queries)+len(acq.QueryFormulas))
queries = append(queries, acq.Queries...)
for _, formula := range acq.QueryFormulas {
queries = append(queries, qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeFormula,
Spec: formula,
})
}
return queries
}
type RuleCondition struct {
CompositeQuery *AlertCompositeQuery `json:"compositeQuery"`
CompareOperator CompareOperator `json:"op"`
@@ -114,7 +131,7 @@ func (rc *RuleCondition) SelectedQueryName() string {
queryNames := map[string]struct{}{}
for _, query := range rc.CompositeQuery.Queries {
for _, query := range rc.CompositeQuery.QueriesIncludingFormulas() {
switch spec := query.Spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
if !spec.Disabled {

View File

@@ -406,19 +406,20 @@ func (r *PostableRule) Validate() error {
if r.RuleCondition.CompositeQuery == nil {
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "condition.compositeQuery: field is required"))
} else {
if len(r.RuleCondition.CompositeQuery.Queries) == 0 {
queries := r.RuleCondition.CompositeQuery.QueriesIncludingFormulas()
if len(queries) == 0 {
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "condition.compositeQuery.queries: must have at least one query"))
} else {
cq := &qbtypes.CompositeQuery{Queries: r.RuleCondition.CompositeQuery.Queries}
cq := &qbtypes.CompositeQuery{Queries: queries}
if err := cq.Validate(qbtypes.GetValidationOptions(qbtypes.RequestTypeTimeSeries)...); err != nil {
errs = append(errs, err)
}
}
}
if r.RuleCondition.SelectedQuery != "" && r.RuleCondition.CompositeQuery != nil && len(r.RuleCondition.CompositeQuery.Queries) > 0 {
if r.RuleCondition.SelectedQuery != "" && r.RuleCondition.CompositeQuery != nil {
found := false
for _, query := range r.RuleCondition.CompositeQuery.Queries {
for _, query := range r.RuleCondition.CompositeQuery.QueriesIncludingFormulas() {
if query.GetQueryName() == r.RuleCondition.SelectedQuery {
found = true
break

View File

@@ -296,6 +296,21 @@ func TestValidate_PostableRule_Common(t *testing.T) {
wantErr: true,
errSubstr: "selectedQueryName",
},
{
name: "selectedQueryName matches queryFormulas entry",
json: `{
"alert": "Test", "version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "aggregations": [{"metricName": "cpu", "spaceAggregation": "p50"}], "stepInterval": "5m"}}],
"queryFormulas": [{"name": "F1", "expression": "A", "disabled": false}]
},
"target": 10.0, "matchType": "1", "op": "1",
"selectedQueryName": "F1"
}
}`,
},
{
name: "empty selectedQueryName is ok (optional)",
json: validV1Builder(),