mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
cloud renamed its composition verb to zip own word, and this module publishes the other half of that seam.
1782 lines
49 KiB
Go
1782 lines
49 KiB
Go
package rules
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
dsmock "github.com/hanzo-ds/mock"
|
|
pql "github.com/prometheus/prometheus/promql"
|
|
|
|
"github.com/hanzoai/o11y/pkg/instrumentation/instrumentationtest"
|
|
"github.com/hanzoai/o11y/pkg/prometheus"
|
|
"github.com/hanzoai/o11y/pkg/prometheus/prometheustest"
|
|
"github.com/hanzoai/o11y/pkg/telemetrystore"
|
|
"github.com/hanzoai/o11y/pkg/telemetrystore/telemetrystoretest"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/hanzoai/o11y/pkg/types/ruletypes"
|
|
"github.com/hanzoai/o11y/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: valuer.MustParseTextDuration("5m"),
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{
|
|
Query: "dummy_query", // This is not used in the test
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
cases := []struct {
|
|
values pql.Series
|
|
expectAlert bool
|
|
compareOperator ruletypes.CompareOperator
|
|
matchType ruletypes.MatchType
|
|
target float64
|
|
expectedAlertSample qbtypes.TimeSeriesValue
|
|
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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 1.5,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 2.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 13.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 4.5,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 0.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.AllTheTimes,
|
|
target: 4,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.AllTheTimes,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.AtleastOnce,
|
|
target: 4,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.OnAverage,
|
|
target: 6.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.OnAverage,
|
|
target: 4.5,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.OnAverage,
|
|
target: 4.5,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.OnAverage,
|
|
target: 12.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.InTotal,
|
|
target: 30.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{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,
|
|
compareOperator: ruletypes.ValueIsEq,
|
|
matchType: ruletypes.InTotal,
|
|
target: 20.0,
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: true,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.InTotal,
|
|
target: 9.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{Value: 10.0},
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: false,
|
|
compareOperator: ruletypes.ValueIsNotEq,
|
|
matchType: ruletypes.InTotal,
|
|
target: 10.0,
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: true,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.InTotal,
|
|
target: 10.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{Value: 20.0},
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: false,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.InTotal,
|
|
target: 20.0,
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: true,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.InTotal,
|
|
target: 30.0,
|
|
expectedAlertSample: qbtypes.TimeSeriesValue{Value: 20.0},
|
|
},
|
|
{
|
|
values: pql.Series{
|
|
Floats: []pql.FPoint{
|
|
{F: 10.0},
|
|
{F: 10.0},
|
|
},
|
|
},
|
|
expectAlert: false,
|
|
compareOperator: ruletypes.ValueIsBelow,
|
|
matchType: ruletypes.InTotal,
|
|
target: 20.0,
|
|
},
|
|
}
|
|
|
|
logger := instrumentationtest.New().Logger()
|
|
|
|
for idx, c := range cases {
|
|
postableRule.RuleCondition.CompareOperator = c.compareOperator
|
|
postableRule.RuleCondition.MatchType = c.matchType
|
|
postableRule.RuleCondition.Target = &c.target
|
|
postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{
|
|
Kind: ruletypes.BasicThresholdKind,
|
|
Spec: ruletypes.BasicRuleThresholds{
|
|
{
|
|
TargetValue: &c.target,
|
|
MatchType: c.matchType,
|
|
CompareOperator: c.compareOperator,
|
|
},
|
|
},
|
|
}
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, nil, externalUrl)
|
|
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: valuer.MustParseTextDuration("5m"),
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{
|
|
Query: "test_metric",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// time_series_v4 cols of interest
|
|
fingerprintCols := []dsmock.ColumnType{
|
|
{Name: "fingerprint", Type: "UInt64"},
|
|
{Name: "any(labels)", Type: "String"},
|
|
}
|
|
|
|
// samples_v4 columns
|
|
samplesCols := []dsmock.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
|
|
compareOperator ruletypes.CompareOperator
|
|
matchType ruletypes.MatchType
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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 := [][]any{
|
|
{fingerprint, labelsJSON},
|
|
}
|
|
fingerprintRows := dsmock.NewRows(fingerprintCols, fingerprintData)
|
|
|
|
// create samples data from test case values
|
|
samplesData := make([][]any, len(c.values))
|
|
for i, v := range c.values {
|
|
samplesData[i] = []any{
|
|
"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 := dsmock.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{Timeout: 2 * time.Minute}, telemetryStore)
|
|
|
|
postableRule.RuleCondition.CompareOperator = c.compareOperator
|
|
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: c.matchType,
|
|
CompareOperator: c.compareOperator,
|
|
},
|
|
},
|
|
}
|
|
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}}",
|
|
}
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
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 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: valuer.MustParseTextDuration("5m"),
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{
|
|
Query: "test_metric",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
AlertOnAbsent: true,
|
|
},
|
|
}
|
|
|
|
// time_series_v4 cols of interest
|
|
fingerprintCols := []dsmock.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 := [][]any{}
|
|
fingerprintRows := dsmock.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{Timeout: 2 * time.Minute}, telemetryStore)
|
|
|
|
var target float64 = 0
|
|
postableRule.RuleCondition.Thresholds = &ruletypes.RuleThresholdData{
|
|
Kind: ruletypes.BasicThresholdKind,
|
|
Spec: ruletypes.BasicRuleThresholds{
|
|
{
|
|
Name: postableRule.AlertName,
|
|
TargetValue: &target,
|
|
MatchType: ruletypes.AtleastOnce,
|
|
CompareOperator: 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}}",
|
|
}
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
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(ruletypes.AlertNameLabel), "[No data]"), "case %d", idx)
|
|
} else {
|
|
assert.False(t, strings.Contains(item.Labels.Get(ruletypes.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: valuer.MustParseTextDuration("5m"),
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{
|
|
Query: "test_metric",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
fingerprintCols := []dsmock.ColumnType{
|
|
{Name: "fingerprint", Type: "UInt64"},
|
|
{Name: "any(labels)", Type: "String"},
|
|
}
|
|
|
|
samplesCols := []dsmock.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
|
|
compareOperator ruletypes.CompareOperator
|
|
matchType ruletypes.MatchType
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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,
|
|
compareOperator: ruletypes.ValueIsAbove,
|
|
matchType: ruletypes.AtleastOnce,
|
|
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 := [][]any{
|
|
{fingerprint, labelsJSON},
|
|
}
|
|
fingerprintRows := dsmock.NewRows(fingerprintCols, fingerprintData)
|
|
|
|
samplesData := make([][]any, len(c.values))
|
|
for i, v := range c.values {
|
|
samplesData[i] = []any{
|
|
"test_metric",
|
|
fingerprint,
|
|
v.timestamp.UnixMilli(),
|
|
v.value,
|
|
uint32(0),
|
|
}
|
|
}
|
|
samplesRows := dsmock.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{Timeout: 2 * time.Minute}, telemetryStore)
|
|
|
|
postableRule.RuleCondition.CompareOperator = c.compareOperator
|
|
postableRule.RuleCondition.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: c.matchType,
|
|
CompareOperator: c.compareOperator,
|
|
},
|
|
{
|
|
Name: "second_threshold",
|
|
TargetValue: &c.secondTarget,
|
|
TargetUnit: c.targetUnit,
|
|
MatchType: c.matchType,
|
|
CompareOperator: c.compareOperator,
|
|
},
|
|
},
|
|
}
|
|
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}}",
|
|
}
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("69", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
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: valuer.MustParseTextDuration("5m"),
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompareOperator: ruletypes.ValueIsAbove,
|
|
MatchType: ruletypes.AtleastOnce,
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{Query: "test_metric"},
|
|
},
|
|
},
|
|
},
|
|
Thresholds: &ruletypes.RuleThresholdData{
|
|
Kind: ruletypes.BasicThresholdKind,
|
|
Spec: ruletypes.BasicRuleThresholds{{Name: "Test no data"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
// time_series_v4 cols of interest
|
|
fingerprintCols := []dsmock.ColumnType{
|
|
{Name: "fingerprint", Type: "UInt64"},
|
|
{Name: "any(labels)", Type: "String"},
|
|
}
|
|
|
|
// samples_v4 columns
|
|
samplesCols := []dsmock.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(dsmock.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(dsmock.NewRows(samplesCols, [][]any{}))
|
|
|
|
promProvider := prometheustest.New(
|
|
context.Background(),
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
prometheus.Config{Timeout: 2 * time.Minute},
|
|
telemetryStore,
|
|
)
|
|
defer func() {
|
|
_ = promProvider.Close()
|
|
}()
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
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 := valuer.MustParseTextDuration("5m")
|
|
|
|
// 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: evalWindow,
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompareOperator: ruletypes.ValueIsAbove,
|
|
MatchType: ruletypes.AtleastOnce,
|
|
AlertOnAbsent: true,
|
|
Target: &target,
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{Query: "test_metric"},
|
|
},
|
|
},
|
|
},
|
|
Thresholds: &ruletypes.RuleThresholdData{
|
|
Kind: ruletypes.BasicThresholdKind,
|
|
Spec: ruletypes.BasicRuleThresholds{{
|
|
Name: "Test no data with AbsentFor",
|
|
TargetValue: &target,
|
|
MatchType: ruletypes.AtleastOnce,
|
|
CompareOperator: ruletypes.ValueIsAbove,
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
|
|
fingerprintCols := []dsmock.ColumnType{
|
|
{Name: "fingerprint", Type: "UInt64"},
|
|
{Name: "any(labels)", Type: "String"},
|
|
}
|
|
|
|
samplesCols := []dsmock.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(dsmock.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(dsmock.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(dsmock.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(dsmock.NewRows(samplesCols, [][]any{})) // empty - no data
|
|
|
|
promProvider := prometheustest.New(
|
|
context.Background(),
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
prometheus.Config{Timeout: 2 * time.Minute},
|
|
telemetryStore,
|
|
)
|
|
defer func() {
|
|
_ = promProvider.Close()
|
|
}()
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
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 := valuer.MustParseTextDuration("5m")
|
|
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: evalWindow,
|
|
Frequency: valuer.MustParseTextDuration("1m"),
|
|
}},
|
|
RuleCondition: &ruletypes.RuleCondition{
|
|
CompareOperator: ruletypes.ValueIsAbove,
|
|
MatchType: ruletypes.AtleastOnce,
|
|
CompositeQuery: &ruletypes.AlertCompositeQuery{
|
|
QueryType: ruletypes.QueryTypePromQL,
|
|
Queries: []qbtypes.QueryEnvelope{
|
|
{
|
|
Type: qbtypes.QueryTypePromQL,
|
|
Spec: qbtypes.PromQuery{Query: "test_metric"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
fingerprintCols := []dsmock.ColumnType{
|
|
{Name: "fingerprint", Type: "UInt64"},
|
|
{Name: "any(labels)", Type: "String"},
|
|
}
|
|
fingerprint := uint64(12345)
|
|
fingerprintData := [][]any{{fingerprint, `{"__name__":"test_metric"}`}}
|
|
|
|
samplesCols := []dsmock.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,
|
|
CompareOperator: rc.CompareOperator,
|
|
},
|
|
},
|
|
}
|
|
|
|
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(dsmock.NewRows(fingerprintCols, fingerprintData))
|
|
telemetryStore.Mock().
|
|
ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
|
|
WithArgs("test_metric", "test_metric", "__name__", "test_metric", queryStart, queryEnd).
|
|
WillReturnRows(dsmock.NewRows(samplesCols, samplesData))
|
|
promProvider := prometheustest.New(
|
|
context.Background(),
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
prometheus.Config{Timeout: 2 * time.Minute, LookbackDelta: lookBackDelta},
|
|
telemetryStore,
|
|
)
|
|
defer func() {
|
|
_ = promProvider.Close()
|
|
}()
|
|
|
|
externalUrl := mustParseURL(t, "http://localhost:8080")
|
|
rule, err := NewPromRule("some-id", valuer.GenerateUUID(), &postableRule, logger, promProvider, externalUrl)
|
|
require.NoError(t, err)
|
|
|
|
alertsFound, err := rule.Eval(context.Background(), evalTime)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, c.expectAlerts, alertsFound)
|
|
})
|
|
}
|
|
}
|