mirror of
https://github.com/grafana/grafana.git
synced 2026-08-13 06:34:55 -05:00
SSE: Support hysteresis threshold expression (#70998)
* extend threshold command with second evaluator called `unloadEvaluator` * Introduce a new expression command Hysteresis and update Threshold unmarshaller to create the HysteresisCommand if the second eval * add feature flag `recoveryThreshold` * update unmarshal threshold command to not re-marshall because it breaks frame definition by shuffling the schema and data fields
This commit is contained in:
+58
-37
@@ -7,8 +7,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
type ThresholdCommand struct {
|
||||
@@ -16,6 +19,7 @@ type ThresholdCommand struct {
|
||||
RefID string
|
||||
ThresholdFunc string
|
||||
Conditions []float64
|
||||
Invert bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -39,6 +43,8 @@ func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions [
|
||||
if len(conditions) < 1 {
|
||||
return nil, fmt.Errorf("incorrect number of arguments: got %d but need 1", len(conditions))
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("expected threshold function to be one of [%s], got %s", strings.Join(supportedThresholdFuncs, ", "), thresholdFunc)
|
||||
}
|
||||
|
||||
return &ThresholdCommand{
|
||||
@@ -49,50 +55,48 @@ func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions [
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ThresholdConditionJSON struct {
|
||||
Evaluator ConditionEvalJSON `json:"evaluator"`
|
||||
}
|
||||
|
||||
type ConditionEvalJSON struct {
|
||||
Params []float64 `json:"params"`
|
||||
Type string `json:"type"` // e.g. "gt"
|
||||
}
|
||||
|
||||
// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
|
||||
func UnmarshalThresholdCommand(rn *rawNode) (*ThresholdCommand, error) {
|
||||
rawQuery := rn.Query
|
||||
|
||||
rawExpression, ok := rawQuery["expression"]
|
||||
if !ok {
|
||||
func UnmarshalThresholdCommand(rn *rawNode, features featuremgmt.FeatureToggles) (Command, error) {
|
||||
cmdConfig := ThresholdCommandConfig{}
|
||||
if err := json.Unmarshal(rn.QueryRaw, &cmdConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse the threshold command: %w", err)
|
||||
}
|
||||
if cmdConfig.Expression == "" {
|
||||
return nil, fmt.Errorf("no variable specified to reference for refId %v", rn.RefID)
|
||||
}
|
||||
referenceVar, ok := rawExpression.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected threshold variable to be a string, got %T for refId %v", rawExpression, rn.RefID)
|
||||
}
|
||||
|
||||
jsonFromM, err := json.Marshal(rawQuery["conditions"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to remarshal threshold expression body: %w", err)
|
||||
}
|
||||
var conditions []ThresholdConditionJSON
|
||||
if err = json.Unmarshal(jsonFromM, &conditions); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal remarshaled threshold expression body: %w", err)
|
||||
}
|
||||
|
||||
for _, condition := range conditions {
|
||||
if !IsSupportedThresholdFunc(condition.Evaluator.Type) {
|
||||
return nil, fmt.Errorf("expected threshold function to be one of %s, got %s", strings.Join(supportedThresholdFuncs, ", "), condition.Evaluator.Type)
|
||||
}
|
||||
}
|
||||
referenceVar := cmdConfig.Expression
|
||||
|
||||
// we only support one condition for now, we might want to turn this in to "OR" expressions later
|
||||
if len(conditions) != 1 {
|
||||
if len(cmdConfig.Conditions) != 1 {
|
||||
return nil, fmt.Errorf("threshold expression requires exactly one condition")
|
||||
}
|
||||
firstCondition := conditions[0]
|
||||
firstCondition := cmdConfig.Conditions[0]
|
||||
|
||||
return NewThresholdCommand(rn.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params)
|
||||
threshold, err := NewThresholdCommand(rn.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid condition: %w", err)
|
||||
}
|
||||
if firstCondition.UnloadEvaluator != nil && features.IsEnabled(featuremgmt.FlagRecoveryThreshold) {
|
||||
unloading, err := NewThresholdCommand(rn.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params)
|
||||
unloading.Invert = true
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid unloadCondition: %w", err)
|
||||
}
|
||||
var d Fingerprints
|
||||
if firstCondition.LoadedDimensions != nil {
|
||||
d, err = FingerprintsFromFrame(firstCondition.LoadedDimensions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse loaded dimensions: %w", err)
|
||||
}
|
||||
}
|
||||
return NewHysteresisCommand(rn.RefID, referenceVar, *threshold, *unloading, d)
|
||||
}
|
||||
return threshold, nil
|
||||
}
|
||||
|
||||
// NeedsVars returns the variable names (refIds) that are dependencies
|
||||
@@ -102,7 +106,7 @@ func (tc *ThresholdCommand) NeedsVars() []string {
|
||||
}
|
||||
|
||||
func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
|
||||
mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions)
|
||||
mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions, tc.Invert)
|
||||
if err != nil {
|
||||
return mathexp.Results{}, err
|
||||
}
|
||||
@@ -116,19 +120,25 @@ func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mat
|
||||
}
|
||||
|
||||
// createMathExpression converts all the info we have about a "threshold" expression in to a Math expression
|
||||
func createMathExpression(referenceVar string, thresholdFunc string, args []float64) (string, error) {
|
||||
func createMathExpression(referenceVar string, thresholdFunc string, args []float64, invert bool) (string, error) {
|
||||
var exp string
|
||||
switch thresholdFunc {
|
||||
case ThresholdIsAbove:
|
||||
return fmt.Sprintf("${%s} > %f", referenceVar, args[0]), nil
|
||||
exp = fmt.Sprintf("${%s} > %f", referenceVar, args[0])
|
||||
case ThresholdIsBelow:
|
||||
return fmt.Sprintf("${%s} < %f", referenceVar, args[0]), nil
|
||||
exp = fmt.Sprintf("${%s} < %f", referenceVar, args[0])
|
||||
case ThresholdIsWithinRange:
|
||||
return fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1]), nil
|
||||
exp = fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1])
|
||||
case ThresholdIsOutsideRange:
|
||||
return fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1]), nil
|
||||
exp = fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1])
|
||||
default:
|
||||
return "", fmt.Errorf("failed to evaluate threshold expression: no such threshold function %s", thresholdFunc)
|
||||
}
|
||||
|
||||
if invert {
|
||||
return fmt.Sprintf("!(%s)", exp), nil
|
||||
}
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func IsSupportedThresholdFunc(name string) bool {
|
||||
@@ -142,3 +152,14 @@ func IsSupportedThresholdFunc(name string) bool {
|
||||
|
||||
return isSupported
|
||||
}
|
||||
|
||||
type ThresholdCommandConfig struct {
|
||||
Expression string `json:"expression"`
|
||||
Conditions []ThresholdConditionJSON `json:"conditions"`
|
||||
}
|
||||
|
||||
type ThresholdConditionJSON struct {
|
||||
Evaluator ConditionEvalJSON `json:"evaluator"`
|
||||
UnloadEvaluator *ConditionEvalJSON `json:"unloadEvaluator"`
|
||||
LoadedDimensions *data.Frame `json:"loadedDimensions"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user