1package expr 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" 10 "github.com/grafana/grafana/pkg/expr/mathexp" 11) 12 13// Command is an interface for all expression commands. 14type Command interface { 15 NeedsVars() []string 16 Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error) 17} 18 19// MathCommand is a command for a math expression such as "1 + $GA / 2" 20type MathCommand struct { 21 RawExpression string 22 Expression *mathexp.Expr 23 refID string 24} 25 26// NewMathCommand creates a new MathCommand. It will return an error 27// if there is an error parsing expr. 28func NewMathCommand(refID, expr string) (*MathCommand, error) { 29 parsedExpr, err := mathexp.New(expr) 30 if err != nil { 31 return nil, err 32 } 33 return &MathCommand{ 34 RawExpression: expr, 35 Expression: parsedExpr, 36 refID: refID, 37 }, nil 38} 39 40// UnmarshalMathCommand creates a MathCommand from Grafana's frontend query. 41func UnmarshalMathCommand(rn *rawNode) (*MathCommand, error) { 42 rawExpr, ok := rn.Query["expression"] 43 if !ok { 44 return nil, fmt.Errorf("math command for refId %v is missing an expression", rn.RefID) 45 } 46 exprString, ok := rawExpr.(string) 47 if !ok { 48 return nil, fmt.Errorf("expected math command for refId %v expression to be a string, got %T", rn.RefID, rawExpr) 49 } 50 51 gm, err := NewMathCommand(rn.RefID, exprString) 52 if err != nil { 53 return nil, fmt.Errorf("invalid math command type in '%v': %v", rn.RefID, err) 54 } 55 return gm, nil 56} 57 58// NeedsVars returns the variable names (refIds) that are dependencies 59// to execute the command and allows the command to fulfill the Command interface. 60func (gm *MathCommand) NeedsVars() []string { 61 return gm.Expression.VarNames 62} 63 64// Execute runs the command and returns the results or an error if the command 65// failed to execute. 66func (gm *MathCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) { 67 return gm.Expression.Execute(gm.refID, vars) 68} 69 70// ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max. 71type ReduceCommand struct { 72 Reducer string 73 VarToReduce string 74 refID string 75} 76 77// NewReduceCommand creates a new ReduceCMD. 78func NewReduceCommand(refID, reducer, varToReduce string) *ReduceCommand { 79 // TODO: validate reducer here, before execution 80 return &ReduceCommand{ 81 Reducer: reducer, 82 VarToReduce: varToReduce, 83 refID: refID, 84 } 85} 86 87// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query. 88func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) { 89 rawVar, ok := rn.Query["expression"] 90 if !ok { 91 return nil, fmt.Errorf("no variable specified to reduce for refId %v", rn.RefID) 92 } 93 varToReduce, ok := rawVar.(string) 94 if !ok { 95 return nil, fmt.Errorf("expected reduce variable to be a string, got %T for refId %v", rawVar, rn.RefID) 96 } 97 varToReduce = strings.TrimPrefix(varToReduce, "$") 98 99 rawReducer, ok := rn.Query["reducer"] 100 if !ok { 101 return nil, fmt.Errorf("no reducer specified for refId %v", rn.RefID) 102 } 103 redFunc, ok := rawReducer.(string) 104 if !ok { 105 return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID) 106 } 107 108 return NewReduceCommand(rn.RefID, redFunc, varToReduce), nil 109} 110 111// NeedsVars returns the variable names (refIds) that are dependencies 112// to execute the command and allows the command to fulfill the Command interface. 113func (gr *ReduceCommand) NeedsVars() []string { 114 return []string{gr.VarToReduce} 115} 116 117// Execute runs the command and returns the results or an error if the command 118// failed to execute. 119func (gr *ReduceCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) { 120 newRes := mathexp.Results{} 121 for _, val := range vars[gr.VarToReduce].Values { 122 series, ok := val.(mathexp.Series) 123 if !ok { 124 return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type()) 125 } 126 num, err := series.Reduce(gr.refID, gr.Reducer) 127 if err != nil { 128 return newRes, err 129 } 130 newRes.Values = append(newRes.Values, num) 131 } 132 return newRes, nil 133} 134 135// ResampleCommand is an expression command for resampling of a timeseries. 136type ResampleCommand struct { 137 Window time.Duration 138 VarToResample string 139 Downsampler string 140 Upsampler string 141 TimeRange TimeRange 142 refID string 143} 144 145// NewResampleCommand creates a new ResampleCMD. 146func NewResampleCommand(refID, rawWindow, varToResample string, downsampler string, upsampler string, tr TimeRange) (*ResampleCommand, error) { 147 // TODO: validate reducer here, before execution 148 window, err := gtime.ParseDuration(rawWindow) 149 if err != nil { 150 return nil, fmt.Errorf(`failed to parse resample "window" duration field %q: %w`, window, err) 151 } 152 return &ResampleCommand{ 153 Window: window, 154 VarToResample: varToResample, 155 Downsampler: downsampler, 156 Upsampler: upsampler, 157 TimeRange: tr, 158 refID: refID, 159 }, nil 160} 161 162// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query. 163func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error) { 164 rawVar, ok := rn.Query["expression"] 165 if !ok { 166 return nil, fmt.Errorf("no variable to resample for refId %v", rn.RefID) 167 } 168 varToReduce, ok := rawVar.(string) 169 if !ok { 170 return nil, fmt.Errorf("expected resample input variable to be type string, but got type %T for refId %v", rawVar, rn.RefID) 171 } 172 varToReduce = strings.TrimPrefix(varToReduce, "$") 173 varToResample := varToReduce 174 175 rawWindow, ok := rn.Query["window"] 176 if !ok { 177 return nil, fmt.Errorf("no time duration specified for the window in resample command for refId %v", rn.RefID) 178 } 179 window, ok := rawWindow.(string) 180 if !ok { 181 return nil, fmt.Errorf("expected resample window to be a string, got %T for refId %v", rawWindow, rn.RefID) 182 } 183 184 rawDownsampler, ok := rn.Query["downsampler"] 185 if !ok { 186 return nil, fmt.Errorf("no downsampler function specified in resample command for refId %v", rn.RefID) 187 } 188 downsampler, ok := rawDownsampler.(string) 189 if !ok { 190 return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", downsampler, rn.RefID) 191 } 192 193 rawUpsampler, ok := rn.Query["upsampler"] 194 if !ok { 195 return nil, fmt.Errorf("no downsampler specified in resample command for refId %v", rn.RefID) 196 } 197 upsampler, ok := rawUpsampler.(string) 198 if !ok { 199 return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", upsampler, rn.RefID) 200 } 201 202 return NewResampleCommand(rn.RefID, window, varToResample, downsampler, upsampler, rn.TimeRange) 203} 204 205// NeedsVars returns the variable names (refIds) that are dependencies 206// to execute the command and allows the command to fulfill the Command interface. 207func (gr *ResampleCommand) NeedsVars() []string { 208 return []string{gr.VarToResample} 209} 210 211// Execute runs the command and returns the results or an error if the command 212// failed to execute. 213func (gr *ResampleCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) { 214 newRes := mathexp.Results{} 215 for _, val := range vars[gr.VarToResample].Values { 216 series, ok := val.(mathexp.Series) 217 if !ok { 218 return newRes, fmt.Errorf("can only resample type series, got type %v", val.Type()) 219 } 220 num, err := series.Resample(gr.refID, gr.Window, gr.Downsampler, gr.Upsampler, gr.TimeRange.From, gr.TimeRange.To) 221 if err != nil { 222 return newRes, err 223 } 224 newRes.Values = append(newRes.Values, num) 225 } 226 return newRes, nil 227} 228 229// CommandType is the type of the expression command. 230type CommandType int 231 232const ( 233 // TypeUnknown is the CMDType for an unrecognized expression type. 234 TypeUnknown CommandType = iota 235 // TypeMath is the CMDType for a math expression. 236 TypeMath 237 // TypeReduce is the CMDType for a reduction expression. 238 TypeReduce 239 // TypeResample is the CMDType for a resampling expression. 240 TypeResample 241 // TypeClassicConditions is the CMDType for the classic condition operation. 242 TypeClassicConditions 243) 244 245func (gt CommandType) String() string { 246 switch gt { 247 case TypeMath: 248 return "math" 249 case TypeReduce: 250 return "reduce" 251 case TypeResample: 252 return "resample" 253 case TypeClassicConditions: 254 return "classic_conditions" 255 default: 256 return "unknown" 257 } 258} 259 260// ParseCommandType returns a CommandType from its string representation. 261func ParseCommandType(s string) (CommandType, error) { 262 switch s { 263 case "math": 264 return TypeMath, nil 265 case "reduce": 266 return TypeReduce, nil 267 case "resample": 268 return TypeResample, nil 269 case "classic_conditions": 270 return TypeClassicConditions, nil 271 default: 272 return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s) 273 } 274} 275