Compare commits

...

4 Commits

Author SHA1 Message Date
Abhishek Kumar Singh
662bfa5b7b chore: skip nan and inf values from evaluating series 2026-02-05 20:57:49 +05:30
Abhishek Kumar Singh
ecbf8b21be Merge branch 'main' into chore/avoid_series_copy_on_threshold_eval 2026-02-05 17:18:37 +05:30
Srikanth Chekuri
a31038d23c Merge branch 'main' into chore/avoid_series_copy_on_threshold_eval 2026-01-19 22:59:35 +05:30
Abhishek Kumar Singh
444beeca34 chore: accept series pointer in threshold eval to avoid repeated series copy 2026-01-19 21:29:02 +05:30
9 changed files with 34 additions and 37 deletions

View File

@@ -249,7 +249,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
continue
}
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
results, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})
@@ -317,7 +317,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
continue
}
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
results, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"math"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -169,7 +170,7 @@ func (r *PromRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletype
)
continue
}
resultSeries, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
resultSeries, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})
@@ -467,6 +468,10 @@ func toCommonSeries(series promql.Series) v3.Series {
}
for _, f := range series.Floats {
// skip the point if it is nan, inf, or timestamp is zero
if math.IsNaN(f.F) || math.IsInf(f.F, 0) || f.T == 0 {
continue
}
commonSeries.Points = append(commonSeries.Points, v3.Point{
Timestamp: f.T,
Value: f.F,

View File

@@ -708,7 +708,8 @@ func TestPromRuleEval(t *testing.T) {
assert.NoError(t, err)
}
resultVectors, err := rule.Threshold.Eval(toCommonSeries(c.values), rule.Unit(), ruletypes.EvalData{})
series := toCommonSeries(c.values)
resultVectors, err := rule.Threshold.Eval(&series, rule.Unit(), ruletypes.EvalData{})
assert.NoError(t, err)
// Compare full result vector with expected vector

View File

@@ -478,7 +478,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID,
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
continue
}
resultSeries, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
resultSeries, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})
@@ -553,7 +553,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
continue
}
resultSeries, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
resultSeries, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})

View File

@@ -82,7 +82,7 @@ func TestThresholdRuleEvalBackwardCompat(t *testing.T) {
values.Points[i].Timestamp = time.Now().UnixMilli()
}
resultVectors, err := rule.Threshold.Eval(c.values, rule.Unit(), ruletypes.EvalData{
resultVectors, err := rule.Threshold.Eval(&c.values, rule.Unit(), ruletypes.EvalData{
ActiveAlerts: map[uint64]struct{}{},
})
assert.NoError(t, err, "Test case %d", idx)
@@ -461,7 +461,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
values.Points[i].Timestamp = time.Now().UnixMilli()
}
vector, err := rule.Threshold.Eval(c.values, rule.Unit(), ruletypes.EvalData{})
vector, err := rule.Threshold.Eval(&c.values, rule.Unit(), ruletypes.EvalData{})
assert.NoError(t, err)
for name, value := range c.values.Labels {
@@ -471,7 +471,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) {
}
// Get result vectors from threshold evaluation
resultVectors, err := rule.Threshold.Eval(c.values, rule.Unit(), ruletypes.EvalData{})
resultVectors, err := rule.Threshold.Eval(&c.values, rule.Unit(), ruletypes.EvalData{})
assert.NoError(t, err, "Test case %d", idx)
// Compare result vectors with expected behavior
@@ -1582,7 +1582,7 @@ func TestThresholdRuleEval_SendUnmatchedBypassesRecovery(t *testing.T) {
alertLabels := ruletypes.PrepareSampleLabelsForRule(series.Labels, "primary")
activeAlerts := map[uint64]struct{}{alertLabels.Hash(): {}}
resultVectors, err := rule.Threshold.Eval(series, rule.Unit(), ruletypes.EvalData{
resultVectors, err := rule.Threshold.Eval(&series, rule.Unit(), ruletypes.EvalData{
ActiveAlerts: activeAlerts,
SendUnmatched: true,
})
@@ -1850,7 +1850,7 @@ func runEvalTests(t *testing.T, postableRule ruletypes.PostableRule, testCases [
SendUnmatched: c.sendUnmatched,
}
resultVectors, err := rule.Threshold.Eval(values, rule.Unit(), evalData)
resultVectors, err := rule.Threshold.Eval(&values, rule.Unit(), evalData)
assert.NoError(t, err)
if c.expectSamples != nil {
@@ -1958,7 +1958,7 @@ func runMultiThresholdEvalTests(t *testing.T, postableRule ruletypes.PostableRul
ActiveAlerts: activeAlerts,
}
resultVectors, err := rule.Threshold.Eval(values, rule.Unit(), evalData)
resultVectors, err := rule.Threshold.Eval(&values, rule.Unit(), evalData)
assert.NoError(t, err)
// Validate total sample count

View File

@@ -2,6 +2,7 @@ package transition
import (
"fmt"
"math"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -39,6 +40,11 @@ func ConvertV5TimeSeriesDataToV4Result(v5Data *qbtypes.TimeSeriesData) *v3.Resul
continue
}
// skip the point if it is nan, inf, or timestamp is zero
if math.IsNaN(tsValue.Value) || math.IsInf(tsValue.Value, 0) || tsValue.Timestamp == 0 {
continue
}
point := v3.Point{
Timestamp: tsValue.Timestamp,
Value: tsValue.Value,

View File

@@ -621,7 +621,7 @@ func TestParseIntoRuleThresholdGeneration(t *testing.T) {
}
// Test that threshold can evaluate properly
vector, err := threshold.Eval(v3.Series{
vector, err := threshold.Eval(&v3.Series{
Points: []v3.Point{{Value: 0.15, Timestamp: 1000}}, // 150ms in seconds
Labels: map[string]string{"test": "label"},
}, "", EvalData{})
@@ -698,7 +698,7 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
}
// Test with a value that should trigger both WARNING and CRITICAL thresholds
vector, err := threshold.Eval(v3.Series{
vector, err := threshold.Eval(&v3.Series{
Points: []v3.Point{{Value: 95.0, Timestamp: 1000}}, // 95% CPU usage
Labels: map[string]string{"service": "test"},
}, "", EvalData{})
@@ -708,7 +708,7 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
assert.Equal(t, 2, len(vector))
vector, err = threshold.Eval(v3.Series{
vector, err = threshold.Eval(&v3.Series{
Points: []v3.Point{{Value: 75.0, Timestamp: 1000}}, // 75% CPU usage
Labels: map[string]string{"service": "test"},
}, "", EvalData{})
@@ -1046,7 +1046,7 @@ func TestAnomalyNegationEval(t *testing.T) {
t.Fatalf("unexpected error from GetRuleThreshold: %v", err)
}
resultVector, err := ruleThreshold.Eval(tt.series, "", EvalData{})
resultVector, err := ruleThreshold.Eval(&tt.series, "", EvalData{})
if err != nil {
t.Fatalf("unexpected error from Eval: %v", err)
}

View File

@@ -83,7 +83,7 @@ func (eval EvalData) HasActiveAlert(sampleLabelFp uint64) bool {
type RuleThreshold interface {
// Eval runs the given series through the threshold rules
// using the given EvalData and returns the matching series
Eval(series v3.Series, unit string, evalData EvalData) (Vector, error)
Eval(series *v3.Series, unit string, evalData EvalData) (Vector, error)
GetRuleReceivers() []RuleReceivers
}
@@ -122,7 +122,7 @@ func (r BasicRuleThresholds) Validate() error {
return errors.Join(errs...)
}
func (r BasicRuleThresholds) Eval(series v3.Series, unit string, evalData EvalData) (Vector, error) {
func (r BasicRuleThresholds) Eval(series *v3.Series, unit string, evalData EvalData) (Vector, error) {
var resultVector Vector
thresholds := []BasicRuleThreshold(r)
sortThresholds(thresholds)
@@ -137,8 +137,6 @@ func (r BasicRuleThresholds) Eval(series v3.Series, unit string, evalData EvalDa
resultVector = append(resultVector, smpl)
continue
} else if evalData.SendUnmatched {
// Sanitise the series points to remove any NaN or Inf values
series.Points = removeGroupinSetPoints(series)
if len(series.Points) == 0 {
continue
}
@@ -255,23 +253,13 @@ func (b BasicRuleThreshold) Validate() error {
return errors.Join(errs...)
}
func (b BasicRuleThreshold) matchesRecoveryThreshold(series v3.Series, ruleUnit string) (Sample, bool) {
func (b BasicRuleThreshold) matchesRecoveryThreshold(series *v3.Series, ruleUnit string) (Sample, bool) {
return b.shouldAlertWithTarget(series, b.recoveryTarget(ruleUnit))
}
func (b BasicRuleThreshold) shouldAlert(series v3.Series, ruleUnit string) (Sample, bool) {
func (b BasicRuleThreshold) shouldAlert(series *v3.Series, ruleUnit string) (Sample, bool) {
return b.shouldAlertWithTarget(series, b.target(ruleUnit))
}
func removeGroupinSetPoints(series v3.Series) []v3.Point {
var result []v3.Point
for _, s := range series.Points {
if s.Timestamp >= 0 && !math.IsNaN(s.Value) && !math.IsInf(s.Value, 0) {
result = append(result, s)
}
}
return result
}
// PrepareSampleLabelsForRule prepares the labels for the sample to be used in the alerting.
// It accepts seriesLabels and thresholdName as input and returns the labels with the threshold name label added.
func PrepareSampleLabelsForRule(seriesLabels map[string]string, thresholdName string) (lbls labels.Labels) {
@@ -284,13 +272,10 @@ func PrepareSampleLabelsForRule(seriesLabels map[string]string, thresholdName st
return lb.Labels()
}
func (b BasicRuleThreshold) shouldAlertWithTarget(series v3.Series, target float64) (Sample, bool) {
func (b BasicRuleThreshold) shouldAlertWithTarget(series *v3.Series, target float64) (Sample, bool) {
var shouldAlert bool
var alertSmpl Sample
lbls := PrepareSampleLabelsForRule(series.Labels, b.Name)
series.Points = removeGroupinSetPoints(series)
// nothing to evaluate
if len(series.Points) == 0 {
return alertSmpl, false

View File

@@ -270,7 +270,7 @@ func TestBasicRuleThresholdEval_UnitConversion(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
thresholds := BasicRuleThresholds{tt.threshold}
vector, err := thresholds.Eval(tt.series, tt.ruleUnit, EvalData{})
vector, err := thresholds.Eval(&tt.series, tt.ruleUnit, EvalData{})
assert.NoError(t, err)
alert := len(vector) > 0