From 1da9355c45159c8e15dab749e577c278400c26e9 Mon Sep 17 00:00:00 2001 From: Nix Date: Thu, 23 Jan 2020 14:05:34 +0500 Subject: [PATCH] feat: Add templates to trigger description #484 * Added templates to trigger description * Added presentation of trigger description in non-editing for api Closed #484 --- api/handler/trigger.go | 30 +++++++++++++++++++++++- api/middleware/context.go | 15 ++++++++++++ api/middleware/middleware.go | 6 +++++ datatypes.go | 45 ++++++++++++++++++++++++++++++++++++ datatypes_test.go | 44 +++++++++++++++++++++++++++++++++++ notifier/notifier.go | 7 ++++++ 6 files changed, 146 insertions(+), 1 deletion(-) diff --git a/api/handler/trigger.go b/api/handler/trigger.go index 5ded55727..90c7aca80 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/metric_source/local" "github.com/moira-alert/moira/metric_source/remote" @@ -20,7 +21,7 @@ import ( func trigger(router chi.Router) { router.Use(middleware.TriggerContext) router.Put("/", updateTrigger) - router.Get("/", getTrigger) + router.With(middleware.TriggerContext, middleware.Populate(false)).Get("/", getTrigger) router.Delete("/", removeTrigger) router.Get("/state", getTriggerState) router.Route("/throttling", func(router chi.Router) { @@ -35,6 +36,7 @@ func trigger(router chi.Router) { func updateTrigger(writer http.ResponseWriter, request *http.Request) { triggerID := middleware.GetTriggerID(request) trigger := &dto.Trigger{} + if err := render.Bind(request, trigger); err != nil { switch err := err.(type) { case local.ErrParseExpr, local.ErrEvalExpr, local.ErrUnknownFunction: @@ -50,9 +52,19 @@ func updateTrigger(writer http.ResponseWriter, request *http.Request) { default: render.Render(writer, request, api.ErrorInternalServer(err)) } + return } + if trigger.Desc != nil { + triggerData := moira.TriggerData{Desc: *trigger.Desc, Name: trigger.Name} + if _, err := triggerData.GetPopulatedDescription(moira.NotificationEvents{}); err != nil { + render.Render(writer, request, api.ErrorRender( + fmt.Errorf("You have an error in your Go template: %v", err))) + return + } + } + timeSeriesNames := middleware.GetTimeSeriesNames(request) response, err := controller.UpdateTrigger(database, &trigger.TriggerModel, triggerID, timeSeriesNames) if err != nil { @@ -79,11 +91,27 @@ func getTrigger(writer http.ResponseWriter, request *http.Request) { if triggerID == "testlog" { panic("Test for multi line logs") } + trigger, err := controller.GetTrigger(database, triggerID) if err != nil { render.Render(writer, request, err) return } + + if needToPopulate := middleware.GetPopulated(request); needToPopulate && trigger.Desc != nil { + triggerData := moira.TriggerData{Desc: *trigger.Desc, Name: trigger.Name} + + eventsList, err := controller.GetTriggerEvents(database, triggerID, 0, 3) + if err != nil { + render.Render(writer, request, err) + } + + desc, errPopulate := triggerData.GetPopulatedDescription(eventsList.List) + if errPopulate == nil { + *trigger.Desc = desc + } + } + if err := render.Render(writer, request, trigger); err != nil { render.Render(writer, request, api.ErrorRender(err)) } diff --git a/api/middleware/context.go b/api/middleware/context.go index 92855d7c4..95cdfa06b 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -124,6 +124,21 @@ func Paginate(defaultPage, defaultSize int64) func(next http.Handler) http.Handl } } +// Populate gets bool value populate from URI query and set it to request context. If query has not values sets given values +func Populate(defaultPopulated bool) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + populate, err := strconv.ParseBool(request.URL.Query().Get("populated")) + if err != nil { + populate = defaultPopulated + } + + ctxTemplate := context.WithValue(request.Context(), populateKey, populate) + next.ServeHTTP(writer, request.WithContext(ctxTemplate)) + }) + } +} + // DateRange gets from and to values from URI query and set it to request context. If query has not values sets given values func DateRange(defaultFrom, defaultTo string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 1682e94c3..2cd3921e7 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -19,6 +19,7 @@ var ( databaseKey ContextKey = "database" searcherKey ContextKey = "searcher" triggerIDKey ContextKey = "triggerID" + populateKey ContextKey = "populated" contactIDKey ContextKey = "contactID" tagKey ContextKey = "tag" subscriptionIDKey ContextKey = "subscriptionID" @@ -46,6 +47,11 @@ func GetTriggerID(request *http.Request) string { return request.Context().Value(triggerIDKey).(string) } +// GetPopulated get populate bool from request context, which was sets in TriggerContext middleware +func GetPopulated(request *http.Request) bool { + return request.Context().Value(populateKey).(bool) +} + // GetTag gets tag string from request context, which was sets in TagContext middleware func GetTag(request *http.Request) string { return request.Context().Value(tagKey).(string) diff --git a/datatypes.go b/datatypes.go index f45914d6e..9a9871720 100644 --- a/datatypes.go +++ b/datatypes.go @@ -3,6 +3,7 @@ package moira import ( "bytes" "fmt" + "html/template" "math" "strconv" "strings" @@ -103,6 +104,23 @@ func (event *NotificationEvent) CreateMessage(location *time.Location) string { // NotificationEvents represents slice of NotificationEvent type NotificationEvents []NotificationEvent +type templateData struct { + Trigger templateTrigger + Events []templateEvent +} + +type templateEvent struct { + Metric string + MetricElements []string + Timestamp int64 + Value *float64 + State State +} + +type templateTrigger struct { + Name string `json:"name"` +} + // TriggerData represents trigger object type TriggerData struct { ID string `json:"id"` @@ -115,6 +133,33 @@ type TriggerData struct { Tags []string `json:"__notifier_trigger_tags"` } +func (trigger TriggerData) GetPopulatedDescription(events NotificationEvents) (string, error) { + buffer := new(bytes.Buffer) + templateEvents := make([]templateEvent, 0, len(events)) + + for _, data := range events { + event := templateEvent{ + Metric: data.Metric, + MetricElements: strings.Split(data.Metric, "."), + Timestamp: data.Timestamp, + State: data.State, + Value: data.Value, + } + + templateEvents = append(templateEvents, event) + } + + triggerTemplate := template.Must(template.New("populate-description").Parse(trigger.Desc)) + if err := triggerTemplate.Execute(buffer, templateData{ + Trigger: templateTrigger{Name: trigger.Name}, + Events: templateEvents, + }); err != nil { + return trigger.Desc, err + } + + return buffer.String(), nil +} + // GetTriggerURI gets frontUri and returns triggerUrl, returns empty string on selfcheck and test notifications func (trigger TriggerData) GetTriggerURI(frontURI string) string { if trigger.ID != "" { diff --git a/datatypes_test.go b/datatypes_test.go index c1fb8812e..863ec7694 100644 --- a/datatypes_test.go +++ b/datatypes_test.go @@ -244,6 +244,50 @@ func TestTriggerData_GetTags(t *testing.T) { }) } +func TestTriggerData_TemplateDescription(t *testing.T) { + + Convey("Test templates", t, func() { + var trigger = TriggerData{Name: "TestName"} + trigger.Desc = "\n" + + "Trigger name: {{.Trigger.Name}}\n" + + "{{range $v := .Events }}\n" + + "Metric: {{$v.Metric}}\n" + + "MetricElements: {{$v.MetricElements}}\n" + + "Timestamp: {{$v.Timestamp}}\n" + + "Value: {{$v.Value}}\n" + + "State: {{$v.State}}\n" + + "{{end}}\n" + + "https://grafana.yourhost.com/some-dashboard{{ range $i, $v := .Events }}{{ if ne $i 0 }}&{{ else }}?{{ end }}var-host={{ $v.Metric }}{{ end }}\n" + + var data = NotificationEvents{{Metric: "1"}, {Metric: "2"}} + + Convey("Test nil data", func() { + + expected, err := trigger.GetPopulatedDescription(nil) + So(err, ShouldBeNil) + So(` +Trigger name: TestName + +https://grafana.yourhost.com/some-dashboard +`, ShouldResemble, expected) + }) + + Convey("Test data", func() { + expected, err := trigger.GetPopulatedDescription(data) + So(err, ShouldBeNil) + So("\nTrigger name: TestName\n\nMetric: 1\nMetricElements: [1]\nTimestamp: 0\nValue: <nil>\nState: \n\nMetric: 2\nMetricElements: [2]\nTimestamp: 0\nValue: <nil>\nState: \n\nhttps://grafana.yourhost.com/some-dashboard?var-host=1&var-host=2\n", ShouldResemble, expected) + }) + + Convey("Test description without templates", func() { + anotherText := "Another text" + trigger.Desc = anotherText + expected, err := trigger.GetPopulatedDescription(data) + So(err, ShouldBeNil) + So(anotherText, ShouldEqual, expected) + }) + }) +} + func TestScheduledNotification_GetKey(t *testing.T) { Convey("Get key", t, func() { notification := ScheduledNotification{ diff --git a/notifier/notifier.go b/notifier/notifier.go index e9e5d0e6f..57d2fdcf1 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -160,6 +160,13 @@ func (notifier *StandardNotifier) runSender(sender moira.Sender, ch chan Notific notifier.logger.Errorf(buildErr) } } + + desc, err := pkg.Trigger.GetPopulatedDescription(pkg.Events) + if err == nil { + notifier.logger.Errorf("Error populate description: %v", err) + pkg.Trigger.Desc = desc + } + err = sender.SendEvents(pkg.Events, pkg.Contact, pkg.Trigger, plot, pkg.Throttled) if err == nil { if metric, found := notifier.metrics.SendersOkMetrics.GetRegisteredMeter(pkg.Contact.Type); found {