package rules import ( "testing" "time" "github.com/stretchr/testify/assert" "go.signoz.io/signoz/pkg/query-service/featureManager" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/utils/labels" ) func TestThresholdRuleShouldAlert(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", AlertType: "METRIC_BASED_ALERT", RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), RuleCondition: &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", StepInterval: 60, AggregateAttribute: v3.AttributeKey{ Key: "probe_success", }, AggregateOperator: v3.AggregateOperatorNoOp, DataSource: v3.DataSourceMetrics, Expression: "A", }, }, }, }, } cases := []struct { values v3.Series expectAlert bool compareOp string matchType string target float64 }{ // Test cases for Equals Always { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, // Test cases for Equals Once { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 1.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "1", // Once target: 0.0, }, // Test cases for Greater Than Always { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "2", // Always target: 1.5, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "2", // Always target: 4.5, }, // Test cases for Greater Than Once { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "1", // Once target: 4.5, }, { values: v3.Series{ Points: []v3.Point{ {Value: 4.0}, {Value: 4.0}, {Value: 4.0}, {Value: 4.0}, {Value: 4.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "1", // Once target: 4.5, }, // Test cases for Not Equals Always { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "2", // Always target: 0.0, }, // Test cases for Not Equals Once { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 1.0}, {Value: 0.0}, {Value: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, {Value: 1.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "1", // Once target: 0.0, }, // Test cases for Less Than Always { values: v3.Series{ Points: []v3.Point{ {Value: 1.5}, {Value: 1.5}, {Value: 1.5}, {Value: 1.5}, {Value: 1.5}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "2", // Always target: 4, }, { values: v3.Series{ Points: []v3.Point{ {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "2", // Always target: 4, }, // Test cases for Less Than Once { values: v3.Series{ Points: []v3.Point{ {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 2.5}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "1", // Once target: 4, }, { values: v3.Series{ Points: []v3.Point{ {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, {Value: 4.5}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "1", // Once target: 4, }, // Test cases for OnAverage { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "3", // OnAverage target: 6.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "3", // OnAverage target: 4.5, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "3", // OnAverage target: 4.5, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "3", // OnAverage target: 6.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "3", // OnAverage target: 4.5, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "3", // OnAverage target: 12.0, }, // Test cases for InTotal { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "4", // InTotal target: 30.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 4.0}, {Value: 6.0}, {Value: 8.0}, {Value: 2.0}, }, }, expectAlert: false, compareOp: "3", // Equals matchType: "4", // InTotal target: 20.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, }, }, expectAlert: true, compareOp: "4", // Not Equals matchType: "4", // InTotal target: 9.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, }, }, expectAlert: false, compareOp: "4", // Not Equals matchType: "4", // InTotal target: 10.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 10.0}, }, }, expectAlert: true, compareOp: "1", // Greater Than matchType: "4", // InTotal target: 10.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 10.0}, }, }, expectAlert: false, compareOp: "1", // Greater Than matchType: "4", // InTotal target: 20.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 10.0}, }, }, expectAlert: true, compareOp: "2", // Less Than matchType: "4", // InTotal target: 30.0, }, { values: v3.Series{ Points: []v3.Point{ {Value: 10.0}, {Value: 10.0}, }, }, expectAlert: false, compareOp: "2", // Less Than matchType: "4", // InTotal target: 20.0, }, } fm := featureManager.StartManager() for idx, c := range cases { postableRule.RuleCondition.CompareOp = CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm, nil) if err != nil { assert.NoError(t, err) } values := c.values for i := range values.Points { values.Points[i].Timestamp = time.Now().UnixMilli() } _, shoulAlert := rule.shouldAlert(c.values) assert.Equal(t, c.expectAlert, shoulAlert, "Test case %d", idx) } } func TestNormalizeLabelName(t *testing.T) { cases := []struct { labelName string expected string }{ { labelName: "label", expected: "label", }, { labelName: "label.with.dots", expected: "label_with_dots", }, { labelName: "label-with-dashes", expected: "label_with_dashes", }, { labelName: "labelwithnospaces", expected: "labelwithnospaces", }, { labelName: "label with spaces", expected: "label_with_spaces", }, { labelName: "label with spaces and .dots", expected: "label_with_spaces_and__dots", }, { labelName: "label with spaces and -dashes", expected: "label_with_spaces_and__dashes", }, } for _, c := range cases { assert.Equal(t, c.expected, normalizeLabelName(c.labelName)) } } func TestPrepareLinksToLogs(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", AlertType: "LOGS_BASED_ALERT", RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), RuleCondition: &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", StepInterval: 60, AggregateAttribute: v3.AttributeKey{ Key: "", }, AggregateOperator: v3.AggregateOperatorNoOp, DataSource: v3.DataSourceLogs, Expression: "A", }, }, }, CompareOp: "4", // Not Equals MatchType: "1", // Once Target: &[]float64{0.0}[0], SelectedQuery: "A", }, } fm := featureManager.StartManager() rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm, nil) if err != nil { assert.NoError(t, err) } ts := time.UnixMilli(1705469040000) link := rule.prepareLinksToLogs(ts, labels.Labels{}) assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468620000%2C%22end%22%3A1705468920000%2C%22pageSize%22%3A100%7D&startTime=1705468620000&endTime=1705468920000") } func TestPrepareLinksToTraces(t *testing.T) { postableRule := PostableRule{ AlertName: "Links to traces test", AlertType: "TRACES_BASED_ALERT", RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), RuleCondition: &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", StepInterval: 60, AggregateAttribute: v3.AttributeKey{ Key: "durationNano", }, AggregateOperator: v3.AggregateOperatorAvg, DataSource: v3.DataSourceTraces, Expression: "A", }, }, }, CompareOp: "4", // Not Equals MatchType: "1", // Once Target: &[]float64{0.0}[0], SelectedQuery: "A", }, } fm := featureManager.StartManager() rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm, nil) if err != nil { assert.NoError(t, err) } ts := time.UnixMilli(1705469040000) link := rule.prepareLinksToTraces(ts, labels.Labels{}) assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468620000000000%2C%22end%22%3A1705468920000000000%2C%22pageSize%22%3A100%7D&startTime=1705468620000000000&endTime=1705468920000000000") } func TestThresholdRuleLabelNormalization(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", AlertType: "METRIC_BASED_ALERT", RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), RuleCondition: &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypeBuilder, BuilderQueries: map[string]*v3.BuilderQuery{ "A": { QueryName: "A", StepInterval: 60, AggregateAttribute: v3.AttributeKey{ Key: "probe_success", }, AggregateOperator: v3.AggregateOperatorNoOp, DataSource: v3.DataSourceMetrics, Expression: "A", }, }, }, }, } cases := []struct { values v3.Series expectAlert bool compareOp string matchType string target float64 }{ // Test cases for Equals Always { values: v3.Series{ Points: []v3.Point{ {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, {Value: 0.0}, }, Labels: map[string]string{ "service.name": "frontend", }, LabelsArray: []map[string]string{ { "service.name": "frontend", }, }, }, expectAlert: true, compareOp: "3", // Equals matchType: "2", // Always target: 0.0, }, } fm := featureManager.StartManager() for idx, c := range cases { postableRule.RuleCondition.CompareOp = CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm, nil) if err != nil { assert.NoError(t, err) } values := c.values for i := range values.Points { values.Points[i].Timestamp = time.Now().UnixMilli() } sample, shoulAlert := rule.shouldAlert(c.values) for name, value := range c.values.Labels { assert.Equal(t, value, sample.Metric.Get(normalizeLabelName(name))) } assert.Equal(t, c.expectAlert, shoulAlert, "Test case %d", idx) } } func TestThresholdRuleClickHouseTmpl(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", AlertType: "METRIC_BASED_ALERT", RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), RuleCondition: &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypeClickHouseSQL, ClickHouseQueries: map[string]*v3.ClickHouseQuery{ "A": { Query: "SELECT 1 >= {{.start_timestamp_ms}} AND 1 <= {{.end_timestamp_ms}}", }, }, }, }, } // 01:39:47 ts := time.Unix(1717205987, 0) cases := []struct { expectedQuery string }{ // Test cases for Equals Always { // 01:32:00 - 01:37:00 expectedQuery: "SELECT 1 >= 1717205520000 AND 1 <= 1717205820000", }, } fm := featureManager.StartManager() for idx, c := range cases { rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm, nil) if err != nil { assert.NoError(t, err) } params := rule.prepareQueryRange(ts) assert.Equal(t, c.expectedQuery, params.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) secondTimeParams := rule.prepareQueryRange(ts) assert.Equal(t, c.expectedQuery, secondTimeParams.CompositeQuery.ClickHouseQueries["A"].Query, "Test case %d", idx) } }