package rules import ( "testing" "time" "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" pql "github.com/prometheus/prometheus/promql" "github.com/stretchr/testify/assert" ) func getVectorValues(vectors []ruletypes.Sample) []float64 { if len(vectors) == 0 { return []float64{} // Return empty slice instead of nil } var values []float64 for _, v := range vectors { values = append(values, v.V) } return values } func TestPromRuleShouldAlert(t *testing.T) { postableRule := ruletypes.PostableRule{ AlertName: "Test Rule", AlertType: ruletypes.AlertTypeMetric, RuleType: ruletypes.RuleTypeProm, Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: ruletypes.Duration(5 * time.Minute), Frequency: ruletypes.Duration(1 * time.Minute), }}, RuleCondition: &ruletypes.RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: "dummy_query", // This is not used in the test }, }, }, }, } cases := []struct { values pql.Series expectAlert bool compareOp string matchType string target float64 expectedAlertSample v3.Point expectedVectorValues []float64 // Expected values in result vector }{ // Test cases for Equals Always { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, expectedAlertSample: v3.Point{Value: 0.0}, expectedVectorValues: []float64{0.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, expectedVectorValues: []float64{}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 1.0}, {F: 0.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, expectedVectorValues: []float64{}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, // Test cases for Equals Once { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 0.0}, expectedVectorValues: []float64{0.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 1.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 0.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 1.0}, {F: 0.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 0.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, expectedVectorValues: []float64{}, }, // Test cases for Greater Than Always { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "2", // Always target: 1.5, expectedAlertSample: v3.Point{Value: 2.0}, expectedVectorValues: []float64{2.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 11.0}, {F: 4.0}, {F: 3.0}, {F: 7.0}, {F: 12.0}, }, }, expectAlert: true, compareOp: "1", // Above matchType: "2", // Always target: 2.0, expectedAlertSample: v3.Point{Value: 3.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 11.0}, {F: 4.0}, {F: 3.0}, {F: 7.0}, {F: 12.0}, }, }, expectAlert: true, compareOp: "2", // Below matchType: "2", // Always target: 13.0, expectedAlertSample: v3.Point{Value: 12.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "2", // Always target: 4.5, }, // Test cases for Greater Than Once { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "1", // Once target: 4.5, expectedAlertSample: v3.Point{Value: 10.0}, expectedVectorValues: []float64{10.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 4.0}, {F: 4.0}, {F: 4.0}, {F: 4.0}, {F: 4.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "1", // Once target: 4.5, }, // Test cases for Not Equals Always { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 1.0}, {F: 0.0}, {F: 1.0}, {F: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, expectedAlertSample: v3.Point{Value: 1.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 0.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, // Test cases for Not Equals Once { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 1.0}, {F: 0.0}, {F: 1.0}, {F: 0.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 1.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, {F: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 0.0}, {F: 0.0}, {F: 1.0}, {F: 0.0}, {F: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 1.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, {F: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, expectedAlertSample: v3.Point{Value: 1.0}, }, // Test cases for Less Than Always { values: pql.Series{ Floats: []pql.FPoint{ {F: 1.5}, {F: 1.5}, {F: 1.5}, {F: 1.5}, {F: 1.5}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "2", // Always target: 4, expectedAlertSample: v3.Point{Value: 1.5}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 4.5}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "2", // Always target: 4, }, // Test cases for Less Than Once { values: pql.Series{ Floats: []pql.FPoint{ {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 2.5}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "1", // Once target: 4, expectedAlertSample: v3.Point{Value: 2.5}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 4.5}, {F: 4.5}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "1", // Once target: 4, }, // Test cases for OnAverage { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "3", // OnAverage target: 6.0, expectedAlertSample: v3.Point{Value: 6.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "3", // OnAverage target: 4.5, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "3", // OnAverage target: 4.5, expectedAlertSample: v3.Point{Value: 6.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "3", // OnAverage target: 6.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "3", // OnAverage target: 4.5, expectedAlertSample: v3.Point{Value: 6.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "3", // OnAverage target: 12.0, expectedAlertSample: v3.Point{Value: 6.0}, }, // Test cases for InTotal { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "4", // InTotal target: 30.0, expectedAlertSample: v3.Point{Value: 30.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 4.0}, {F: 6.0}, {F: 8.0}, {F: 2.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "4", // InTotal target: 20.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "4", // InTotal target: 9.0, expectedAlertSample: v3.Point{Value: 10.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "4", // InTotal target: 10.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 10.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "4", // InTotal target: 10.0, expectedAlertSample: v3.Point{Value: 20.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 10.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "4", // InTotal target: 20.0, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 10.0}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "4", // InTotal target: 30.0, expectedAlertSample: v3.Point{Value: 20.0}, }, { values: pql.Series{ Floats: []pql.FPoint{ {F: 10.0}, {F: 10.0}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "4", // InTotal target: 20.0, }, } logger := instrumentationtest.New().Logger() for idx, c := range cases { postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{ { TargetValue: &c.target, MatchType: ruletypes.MatchType(c.matchType), CompareOp: ruletypes.CompareOp(c.compareOp), }, }, } rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, nil, nil) if err != nil { assert.NoError(t, err) } resultVectors, err := rule.Threshold.ShouldAlert(toCommonSeries(c.values), rule.Unit()) assert.NoError(t, err) // Compare full result vector with expected vector actualValues := getVectorValues(resultVectors) if c.expectedVectorValues != nil { // If expected vector values are specified, compare them exactly assert.Equal(t, c.expectedVectorValues, actualValues, "Result vector values don't match expected for case %d", idx) } else { // Fallback to the old logic for cases without expectedVectorValues if c.expectAlert { assert.NotEmpty(t, resultVectors, "Expected alert but got no result vectors for case %d", idx) // Verify at least one of the result vectors matches the expected alert sample if len(resultVectors) > 0 { found := false for _, sample := range resultVectors { if sample.V == c.expectedAlertSample.Value { found = true break } } assert.True(t, found, "Expected alert sample value %.2f not found in result vectors for case %d. Got values: %v", c.expectedAlertSample.Value, idx, actualValues) } } else { assert.Empty(t, resultVectors, "Expected no alert but got result vectors for case %d", idx) } } } }