2022-09-26 09:05:44 -05:00
package expr
import (
"context"
"encoding/json"
2023-12-11 14:40:31 -06:00
"errors"
2022-09-26 09:05:44 -05:00
"fmt"
"strings"
2022-10-26 15:13:58 -05:00
"time"
2022-09-26 09:05:44 -05:00
2023-10-10 09:51:50 -05:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2022-09-26 09:05:44 -05:00
"github.com/grafana/grafana/pkg/expr/mathexp"
2023-04-18 07:04:51 -05:00
"github.com/grafana/grafana/pkg/infra/tracing"
2023-10-10 09:51:50 -05:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-09-26 09:05:44 -05:00
)
type ThresholdCommand struct {
ReferenceVar string
RefID string
ThresholdFunc string
Conditions [ ] float64
2023-10-10 09:51:50 -05:00
Invert bool
2022-09-26 09:05:44 -05:00
}
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 ) {
2023-04-20 04:42:34 -05:00
switch thresholdFunc {
case ThresholdIsOutsideRange , ThresholdIsWithinRange :
if len ( conditions ) < 2 {
return nil , fmt . Errorf ( "incorrect number of arguments: got %d but need 2" , len ( conditions ) )
}
case ThresholdIsAbove , ThresholdIsBelow :
if len ( conditions ) < 1 {
return nil , fmt . Errorf ( "incorrect number of arguments: got %d but need 1" , len ( conditions ) )
}
2023-10-10 09:51:50 -05:00
default :
return nil , fmt . Errorf ( "expected threshold function to be one of [%s], got %s" , strings . Join ( supportedThresholdFuncs , ", " ) , thresholdFunc )
2023-04-20 04:42:34 -05:00
}
2022-09-26 09:05:44 -05:00
return & ThresholdCommand {
RefID : refID ,
ReferenceVar : referenceVar ,
ThresholdFunc : thresholdFunc ,
Conditions : conditions ,
} , nil
}
type ConditionEvalJSON struct {
Params [ ] float64 ` json:"params" `
Type string ` json:"type" ` // e.g. "gt"
}
// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
2023-10-10 09:51:50 -05:00
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 == "" {
2022-09-26 09:05:44 -05:00
return nil , fmt . Errorf ( "no variable specified to reference for refId %v" , rn . RefID )
}
2023-10-10 09:51:50 -05:00
referenceVar := cmdConfig . Expression
// we only support one condition for now, we might want to turn this in to "OR" expressions later
if len ( cmdConfig . Conditions ) != 1 {
return nil , fmt . Errorf ( "threshold expression requires exactly one condition" )
2022-09-26 09:05:44 -05:00
}
2023-10-10 09:51:50 -05:00
firstCondition := cmdConfig . Conditions [ 0 ]
2022-09-26 09:05:44 -05:00
2023-10-10 09:51:50 -05:00
threshold , err := NewThresholdCommand ( rn . RefID , referenceVar , firstCondition . Evaluator . Type , firstCondition . Evaluator . Params )
2022-09-26 09:05:44 -05:00
if err != nil {
2023-10-10 09:51:50 -05:00
return nil , fmt . Errorf ( "invalid condition: %w" , err )
2022-09-26 09:05:44 -05:00
}
2023-11-14 14:50:27 -06:00
if firstCondition . UnloadEvaluator != nil && features . IsEnabledGlobally ( featuremgmt . FlagRecoveryThreshold ) {
2023-10-10 09:51:50 -05:00
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 )
2022-09-26 09:05:44 -05:00
}
2023-10-10 09:51:50 -05:00
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 )
2022-09-26 09:05:44 -05:00
}
2023-10-10 09:51:50 -05:00
return threshold , nil
2022-09-26 09:05:44 -05:00
}
// 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 }
}
2023-04-18 07:04:51 -05:00
func ( tc * ThresholdCommand ) Execute ( ctx context . Context , now time . Time , vars mathexp . Vars , tracer tracing . Tracer ) ( mathexp . Results , error ) {
2023-10-10 09:51:50 -05:00
mathExpression , err := createMathExpression ( tc . ReferenceVar , tc . ThresholdFunc , tc . Conditions , tc . Invert )
2022-09-26 09:05:44 -05:00
if err != nil {
return mathexp . Results { } , err
}
mathCommand , err := NewMathCommand ( tc . ReferenceVar , mathExpression )
if err != nil {
return mathexp . Results { } , err
}
2023-04-18 07:04:51 -05:00
return mathCommand . Execute ( ctx , now , vars , tracer )
2022-09-26 09:05:44 -05:00
}
// createMathExpression converts all the info we have about a "threshold" expression in to a Math expression
2023-10-10 09:51:50 -05:00
func createMathExpression ( referenceVar string , thresholdFunc string , args [ ] float64 , invert bool ) ( string , error ) {
var exp string
2022-09-26 09:05:44 -05:00
switch thresholdFunc {
case ThresholdIsAbove :
2023-10-10 09:51:50 -05:00
exp = fmt . Sprintf ( "${%s} > %f" , referenceVar , args [ 0 ] )
2022-09-26 09:05:44 -05:00
case ThresholdIsBelow :
2023-10-10 09:51:50 -05:00
exp = fmt . Sprintf ( "${%s} < %f" , referenceVar , args [ 0 ] )
2022-09-26 09:05:44 -05:00
case ThresholdIsWithinRange :
2023-10-10 09:51:50 -05:00
exp = fmt . Sprintf ( "${%s} > %f && ${%s} < %f" , referenceVar , args [ 0 ] , referenceVar , args [ 1 ] )
2022-09-26 09:05:44 -05:00
case ThresholdIsOutsideRange :
2023-10-10 09:51:50 -05:00
exp = fmt . Sprintf ( "${%s} < %f || ${%s} > %f" , referenceVar , args [ 0 ] , referenceVar , args [ 1 ] )
2022-09-26 09:05:44 -05:00
default :
return "" , fmt . Errorf ( "failed to evaluate threshold expression: no such threshold function %s" , thresholdFunc )
}
2023-10-10 09:51:50 -05:00
if invert {
return fmt . Sprintf ( "!(%s)" , exp ) , nil
}
return exp , nil
2022-09-26 09:05:44 -05:00
}
func IsSupportedThresholdFunc ( name string ) bool {
isSupported := false
for _ , funcName := range supportedThresholdFuncs {
if funcName == name {
isSupported = true
}
}
return isSupported
}
2023-10-10 09:51:50 -05:00
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" `
}
2023-12-11 14:40:31 -06:00
// IsHysteresisExpression returns true if the raw model describes a hysteresis command:
// - field 'type' has value "threshold",
// - field 'conditions' is array of objects and has exactly one element
// - field 'conditions[0].unloadEvaluator is not nil
func IsHysteresisExpression ( query map [ string ] any ) bool {
c , err := getConditionForHysteresisCommand ( query )
if err != nil {
return false
}
return c != nil
}
// SetLoadedDimensionsToHysteresisCommand mutates the input map and sets field "conditions[0].loadedMetrics" with the data frame created from the provided fingerprints.
func SetLoadedDimensionsToHysteresisCommand ( query map [ string ] any , fingerprints Fingerprints ) error {
condition , err := getConditionForHysteresisCommand ( query )
if err != nil {
return err
}
if condition == nil {
return errors . New ( "not a hysteresis command" )
}
fr := FingerprintsToFrame ( fingerprints )
condition [ "loadedDimensions" ] = fr
return nil
}
func getConditionForHysteresisCommand ( query map [ string ] any ) ( map [ string ] any , error ) {
t , err := GetExpressionCommandType ( query )
if err != nil {
return nil , err
}
if t != TypeThreshold {
return nil , errors . New ( "not a threshold command" )
}
c , ok := query [ "conditions" ]
if ! ok {
return nil , errors . New ( "invalid threshold command: expected field \"condition\"" )
}
var condition map [ string ] any
switch arr := c . ( type ) {
case [ ] any :
if len ( arr ) != 1 {
return nil , errors . New ( "invalid threshold command: field \"condition\" expected to have exactly 1 field" )
}
switch m := arr [ 0 ] . ( type ) {
case map [ string ] any :
condition = m
default :
return nil , errors . New ( "invalid threshold command: value of the first element of field \"condition\" expected to be an object" )
}
default :
return nil , errors . New ( "invalid threshold command: field \"condition\" expected to be an array of objects" )
}
_ , ok = condition [ "unloadEvaluator" ]
if ! ok {
return nil , nil
}
return condition , nil
}