package rules import ( "context" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" pql "github.com/prometheus/prometheus/promql" cmock "github.com/srikanthccv/ClickHouse-go-mock" "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/prometheus/prometheustest" "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" qslabels "github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest" "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" ) 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 TestPromRuleEval(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.Eval(toCommonSeries(c.values), rule.Unit(), ruletypes.EvalData{}) 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) } } } } func TestPromRuleUnitCombinations(t *testing.T) { // fixed base time for deterministic tests baseTime := time.Unix(1700000000, 0) evalTime := baseTime.Add(5 * time.Minute) postableRule := ruletypes.PostableRule{ AlertName: "Units test", 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: "test_metric", }, }, }, }, } // time_series_v4 cols of interest fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } // samples_v4 columns samplesCols := []cmock.ColumnType{ {Name: "metric_name", Type: "String"}, {Name: "fingerprint", Type: "UInt64"}, {Name: "unix_milli", Type: "Int64"}, {Name: "value", Type: "Float64"}, {Name: "flags", Type: "UInt32"}, } // see Timestamps on base_rule evalWindowMs := int64(5 * 60 * 1000) // 5 minutes in ms evalTimeMs := evalTime.UnixMilli() queryStart := ((evalTimeMs-2*evalWindowMs)/60000)*60000 + 1 // truncate to minute + 1ms queryEnd := (evalTimeMs / 60000) * 60000 // truncate to minute cases := []struct { targetUnit string yAxisUnit string values []struct { timestamp time.Time value float64 } expectAlerts int compareOp string matchType string target float64 summaryAny []string }{ { targetUnit: "s", yAxisUnit: "ns", values: []struct { timestamp time.Time value float64 }{ {baseTime, 572588400}, // 0.57 seconds {baseTime.Add(1 * time.Minute), 572386400}, // 0.57 seconds {baseTime.Add(2 * time.Minute), 300947400}, // 0.3 seconds {baseTime.Add(3 * time.Minute), 299316000}, // 0.3 seconds {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 0.06 seconds }, expectAlerts: 0, compareOp: "1", // Above matchType: "1", // Once target: 1, // 1 second }, { targetUnit: "ms", yAxisUnit: "ns", values: []struct { timestamp time.Time value float64 }{ {baseTime, 572588400}, // 572.58 ms {baseTime.Add(1 * time.Minute), 572386400}, // 572.38 ms {baseTime.Add(2 * time.Minute), 300947400}, // 300.94 ms {baseTime.Add(3 * time.Minute), 299316000}, // 299.31 ms {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 66.64 ms }, expectAlerts: 1, compareOp: "1", // Above matchType: "1", // Once target: 200, // 200 ms summaryAny: []string{ "observed metric value is 299 ms", "the observed metric value is 573 ms", "the observed metric value is 572 ms", "the observed metric value is 301 ms", }, }, { targetUnit: "decgbytes", yAxisUnit: "bytes", values: []struct { timestamp time.Time value float64 }{ {baseTime, 2863284053}, // 2.86 GB {baseTime.Add(1 * time.Minute), 2863388842}, // 2.86 GB {baseTime.Add(2 * time.Minute), 300947400}, // 0.3 GB {baseTime.Add(3 * time.Minute), 299316000}, // 0.3 GB {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 66.64 MB }, expectAlerts: 0, compareOp: "1", // Above matchType: "1", // Once target: 200, // 200 GB }, { targetUnit: "decgbytes", yAxisUnit: "By", values: []struct { timestamp time.Time value float64 }{ {baseTime, 2863284053}, // 2.86 GB {baseTime.Add(1 * time.Minute), 2863388842}, // 2.86 GB {baseTime.Add(2 * time.Minute), 300947400}, // 0.3 GB {baseTime.Add(3 * time.Minute), 299316000}, // 0.3 GB {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 66.64 MB }, expectAlerts: 0, compareOp: "1", // Above matchType: "1", // Once target: 200, // 200 GB }, { targetUnit: "h", yAxisUnit: "min", values: []struct { timestamp time.Time value float64 }{ {baseTime, 55}, // 55 minutes {baseTime.Add(1 * time.Minute), 57}, // 57 minutes {baseTime.Add(2 * time.Minute), 30}, // 30 minutes {baseTime.Add(3 * time.Minute), 29}, // 29 minutes }, expectAlerts: 0, compareOp: "1", // Above matchType: "1", // Once target: 1, // 1 hour }, } logger := instrumentationtest.New().Logger() for idx, c := range cases { telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) // single fingerprint with labels JSON fingerprint := uint64(12345) labelsJSON := `{"__name__":"test_metric"}` fingerprintData := [][]interface{}{ {fingerprint, labelsJSON}, } fingerprintRows := cmock.NewRows(fingerprintCols, fingerprintData) // create samples data from test case values samplesData := make([][]interface{}, len(c.values)) for i, v := range c.values { samplesData[i] = []interface{}{ "test_metric", fingerprint, v.timestamp.UnixMilli(), v.value, uint32(0), // flags - 0 means normal value, 1 means stale, we are not doing staleness tests } } samplesRows := cmock.NewRows(samplesCols, samplesData) // args: $1=metric_name, $2=label_name, $3=label_value telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(fingerprintRows) // args: $1=metric_name (outer), $2=metric_name (subquery), $3=label_name, $4=label_value, $5=start, $6=end telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs( "test_metric", "test_metric", "__name__", "test_metric", queryStart, queryEnd, ). WillReturnRows(samplesRows) promProvider := prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, telemetryStore) postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target postableRule.RuleCondition.CompositeQuery.Unit = c.yAxisUnit postableRule.RuleCondition.TargetUnit = c.targetUnit postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{ { Name: postableRule.AlertName, TargetValue: &c.target, TargetUnit: c.targetUnit, MatchType: ruletypes.MatchType(c.matchType), CompareOp: ruletypes.CompareOp(c.compareOp), }, }, } postableRule.Annotations = map[string]string{ "description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})", "summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}", } options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) if err != nil { assert.NoError(t, err) promProvider.Close() continue } alertsFound, err := rule.Eval(context.Background(), evalTime) if err != nil { assert.NoError(t, err) promProvider.Close() continue } assert.Equal(t, c.expectAlerts, alertsFound, "case %d", idx) if c.expectAlerts != 0 { foundCount := 0 for _, item := range rule.Active { for _, summary := range c.summaryAny { if strings.Contains(item.Annotations.Get("summary"), summary) { foundCount++ break } } } assert.Equal(t, c.expectAlerts, foundCount, "case %d", idx) } promProvider.Close() } } // TODO(abhishekhugetech): enable this func _Enable_this_after_9146_issue_fix_is_merged_TestPromRuleNoData(t *testing.T) { baseTime := time.Unix(1700000000, 0) evalTime := baseTime.Add(5 * time.Minute) postableRule := ruletypes.PostableRule{ AlertName: "No data test", 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: "test_metric", }, }, }, AlertOnAbsent: true, }, } // time_series_v4 cols of interest fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } cases := []struct { values []struct { timestamp time.Time value float64 } expectNoData bool }{ { values: []struct { timestamp time.Time value float64 }{}, expectNoData: true, }, } logger := instrumentationtest.New().Logger() for idx, c := range cases { telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) // no data fingerprintData := [][]interface{}{} fingerprintRows := cmock.NewRows(fingerprintCols, fingerprintData) // no rows == no data telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(fingerprintRows) promProvider := prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, telemetryStore) var target float64 = 0 postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{ { Name: postableRule.AlertName, TargetValue: &target, MatchType: ruletypes.AtleastOnce, CompareOp: ruletypes.ValueIsEq, }, }, } postableRule.Annotations = map[string]string{ "description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})", "summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}", } options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) if err != nil { assert.NoError(t, err) promProvider.Close() continue } alertsFound, err := rule.Eval(context.Background(), evalTime) if err != nil { assert.NoError(t, err) promProvider.Close() continue } assert.Equal(t, 1, alertsFound, "case %d", idx) for _, item := range rule.Active { if c.expectNoData { assert.True(t, strings.Contains(item.Labels.Get(qslabels.AlertNameLabel), "[No data]"), "case %d", idx) } else { assert.False(t, strings.Contains(item.Labels.Get(qslabels.AlertNameLabel), "[No data]"), "case %d", idx) } } promProvider.Close() } } func TestMultipleThresholdPromRule(t *testing.T) { // fixed base time for deterministic tests baseTime := time.Unix(1700000000, 0) evalTime := baseTime.Add(5 * time.Minute) postableRule := ruletypes.PostableRule{ AlertName: "Multiple threshold test", 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: "test_metric", }, }, }, }, } fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } samplesCols := []cmock.ColumnType{ {Name: "metric_name", Type: "String"}, {Name: "fingerprint", Type: "UInt64"}, {Name: "unix_milli", Type: "Int64"}, {Name: "value", Type: "Float64"}, {Name: "flags", Type: "UInt32"}, } // see .Timestamps of base rule evalWindowMs := int64(5 * 60 * 1000) evalTimeMs := evalTime.UnixMilli() queryStart := ((evalTimeMs-2*evalWindowMs)/60000)*60000 + 1 queryEnd := (evalTimeMs / 60000) * 60000 cases := []struct { targetUnit string yAxisUnit string values []struct { timestamp time.Time value float64 } expectAlerts int compareOp string matchType string target float64 secondTarget float64 summaryAny []string }{ { targetUnit: "s", yAxisUnit: "ns", values: []struct { timestamp time.Time value float64 }{ {baseTime, 572588400}, // 0.57 seconds {baseTime.Add(1 * time.Minute), 572386400}, // 0.57 seconds {baseTime.Add(2 * time.Minute), 300947400}, // 0.3 seconds {baseTime.Add(3 * time.Minute), 299316000}, // 0.3 seconds {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 0.06 seconds }, expectAlerts: 1, compareOp: "1", // Above matchType: "1", // Once target: 1, // 1 second secondTarget: .5, summaryAny: []string{ "observed metric value is 573 ms", "observed metric value is 572 ms", }, }, { targetUnit: "ms", yAxisUnit: "ns", values: []struct { timestamp time.Time value float64 }{ {baseTime, 572588400}, // 572.58 ms {baseTime.Add(1 * time.Minute), 572386400}, // 572.38 ms {baseTime.Add(2 * time.Minute), 300947400}, // 300.94 ms {baseTime.Add(3 * time.Minute), 299316000}, // 299.31 ms {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 66.64 ms }, expectAlerts: 2, // One alert per threshold that fires compareOp: "1", // Above matchType: "1", // Once target: 200, // 200 ms secondTarget: 500, summaryAny: []string{ "observed metric value is 299 ms", "the observed metric value is 573 ms", "the observed metric value is 572 ms", "the observed metric value is 301 ms", }, }, { targetUnit: "decgbytes", yAxisUnit: "bytes", values: []struct { timestamp time.Time value float64 }{ {baseTime, 2863284053}, // 2.86 GB {baseTime.Add(1 * time.Minute), 2863388842}, // 2.86 GB {baseTime.Add(2 * time.Minute), 300947400}, // 0.3 GB {baseTime.Add(3 * time.Minute), 299316000}, // 0.3 GB {baseTime.Add(4 * time.Minute), 66640400.00000001}, // 66.64 MB }, expectAlerts: 1, compareOp: "1", // Above matchType: "1", // Once target: 200, // 200 GB secondTarget: 2, // 2GB summaryAny: []string{ "observed metric value is 2.7 GiB", "the observed metric value is 0.3 GB", }, }, } logger := instrumentationtest.New().Logger() for idx, c := range cases { telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) fingerprint := uint64(12345) labelsJSON := `{"__name__":"test_metric"}` fingerprintData := [][]interface{}{ {fingerprint, labelsJSON}, } fingerprintRows := cmock.NewRows(fingerprintCols, fingerprintData) samplesData := make([][]interface{}, len(c.values)) for i, v := range c.values { samplesData[i] = []interface{}{ "test_metric", fingerprint, v.timestamp.UnixMilli(), v.value, uint32(0), } } samplesRows := cmock.NewRows(samplesCols, samplesData) telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(fingerprintRows) telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs( "test_metric", "test_metric", "__name__", "test_metric", queryStart, queryEnd, ). WillReturnRows(samplesRows) promProvider := prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, telemetryStore) postableRule.RuleCondition.CompareOp = ruletypes.CompareOp(c.compareOp) postableRule.RuleCondition.MatchType = ruletypes.MatchType(c.matchType) postableRule.RuleCondition.Target = &c.target postableRule.RuleCondition.CompositeQuery.Unit = c.yAxisUnit postableRule.RuleCondition.TargetUnit = c.targetUnit postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{ { Name: "first_threshold", TargetValue: &c.target, TargetUnit: c.targetUnit, MatchType: ruletypes.MatchType(c.matchType), CompareOp: ruletypes.CompareOp(c.compareOp), }, { Name: "second_threshold", TargetValue: &c.secondTarget, TargetUnit: c.targetUnit, MatchType: ruletypes.MatchType(c.matchType), CompareOp: ruletypes.CompareOp(c.compareOp), }, }, } postableRule.Annotations = map[string]string{ "description": "This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})", "summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}", } options := clickhouseReader.NewOptions("", "", "archiveNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) if err != nil { assert.NoError(t, err) promProvider.Close() continue } alertsFound, err := rule.Eval(context.Background(), evalTime) if err != nil { assert.NoError(t, err) promProvider.Close() continue } assert.Equal(t, c.expectAlerts, alertsFound, "case %d", idx) if c.expectAlerts != 0 { foundCount := 0 for _, item := range rule.Active { for _, summary := range c.summaryAny { if strings.Contains(item.Annotations.Get("summary"), summary) { foundCount++ break } } } assert.Equal(t, c.expectAlerts, foundCount, "case %d", idx) } promProvider.Close() } } func TestPromRule_NoData(t *testing.T) { evalTime := time.Now() postableRule := ruletypes.PostableRule{ AlertName: "Test no data", 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{ CompareOp: ruletypes.ValueIsAbove, MatchType: ruletypes.AtleastOnce, CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": {Query: "test_metric"}, }, }, Thresholds: &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{{Name: "Test no data"}}, }, }, } // time_series_v4 cols of interest fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } // samples_v4 columns samplesCols := []cmock.ColumnType{ {Name: "metric_name", Type: "String"}, {Name: "fingerprint", Type: "UInt64"}, {Name: "unix_milli", Type: "Int64"}, {Name: "value", Type: "Float64"}, {Name: "flags", Type: "UInt32"}, } // see Timestamps on base_rule evalWindowMs := int64(5 * 60 * 1000) // 5 minutes in ms evalTimeMs := evalTime.UnixMilli() queryStart := ((evalTimeMs-2*evalWindowMs)/60000)*60000 + 1 // truncate to minute + 1ms queryEnd := (evalTimeMs / 60000) * 60000 // truncate to minute cases := []struct { description string alertOnAbsent bool values []any target float64 expectAlerts int }{ { description: "AlertOnAbsent=false", alertOnAbsent: false, values: []any{}, target: 200, expectAlerts: 0, }, { description: "AlertOnAbsent=true", alertOnAbsent: true, values: []any{}, target: 200, expectAlerts: 1, }, } logger := instrumentationtest.New().Logger() for _, c := range cases { t.Run(c.description, func(t *testing.T) { postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) // single fingerprint with labels JSON fingerprint := uint64(12345) labelsJSON := `{"__name__":"test_metric"}` telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(cmock.NewRows(fingerprintCols, [][]any{{fingerprint, labelsJSON}})) telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs("test_metric", "test_metric", "__name__", "test_metric", queryStart, queryEnd). WillReturnRows(cmock.NewRows(samplesCols, [][]any{})) promProvider := prometheustest.New( context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, telemetryStore, ) defer func() { _ = promProvider.Close() }() options := clickhouseReader.NewOptions("primaryNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) require.NoError(t, err) alertsFound, err := rule.Eval(context.Background(), evalTime) require.NoError(t, err) assert.Equal(t, c.expectAlerts, alertsFound) }) } } func TestPromRule_NoData_AbsentFor(t *testing.T) { // 1. Call Eval with data at time t1, to populate lastTimestampWithDatapoints // 2. Call Eval without data at time t2 // 3. Alert fires only if t2 - t1 > AbsentFor baseTime := time.Unix(1700000000, 0) evalWindow := 5 * time.Minute // Set target higher than test data (100.0) so regular threshold alerts don't fire target := 500.0 postableRule := ruletypes.PostableRule{ AlertName: "Test no data with AbsentFor", AlertType: ruletypes.AlertTypeMetric, RuleType: ruletypes.RuleTypeProm, Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: ruletypes.Duration(evalWindow), Frequency: ruletypes.Duration(1 * time.Minute), }}, RuleCondition: &ruletypes.RuleCondition{ CompareOp: ruletypes.ValueIsAbove, MatchType: ruletypes.AtleastOnce, AlertOnAbsent: true, Target: &target, CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": {Query: "test_metric"}, }, }, Thresholds: &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{{ Name: "Test no data with AbsentFor", TargetValue: &target, MatchType: ruletypes.AtleastOnce, CompareOp: ruletypes.ValueIsAbove, }}, }, }, } fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } samplesCols := []cmock.ColumnType{ {Name: "metric_name", Type: "String"}, {Name: "fingerprint", Type: "UInt64"}, {Name: "unix_milli", Type: "Int64"}, {Name: "value", Type: "Float64"}, {Name: "flags", Type: "UInt32"}, } cases := []struct { description string absentFor uint64 // grace period in minutes timeBetweenEvals time.Duration // time between first eval (with data) and second eval (no data) expectAlertOnEval2 int }{ { description: "WithinGracePeriod", absentFor: 5, timeBetweenEvals: 4 * time.Minute, expectAlertOnEval2: 0, }, { description: "AfterGracePeriod", absentFor: 5, timeBetweenEvals: 6 * time.Minute, expectAlertOnEval2: 1, }, } logger := instrumentationtest.New().Logger() for _, c := range cases { t.Run(c.description, func(t *testing.T) { postableRule.RuleCondition.AbsentFor = c.absentFor // Timestamps for two evaluations // t1 is the eval time for first eval, data points are in the past t1 := baseTime.Add(5 * time.Minute) // first eval with data t2 := t1.Add(c.timeBetweenEvals) // second eval without data telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) fingerprint := uint64(12345) labelsJSON := `{"__name__":"test_metric"}` // Helper to calculate query time range for an eval time calcQueryRange := func(evalTime time.Time) (int64, int64) { evalTimeMs := evalTime.UnixMilli() queryStart := ((evalTimeMs-2*evalWindow.Milliseconds())/60000)*60000 + 1 queryEnd := (evalTimeMs / 60000) * 60000 return queryStart, queryEnd } // First eval (t1) - with data queryStart1, queryEnd1 := calcQueryRange(t1) telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(cmock.NewRows(fingerprintCols, [][]any{{fingerprint, labelsJSON}})) telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs("test_metric", "test_metric", "__name__", "test_metric", queryStart1, queryEnd1). WillReturnRows(cmock.NewRows(samplesCols, [][]any{ // Data points in the past relative to t1 {"test_metric", fingerprint, baseTime.UnixMilli(), 100.0, uint32(0)}, {"test_metric", fingerprint, baseTime.Add(1 * time.Minute).UnixMilli(), 100.0, uint32(0)}, {"test_metric", fingerprint, baseTime.Add(2 * time.Minute).UnixMilli(), 100.0, uint32(0)}, })) // Second eval (t2) - no data queryStart2, queryEnd2 := calcQueryRange(t2) telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(cmock.NewRows(fingerprintCols, [][]any{{fingerprint, labelsJSON}})) telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs("test_metric", "test_metric", "__name__", "test_metric", queryStart2, queryEnd2). WillReturnRows(cmock.NewRows(samplesCols, [][]any{})) // empty - no data promProvider := prometheustest.New( context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, telemetryStore, ) defer func() { _ = promProvider.Close() }() options := clickhouseReader.NewOptions("primaryNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) require.NoError(t, err) // First eval with data - should NOT alert, but populates lastTimestampWithDatapoints alertsFound1, err := rule.Eval(context.Background(), t1) require.NoError(t, err) assert.Equal(t, 0, alertsFound1, "First eval with data should not alert") // Second eval without data - should alert based on AbsentFor alertsFound2, err := rule.Eval(context.Background(), t2) require.NoError(t, err) assert.Equal(t, c.expectAlertOnEval2, alertsFound2) }) } } func TestPromRuleEval_RequireMinPoints(t *testing.T) { // fixed base time for deterministic tests baseTime := time.Unix(1700000000, 0) evalTime := baseTime.Add(5 * time.Minute) evalWindow := 5 * time.Minute lookBackDelta := time.Minute postableRule := ruletypes.PostableRule{ AlertName: "Unit test", AlertType: ruletypes.AlertTypeMetric, RuleType: ruletypes.RuleTypeProm, Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: ruletypes.Duration(evalWindow), Frequency: ruletypes.Duration(time.Minute), }}, RuleCondition: &ruletypes.RuleCondition{ CompareOp: ruletypes.ValueIsAbove, MatchType: ruletypes.AtleastOnce, CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": {Query: "test_metric"}, }, }, }, } fingerprintCols := []cmock.ColumnType{ {Name: "fingerprint", Type: "UInt64"}, {Name: "any(labels)", Type: "String"}, } fingerprint := uint64(12345) fingerprintData := [][]any{{fingerprint, `{"__name__":"test_metric"}`}} samplesCols := []cmock.ColumnType{ {Name: "metric_name", Type: "String"}, {Name: "fingerprint", Type: "UInt64"}, {Name: "unix_milli", Type: "Int64"}, {Name: "value", Type: "Float64"}, {Name: "flags", Type: "UInt32"}, } samplesData := [][]any{ {"test_metric", fingerprint, baseTime.UnixMilli(), 100.0, 0}, {"test_metric", fingerprint, baseTime.Add(time.Minute).UnixMilli(), 150.0, 0}, {"test_metric", fingerprint, baseTime.Add(2 * time.Minute).UnixMilli(), 250.0, 0}, } targetForAlert := 200.0 targetForNoAlert := 500.0 // see Timestamps on base_rule evalTimeMs := evalTime.UnixMilli() queryStart := ((evalTimeMs-evalWindow.Milliseconds()-lookBackDelta.Milliseconds())/60000)*60000 + 1 // truncate to minute + 1ms queryEnd := (evalTimeMs / 60000) * 60000 // truncate to minute cases := []struct { description string alertCondition bool requireMinPoints bool requiredNumPoints int expectAlerts int }{ { description: "AlertCondition=false, RequireMinPoints=false", alertCondition: false, requireMinPoints: false, expectAlerts: 0, }, { description: "AlertCondition=true, RequireMinPoints=false", alertCondition: true, requireMinPoints: false, expectAlerts: 1, }, { description: "AlertCondition=true, RequireMinPoints=true, NumPoints=more_than_required", alertCondition: true, requireMinPoints: true, requiredNumPoints: 2, expectAlerts: 1, }, { description: "AlertCondition=true, RequireMinPoints=true, NumPoints=same_as_required", alertCondition: true, requireMinPoints: true, requiredNumPoints: 3, expectAlerts: 1, }, { description: "AlertCondition=true, RequireMinPoints=true, NumPoints=insufficient", alertCondition: true, requireMinPoints: true, requiredNumPoints: 4, expectAlerts: 0, }, } logger := instrumentationtest.New().Logger() for _, c := range cases { rc := postableRule.RuleCondition rc.Target = &targetForNoAlert if c.alertCondition { rc.Target = &targetForAlert } rc.RequireMinPoints = c.requireMinPoints rc.RequiredNumPoints = c.requiredNumPoints rc.Thresholds = &ruletypes.RuleThresholdData{ Kind: ruletypes.BasicThresholdKind, Spec: ruletypes.BasicRuleThresholds{ { Name: postableRule.AlertName, TargetValue: rc.Target, MatchType: rc.MatchType, CompareOp: rc.CompareOp, }, }, } t.Run(c.description, func(t *testing.T) { telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &queryMatcherAny{}) telemetryStore.Mock(). ExpectQuery("SELECT fingerprint, any"). WithArgs("test_metric", "__name__", "test_metric"). WillReturnRows(cmock.NewRows(fingerprintCols, fingerprintData)) telemetryStore.Mock(). ExpectQuery("SELECT metric_name, fingerprint, unix_milli"). WithArgs("test_metric", "test_metric", "__name__", "test_metric", queryStart, queryEnd). WillReturnRows(cmock.NewRows(samplesCols, samplesData)) promProvider := prometheustest.New( context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{LookbackDelta: lookBackDelta}, telemetryStore, ) defer func() { _ = promProvider.Close() }() options := clickhouseReader.NewOptions("primaryNamespace") reader := clickhouseReader.NewReader(nil, telemetryStore, promProvider, "", time.Second, nil, nil, options) rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, reader, promProvider) require.NoError(t, err) alertsFound, err := rule.Eval(context.Background(), evalTime) require.NoError(t, err) assert.Equal(t, c.expectAlerts, alertsFound) }) } }