| 
									
										
										
										
											2022-09-26 22:05:44 +08:00
										 |  |  | package expr | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2022-10-27 04:13:58 +08:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2022-09-26 22:05:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/expr/mathexp" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ThresholdCommand struct { | 
					
						
							|  |  |  | 	ReferenceVar  string | 
					
						
							|  |  |  | 	RefID         string | 
					
						
							|  |  |  | 	ThresholdFunc string | 
					
						
							|  |  |  | 	Conditions    []float64 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ( | 
					
						
							|  |  |  | 	ThresholdIsAbove        = "gt" | 
					
						
							|  |  |  | 	ThresholdIsBelow        = "lt" | 
					
						
							|  |  |  | 	ThresholdIsWithinRange  = "within_range" | 
					
						
							|  |  |  | 	ThresholdIsOutsideRange = "outside_range" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var ( | 
					
						
							|  |  |  | 	supportedThresholdFuncs = []string{ThresholdIsAbove, ThresholdIsBelow, ThresholdIsWithinRange, ThresholdIsOutsideRange} | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions []float64) (*ThresholdCommand, error) { | 
					
						
							|  |  |  | 	return &ThresholdCommand{ | 
					
						
							|  |  |  | 		RefID:         refID, | 
					
						
							|  |  |  | 		ReferenceVar:  referenceVar, | 
					
						
							|  |  |  | 		ThresholdFunc: thresholdFunc, | 
					
						
							|  |  |  | 		Conditions:    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 { | 
					
						
							|  |  |  | 		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) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// we only support one condition for now, we might want to turn this in to "OR" expressions later
 | 
					
						
							|  |  |  | 	if len(conditions) != 1 { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("threshold expression requires exactly one condition") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	firstCondition := conditions[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return NewThresholdCommand(rn.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // NeedsVars returns the variable names (refIds) that are dependencies
 | 
					
						
							|  |  |  | // to execute the command and allows the command to fulfill the Command interface.
 | 
					
						
							|  |  |  | func (tc *ThresholdCommand) NeedsVars() []string { | 
					
						
							|  |  |  | 	return []string{tc.ReferenceVar} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-27 04:13:58 +08:00
										 |  |  | func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars) (mathexp.Results, error) { | 
					
						
							| 
									
										
										
										
											2022-09-26 22:05:44 +08:00
										 |  |  | 	mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return mathexp.Results{}, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	mathCommand, err := NewMathCommand(tc.ReferenceVar, mathExpression) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return mathexp.Results{}, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-27 04:13:58 +08:00
										 |  |  | 	return mathCommand.Execute(ctx, now, vars) | 
					
						
							| 
									
										
										
										
											2022-09-26 22:05:44 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // 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) { | 
					
						
							|  |  |  | 	switch thresholdFunc { | 
					
						
							|  |  |  | 	case ThresholdIsAbove: | 
					
						
							|  |  |  | 		return fmt.Sprintf("${%s} > %f", referenceVar, args[0]), nil | 
					
						
							|  |  |  | 	case ThresholdIsBelow: | 
					
						
							|  |  |  | 		return fmt.Sprintf("${%s} < %f", referenceVar, args[0]), nil | 
					
						
							|  |  |  | 	case ThresholdIsWithinRange: | 
					
						
							|  |  |  | 		return fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1]), nil | 
					
						
							|  |  |  | 	case ThresholdIsOutsideRange: | 
					
						
							|  |  |  | 		return fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1]), nil | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		return "", fmt.Errorf("failed to evaluate threshold expression: no such threshold function %s", thresholdFunc) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func IsSupportedThresholdFunc(name string) bool { | 
					
						
							|  |  |  | 	isSupported := false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, funcName := range supportedThresholdFuncs { | 
					
						
							|  |  |  | 		if funcName == name { | 
					
						
							|  |  |  | 			isSupported = true | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return isSupported | 
					
						
							|  |  |  | } |