* add function to convert StateTransition to LokiEntry * add QueryResultBuilder * update backtesting to produce result similar to historian * make shouldRecord public * filter out noop transitions * add experimental front-end * add new fields * move conversion of api model to AlertRule to validation * add extra labels * calculate tick timestamp using the same logic as in scheduler * implement correct logic of calculating first evaluation timestamp * add uid, group and folder uid they are needed for jitter strategy * add JitterOffsetInDuration and JitterStrategy.String() * add config `backtesting_max_evaluations` to [unified_alerting] (not documented for now) * remove obsolete tests * elevate permisisons for backtesting endpoint * move backtesting to separate dir
82 lines
2.7 KiB
Go
82 lines
2.7 KiB
Go
package schedule
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
// JitterStrategy represents a modifier to alert rule timing that affects how evaluations are distributed.
|
|
type JitterStrategy int
|
|
|
|
func (s JitterStrategy) String() string {
|
|
return [...]string{"never", "by group", "by rule"}[s]
|
|
}
|
|
|
|
const (
|
|
JitterNever JitterStrategy = iota
|
|
JitterByGroup
|
|
JitterByRule
|
|
)
|
|
|
|
// JitterStrategyFrom returns the JitterStrategy indicated by the current Grafana feature toggles.
|
|
func JitterStrategyFrom(cfg setting.UnifiedAlertingSettings, toggles featuremgmt.FeatureToggles) JitterStrategy {
|
|
strategy := JitterByGroup
|
|
if cfg.DisableJitter {
|
|
return JitterNever
|
|
}
|
|
if toggles == nil {
|
|
return strategy
|
|
}
|
|
//nolint:staticcheck // not yet migrated to OpenFeature
|
|
if toggles.IsEnabledGlobally(featuremgmt.FlagJitterAlertRulesWithinGroups) {
|
|
strategy = JitterByRule
|
|
}
|
|
return strategy
|
|
}
|
|
|
|
// jitterOffsetInTicks gives the jitter offset for a rule, in terms of a number of ticks relative to its interval and a base interval.
|
|
// The resulting number of ticks is non-negative. We assume the rule is well-formed and has an IntervalSeconds greater to or equal than baseInterval.
|
|
func jitterOffsetInTicks(r *ngmodels.AlertRule, baseInterval time.Duration, strategy JitterStrategy) int64 {
|
|
if strategy == JitterNever {
|
|
return 0
|
|
}
|
|
|
|
itemFrequency := r.IntervalSeconds / int64(baseInterval.Seconds())
|
|
offset := jitterHash(r, strategy) % uint64(itemFrequency)
|
|
// Offset is always nonnegative and less than int64.max, because above we mod by itemFrequency which fits in the positive half of int64.
|
|
// offset <= itemFrequency <= int64.max
|
|
// So, this will not overflow and produce a negative offset.
|
|
res := int64(offset)
|
|
|
|
// Regardless, take an absolute value anyway for an extra layer of safety in case the above logic ever changes.
|
|
// Our contract requires that the result is nonnegative and less than int64.max.
|
|
if res < 0 {
|
|
return -res
|
|
}
|
|
return res
|
|
}
|
|
|
|
// JitterOffsetInDuration gives the jitter offset for a rule, in terms of a duration relative to its interval and a base interval.
|
|
func JitterOffsetInDuration(r *ngmodels.AlertRule, baseInterval time.Duration, strategy JitterStrategy) time.Duration {
|
|
return time.Duration(jitterOffsetInTicks(r, baseInterval, strategy)) * baseInterval
|
|
}
|
|
|
|
func jitterHash(r *ngmodels.AlertRule, strategy JitterStrategy) uint64 {
|
|
ls := data.Labels{
|
|
"name": r.RuleGroup,
|
|
"file": r.NamespaceUID,
|
|
"orgId": fmt.Sprint(r.OrgID),
|
|
}
|
|
|
|
if strategy == JitterByRule {
|
|
ls["uid"] = r.UID
|
|
}
|
|
return uint64(ls.Fingerprint())
|
|
}
|