Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): added full trigger validation in api #957

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/dto/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (trigger *Trigger) Bind(request *http.Request) error {
return err
}

if err := checkTTLSanity(trigger, metricsSource); err != nil {
if err = checkTTLSanity(trigger, metricsSource); err != nil {
return api.ErrInvalidRequestContent{ValidationError: err}
}

Expand All @@ -202,7 +202,7 @@ func (trigger *Trigger) Bind(request *http.Request) error {

middleware.SetTimeSeriesNames(request, metricsDataNames)

if _, err := triggerExpression.Evaluate(); err != nil {
if err = triggerExpression.Validate(); err != nil {
return err
}

Expand Down
58 changes: 42 additions & 16 deletions expression/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"fmt"
"strings"

"github.com/antonmedv/expr"
"github.com/patrickmn/go-cache"

"github.com/Knetic/govaluate"

"github.com/moira-alert/moira"
)

Expand Down Expand Up @@ -77,6 +79,45 @@ func (triggerExpression TriggerExpression) Get(name string) (interface{}, error)
}
}

// Validate returns error if triggers of type moira.ExpressionTrigger are badly formatted otherwise nil
func (triggerExpression *TriggerExpression) Validate() error {
if triggerExpression.TriggerType != moira.ExpressionTrigger {
return nil
}
if triggerExpression.Expression == nil || *triggerExpression.Expression == "" {
return ErrInvalidExpression{
internalError: fmt.Errorf("trigger_type set to expression, but no expression provided"),
}
}
expression := *triggerExpression.Expression
env := map[string]interface{}{
"ok": moira.StateOK,
"error": moira.StateERROR,
"warn": moira.StateWARN,
"warning": moira.StateWARN,
"nodata": moira.StateNODATA,
"t1": triggerExpression.MainTargetValue,
"prev_state": triggerExpression.PreviousState,
}
if triggerExpression.WarnValue != nil {
env["warn_value"] = *triggerExpression.WarnValue
}
if triggerExpression.ErrorValue != nil {
env["error_value"] = *triggerExpression.ErrorValue
}
for k, v := range triggerExpression.AdditionalTargetsValues {
env[k] = v
}
if _, err := expr.Compile(
strings.ToLower(expression),
expr.Optimize(true),
expr.Env(env),
); err != nil {
return ErrInvalidExpression{err}
}
return nil
}

// Evaluate gets trigger expression and evaluates it for given parameters using govaluate
func (triggerExpression *TriggerExpression) Evaluate() (moira.State, error) {
expr, err := getExpression(triggerExpression)
Expand All @@ -95,27 +136,12 @@ func (triggerExpression *TriggerExpression) Evaluate() (moira.State, error) {
}
}

func validateUserExpression(triggerExpression *TriggerExpression, userExpression *govaluate.EvaluableExpression) (*govaluate.EvaluableExpression, error) {
for _, v := range userExpression.Vars() {
if _, err := triggerExpression.Get(v); err != nil {
return nil, fmt.Errorf("invalid variable value: %w", err)
}
}
return userExpression, nil
}

func getExpression(triggerExpression *TriggerExpression) (*govaluate.EvaluableExpression, error) {
if triggerExpression.TriggerType == moira.ExpressionTrigger {
if triggerExpression.Expression == nil || *triggerExpression.Expression == "" {
return nil, fmt.Errorf("trigger_type set to expression, but no expression provided")
}

userExpression, err := getUserExpression(*triggerExpression.Expression)
if err != nil {
return nil, err
}

return validateUserExpression(triggerExpression, userExpression)
return getUserExpression(*triggerExpression.Expression)
}
return getSimpleExpression(triggerExpression)
}
Expand Down
42 changes: 34 additions & 8 deletions expression/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"fmt"
"testing"

"github.com/moira-alert/moira"
. "github.com/smartystreets/goconvey/convey"

"github.com/moira-alert/moira"
)

type getExpressionValuesTest struct {
Expand Down Expand Up @@ -86,16 +87,41 @@ func TestExpression(t *testing.T) {
result, err = (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{"t2": 4.0}, TriggerType: moira.ExpressionTrigger, PreviousState: moira.StateNODATA}).Evaluate()
So(err, ShouldBeNil)
So(result, ShouldResemble, moira.StateNODATA)
})
}

expression = "t1 > 10 && t2 > 3 ? OK : ddd"
result, err = (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{"t2": 4.0}, TriggerType: moira.ExpressionTrigger}).Evaluate()
So(err, ShouldResemble, ErrInvalidExpression{fmt.Errorf("invalid variable value: %w", fmt.Errorf("no value with name ddd"))})
So(result, ShouldBeEmpty)
func TestValidate(t *testing.T) {
Convey("Test valid expressions", t, func() {
expression := "t1 > 10 && t2 > 3 ? OK : ERROR"
err := (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{"t2": 4.0}, TriggerType: moira.ExpressionTrigger}).Validate()
So(err, ShouldBeNil)

expression = "t1 <= 0 ? PREV_STATE : (t1 >= 20 ? ERROR : (t1 >= 10 ? WARN : OK))"
err = (&TriggerExpression{PreviousState: moira.StateNODATA, Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{}, TriggerType: moira.ExpressionTrigger}).Validate()
So(err, ShouldBeNil)

warnValue, errorValue := 60.0, 90.0
err = (&TriggerExpression{PreviousState: moira.StateNODATA, Expression: nil, WarnValue: &warnValue, ErrorValue: &errorValue, TriggerType: moira.RisingTrigger, MainTargetValue: 5}).Validate()
SoMsg("validating simple expression", err, ShouldBeNil)
})
Convey("Test bad expressions", t, func() {
err := (&TriggerExpression{Expression: nil, TriggerType: moira.ExpressionTrigger}).Validate()
So(err, ShouldResemble, ErrInvalidExpression{fmt.Errorf("trigger_type set to expression, but no expression provided")})
})
Convey("Test invalid expressions", t, func() {
expression := "t1 > 10 && t2 > 3 ? OK : ddd"
err := (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{"t2": 4.0}, TriggerType: moira.ExpressionTrigger}).Validate()
So(err, ShouldNotBeNil)
So(err.Error(), ShouldResemble, `unknown name ddd (1:26)
| t1 > 10 && t2 > 3 ? ok : ddd
| .........................^`)

expression = "t1 > 10 ? OK : (t2 < 5 ? WARN : ERROR)"
result, err = (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{}, TriggerType: moira.ExpressionTrigger}).Evaluate()
So(err, ShouldResemble, ErrInvalidExpression{fmt.Errorf("invalid variable value: %w", fmt.Errorf("no value with name t2"))})
So(result, ShouldBeEmpty)
err = (&TriggerExpression{Expression: &expression, MainTargetValue: 11.0, AdditionalTargetsValues: map[string]float64{}, TriggerType: moira.ExpressionTrigger}).Validate()
So(err, ShouldNotBeNil)
So(err.Error(), ShouldResemble, `unknown name t2 (1:17)
| t1 > 10 ? ok : (t2 < 5 ? warn : error)
| ................^`)
})
}

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ require (
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible
github.com/PagerDuty/go-pagerduty v1.5.1
github.com/ansel1/merry v1.6.2
github.com/aws/aws-sdk-go v1.44.293
github.com/antonmedv/expr v1.12.7
github.com/aws/aws-sdk-go v1.44.219
github.com/blevesearch/bleve/v2 v2.3.8
github.com/bwmarrin/discordgo v0.25.0
github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1
Expand All @@ -26,6 +27,8 @@ require (
github.com/gotokatsuya/ipare v0.0.0-20161202043954-fd52c5b6c44b
github.com/gregdel/pushover v1.1.0
github.com/karriereat/blackfriday-slack v0.1.0
github.com/mattermost/mattermost-server/v6 v6.0.0-20230405170428-2a75f997ee6c // it is last commit of 7.9.2 (https://github.com/mattermost/mattermost-server/commits/v7.9.2). Can't use v7, issue https://github.com/mattermost/mattermost-server/issues/20817
github.com/mitchellh/mapstructure v1.5.0
github.com/moira-alert/go-chart v0.0.0-20230220064910-812fb2829b9b
github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.13
github.com/patrickmn/go-cache v2.1.0+incompatible
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,11 @@ github.com/ansel1/merry/v2 v2.1.1 h1:Ax0gQh7Z/GfimoVg2EDBAU6CJIieWwVvhtBKJdkCE1M
github.com/ansel1/merry/v2 v2.1.1/go.mod h1:4p/FFyQbCgqlDbseWOVQaL5USpgkE9sr5xh4V6Ry0JU=
github.com/ansel1/vespucci/v4 v4.1.1/go.mod h1:zzdrO4IgBfgcGMbGTk/qNGL8JPslmW3nPpcBHKReFYY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antonmedv/expr v1.12.7 h1:jfV/l/+dHWAadLwAtESXNxXdfbK9bE4+FNMHYCMntwk=
github.com/antonmedv/expr v1.12.7/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.44.219 h1:YOFxTUQZvdRzgwb6XqLFRwNHxoUdKBuunITC7IFhvbc=
github.com/aws/aws-sdk-go v1.44.219/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.293 h1:oBPrQqsyMYe61Sl/xKVvQFflXjPwYH11aKi8QR3Nhts=
github.com/aws/aws-sdk-go v1.44.293/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
Expand Down Expand Up @@ -870,6 +874,7 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
github.com/mattermost/logr/v2 v2.0.18 h1:qiznuwwKckZJoGtBYc4Y9FAY97/oQwV1Pq9oO5qP5nk=
github.com/mattermost/logr/v2 v2.0.18/go.mod h1:1dm/YhTpozsqANXxo5Pi5zYLBsal2xY0pX+JZNbzYJY=
github.com/mattermost/mattermost-server/v6 v6.0.0-20230405170428-2a75f997ee6c/go.mod h1:o61MGMh7We01wGr1ydGDA5mmNpjTzaBVWUAlezsgx50=
github.com/mattermost/mattermost/server/public v0.0.9 h1:Qsktgxx5dc8xVAUHP5MbSLi6Cf82iB/83r6S9bluHto=
github.com/mattermost/mattermost/server/public v0.0.9/go.mod h1:sgXQrYzs+IJy51mB8E8OBljagk2u3YwQRoYlBH5goiw=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
Expand Down
69 changes: 66 additions & 3 deletions perfomance_tests/expression/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expression
import (
"testing"

"github.com/moira-alert/moira"
"github.com/moira-alert/moira/expression"
)

Expand All @@ -13,9 +14,13 @@ func BenchmarkDefault1Expr(b *testing.B) {
MainTargetValue: 10.0,
WarnValue: &warnValue,
ErrorValue: &errorValue,
TriggerType: moira.RisingTrigger,
}
for i := 0; i < b.N; i++ {
(expr).Evaluate() //nolint
_, err := expr.Evaluate()
if err != nil {
b.Log(err)
}
}
}

Expand All @@ -26,19 +31,77 @@ func BenchmarkDefault2Expr(b *testing.B) {
MainTargetValue: 10.0,
WarnValue: &warnValue,
ErrorValue: &errorValue,
TriggerType: moira.RisingTrigger,
}
for i := 0; i < b.N; i++ {
(expr).Evaluate() //nolint
_, err := expr.Evaluate()
if err != nil {
b.Log(err)
}
}
}

func BenchmarkCustomExpr(b *testing.B) {
expressionStr := "t1 > 10 && t2 > 3 ? ERROR : OK"
expr := &expression.TriggerExpression{
Expression: &expressionStr,
TriggerType: moira.ExpressionTrigger,
MainTargetValue: 11.0,
AdditionalTargetsValues: map[string]float64{"t2": 4.0}}
for i := 0; i < b.N; i++ {
(expr).Evaluate() //nolint
_, err := expr.Evaluate()
if err != nil {
b.Log(err)
}
}
}

func BenchmarkValidateComplex(b *testing.B) {
expressionStr := "(t1 * 2 > t2 && t2 / 2 != 0) || (t3 * t4 == t5 && t6 < t7) ? (t8 > t9 ? OK : WARN) : ERROR"
expr := &expression.TriggerExpression{
Expression: &expressionStr,
TriggerType: moira.ExpressionTrigger,
MainTargetValue: 4,
AdditionalTargetsValues: map[string]float64{
"t2": 5,
"t3": 3,
"t4": 6,
"t5": 18,
"t6": 10,
"t7": 15,
"t8": 20,
"t9": 10,
},
}
for i := 0; i < b.N; i++ {
err := expr.Validate()
if err != nil {
b.Log(err)
}
}
}

func BenchmarkEvaluateComplex(b *testing.B) {
expressionStr := "(t1 * 2 > t2 && t2 / 2 != 0) || (t3 * t4 == t5 && t6 < t7) ? (t8 > t9 ? OK : WARN) : ERROR"
expr := &expression.TriggerExpression{
Expression: &expressionStr,
TriggerType: moira.ExpressionTrigger,
MainTargetValue: 4,
AdditionalTargetsValues: map[string]float64{
"t2": 5,
"t3": 3,
"t4": 6,
"t5": 18,
"t6": 10,
"t7": 15,
"t8": 20,
"t9": 10,
},
}
for i := 0; i < b.N; i++ {
_, err := expr.Evaluate()
if err != nil {
b.Log(err)
}
}
}