mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-03 08:33:26 +00:00
1769 lines
48 KiB
Go
1769 lines
48 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|