diff --git a/api/controller/events.go b/api/controller/events.go index 218c8ecfe..5a45e2821 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -1,14 +1,23 @@ package controller import ( + "regexp" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. -func GetTriggerEvents(database moira.Database, triggerID string, page int64, size int64) (*dto.EventsList, *api.ErrorResponse) { - events, err := database.GetNotificationEvents(triggerID, page*size, size-1) +// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range +// (`from` and `to` params), metric (regular expression) and states. If `states` map is empty or nil then all states are accepted. +func GetTriggerEvents( + database moira.Database, + triggerID string, + page, size, from, to int64, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) (*dto.EventsList, *api.ErrorResponse) { + events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) if err != nil { return nil, api.ErrorInternalServer(err) } @@ -18,7 +27,7 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz Size: size, Page: page, Total: eventCount, - List: make([]moira.NotificationEvent, 0), + List: make([]moira.NotificationEvent, 0, len(events)), } for _, event := range events { if event != nil { @@ -28,6 +37,67 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz return eventsList, nil } +func getFilteredNotificationEvents( + database moira.Database, + triggerID string, + page, size, from, to int64, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) ([]*moira.NotificationEvent, error) { + // fetch all events + if size < 0 { + events, err := database.GetNotificationEvents(triggerID, page, size, from, to) + if err != nil { + return nil, err + } + + return filterNotificationEvents(events, metricRegexp, states), nil + } + + // fetch at most `size` events + filtered := make([]*moira.NotificationEvent, 0, size) + var count int64 + + for int64(len(filtered)) < size { + eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) + if err != nil { + return nil, err + } + + if len(eventsData) == 0 { + break + } + + filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) + count += 1 + + if int64(len(eventsData)) < size { + break + } + } + + return filtered, nil +} + +func filterNotificationEvents( + notificationEvents []*moira.NotificationEvent, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) []*moira.NotificationEvent { + filteredNotificationEvents := make([]*moira.NotificationEvent, 0) + + for _, event := range notificationEvents { + if metricRegexp.MatchString(event.Metric) { + _, ok := states[string(event.State)] + if len(states) == 0 || ok { + filteredNotificationEvents = append(filteredNotificationEvents, event) + } + } + } + + return filteredNotificationEvents +} + // DeleteAllEvents deletes all notification events. func DeleteAllEvents(database moira.Database) *api.ErrorResponse { if err := database.RemoveAllNotificationEvents(); err != nil { diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 5cf3e3f34..3113f5203 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -2,7 +2,9 @@ package controller import ( "fmt" + "regexp" "testing" + "time" "github.com/gofrs/uuid" "github.com/moira-alert/moira" @@ -13,6 +15,11 @@ import ( "go.uber.org/mock/gomock" ) +var ( + allMetrics = regexp.MustCompile(``) + allStates map[string]struct{} +) + func TestGetEvents(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -20,12 +27,25 @@ func TestGetEvents(t *testing.T) { triggerID := uuid.Must(uuid.NewV4()).String() var page int64 = 10 var size int64 = 100 + var from int64 = 0 + to := time.Now().Unix() Convey("Test has events", t, func() { var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return([]*moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). + Return([]*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + }, nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, @@ -37,9 +57,9 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(make([]*moira.NotificationEvent, 0), nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: make([]moira.NotificationEvent, 0), @@ -51,11 +71,101 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(nil, expected) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) }) + + Convey("Test filtering", t, func() { + Convey("by metric regex", func() { + page = 0 + size = 2 + Convey("with same pattern", func() { + filtered := []*moira.NotificationEvent{ + {Metric: "metric.test.event1"}, + {Metric: "a.metric.test.event2"}, + } + notFiltered := []*moira.NotificationEvent{ + {Metric: "another.mEtric.test.event"}, + {Metric: "metric.test"}, + } + firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) + + secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) + + total := int64(len(firstPortion) + len(secondPortion)) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + page = 0 + size = -1 + + Convey("by state", func() { + filtered := []*moira.NotificationEvent{ + {State: moira.StateOK}, + {State: moira.StateTEST}, + {State: moira.StateEXCEPTION}, + } + notFiltered := []*moira.NotificationEvent{ + {State: moira.StateWARN}, + {State: moira.StateNODATA}, + {State: moira.StateERROR}, + } + Convey("with empty map all allowed", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(append(filtered, notFiltered...)), + }) + }) + + Convey("with given states", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ + string(moira.StateOK): {}, + string(moira.StateEXCEPTION): {}, + string(moira.StateTEST): {}, + }) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + }) +} + +func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { + events := make([]moira.NotificationEvent, 0, len(eventPtrs)) + for _, ptr := range eventPtrs { + events = append(events, *ptr) + } + return events } func TestDeleteAllNotificationEvents(t *testing.T) { diff --git a/api/handler/constants.go b/api/handler/constants.go index 413204120..2625a8d46 100644 --- a/api/handler/constants.go +++ b/api/handler/constants.go @@ -1,5 +1,15 @@ package handler +const allMetricsPattern = ".*" + +const ( + eventDefaultPage = 0 + eventDefaultSize = -1 + eventDefaultFrom = "-3hour" + eventDefaultTo = "now" + eventDefaultMetric = allMetricsPattern +) + const ( contactEventsDefaultFrom = "-3hour" contactEventsDefaultTo = "now" diff --git a/api/handler/event.go b/api/handler/event.go index c5d8602d5..231abd50e 100644 --- a/api/handler/event.go +++ b/api/handler/event.go @@ -1,7 +1,12 @@ package handler import ( + "fmt" "net/http" + "regexp" + "time" + + "github.com/go-graphite/carbonapi/date" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -11,7 +16,13 @@ import ( ) func event(router chi.Router) { - router.With(middleware.TriggerContext, middleware.Paginate(0, 100)).Get("/{triggerId}", getEventsList) + router.With( + middleware.TriggerContext, + middleware.Paginate(eventDefaultPage, eventDefaultSize), + middleware.DateRange(eventDefaultFrom, eventDefaultTo), + middleware.MetricContext(eventDefaultMetric), + middleware.StatesContext(), + ).Get("/{triggerId}", getEventsList) router.With(middleware.AdminOnlyMiddleware()).Delete("/all", deleteAllEvents) } @@ -22,8 +33,12 @@ func event(router chi.Router) { // @tags event // @produce json // @param triggerID path string true "The ID of updated trigger" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param size query int false "Number of items to be displayed on one page" default(100) -// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param size query int false "Number of items to be displayed on one page. if size = -1 then all events returned" default(-1) +// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param from query string false "Start time of the time range" default(-3hour) +// @param to query string false "End time of the time range" default(now) +// @param metric query string false "Regular expression that will be used to filter events" default(.*) +// @param states query []string false "String of ',' separated state names. If empty then all states will be used." collectionFormat(csv) // @success 200 {object} dto.EventsList "Events fetched successfully" // @Failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @Failure 404 {object} api.ErrorNotFoundExample "Resource not found" @@ -34,7 +49,31 @@ func getEventsList(writer http.ResponseWriter, request *http.Request) { triggerID := middleware.GetTriggerID(request) size := middleware.GetSize(request) page := middleware.GetPage(request) - eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size) + fromStr := middleware.GetFromStr(request) + toStr := middleware.GetToStr(request) + + from := date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) + if from == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse from: %s", fromStr))) //nolint + return + } + + to := date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) + if to == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse to: %v", to))) //nolint + return + } + + metricStr := middleware.GetMetric(request) + metricRegexp, errCompile := regexp.Compile(metricStr) + if errCompile != nil { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse metric \"%s\": %w", metricStr, errCompile))) + return + } + + states := middleware.GetStates(request) + + eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size, from, to, metricRegexp, states) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/trigger.go b/api/handler/trigger.go index d17b09cc9..9d3cb9aff 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "regexp" "time" "github.com/go-chi/chi" @@ -179,7 +180,19 @@ func checkingTemplateFilling(request *http.Request, trigger dto.Trigger) *api.Er return nil } - eventsList, err := controller.GetTriggerEvents(database, trigger.ID, 0, 3) + const ( + page = 0 + size = 3 + from = 0 + ) + + var ( + to = time.Now().Unix() + allMetricRegexp = regexp.MustCompile(allMetricsPattern) + allStates map[string]struct{} + ) + + eventsList, err := controller.GetTriggerEvents(database, trigger.ID, page, size, from, to, allMetricRegexp, allStates) if err != nil { return err } diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index 1e32507d8..f9d96ec3c 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -420,7 +420,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -464,7 +464,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -508,7 +508,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() diff --git a/api/middleware/context.go b/api/middleware/context.go index 5dbc88e34..0d9e26f31 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/go-chi/chi" @@ -277,3 +278,56 @@ func AuthorizationContext(auth *api.Authorization) func(next http.Handler) http. }) } } + +// MetricContext is a function that gets `metric` value from query string and places it in context. If query does not have value sets given value. +func MetricContext(defaultMetric string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + metric := urlValues.Get("metric") + if metric == "" { + metric = defaultMetric + } + + ctx := context.WithValue(request.Context(), metricContextKey, metric) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + +const statesArraySeparator = "," + +// StatesContext is a function that gets `states` value from query string and places it in context. If query does not have value empty map will be used. +func StatesContext() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + states := make(map[string]struct{}, 0) + + statesStr := urlValues.Get("states") + if statesStr != "" { + statesList := strings.Split(statesStr, statesArraySeparator) + for _, state := range statesList { + if !moira.State(state).IsValid() { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("bad state in query parameter: %s", state))) + return + } + states[state] = struct{}{} + } + } + + ctx := context.WithValue(request.Context(), statesContextKey, states) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/context_test.go b/api/middleware/context_test.go index 1c4e9e7c9..6031e77ee 100644 --- a/api/middleware/context_test.go +++ b/api/middleware/context_test.go @@ -216,3 +216,92 @@ func TestTargetNameMiddleware(t *testing.T) { }) }) } + +func TestMetricProviderMiddleware(t *testing.T) { + Convey("Check metric provider", t, func() { + responseWriter := httptest.NewRecorder() + defaultMetric := ".*" + + Convey("status ok with correct query paramete", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric=test%5C.metric.*", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("status bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} + +func TestStatesProviderMiddleware(t *testing.T) { + Convey("Checking states provide", t, func() { + responseWriter := httptest.NewRecorder() + + Convey("ok with correct states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("bad request with bad states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR%2Cwarn", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index a520f01b4..80d65066d 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -39,6 +39,8 @@ var ( teamIDKey ContextKey = "teamID" teamUserIDKey ContextKey = "teamUserIDKey" authKey ContextKey = "auth" + metricContextKey ContextKey = "metric" + statesContextKey ContextKey = "states" anonymousUser = "anonymous" ) @@ -63,7 +65,7 @@ func GetTriggerID(request *http.Request) string { return request.Context().Value(triggerIDKey).(string) } -// GetLocalMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. +// GetMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. func GetMetricTTL(request *http.Request) map[moira.ClusterKey]time.Duration { return request.Context().Value(clustersMetricTTLKey).(map[moira.ClusterKey]time.Duration) } @@ -118,13 +120,13 @@ func GetToStr(request *http.Request) string { return request.Context().Value(toKey).(string) } -// SetTimeSeriesNames sets to requests context timeSeriesNames from saved trigger. +// SetTimeSeriesNames sets to request's context timeSeriesNames from saved trigger. func SetTimeSeriesNames(request *http.Request, timeSeriesNames map[string]bool) { ctx := context.WithValue(request.Context(), timeSeriesNamesKey, timeSeriesNames) *request = *request.WithContext(ctx) } -// GetTimeSeriesNames gets from requests context timeSeriesNames from saved trigger. +// GetTimeSeriesNames gets from request's context timeSeriesNames from saved trigger. func GetTimeSeriesNames(request *http.Request) map[string]bool { return request.Context().Value(timeSeriesNamesKey).(map[string]bool) } @@ -162,3 +164,13 @@ func SetContextValueForTest(ctx context.Context, key string, value interface{}) func GetAuth(request *http.Request) *api.Authorization { return request.Context().Value(authKey).(*api.Authorization) } + +// GetMetric is used to retrieve metric name. +func GetMetric(request *http.Request) string { + return request.Context().Value(metricContextKey).(string) +} + +// GetStates is used to retrieve trigger state. +func GetStates(request *http.Request) map[string]struct{} { + return request.Context().Value(statesContextKey).(map[string]struct{}) +} diff --git a/database/redis/notification_event.go b/database/redis/notification_event.go index 4a5974969..8f9502f4e 100644 --- a/database/redis/notification_event.go +++ b/database/redis/notification_event.go @@ -14,19 +14,24 @@ import ( var eventsTTL int64 = 3600 * 24 * 30 -// GetNotificationEvents gets NotificationEvents by given triggerID and interval. -func (connector *DbConnector) GetNotificationEvents(triggerID string, start int64, size int64) ([]*moira.NotificationEvent, error) { - ctx := connector.context - c := *connector.client - - eventsData, err := reply.Events(c.ZRevRange(ctx, triggerEventsKey(triggerID), start, start+size)) +// GetNotificationEvents gets NotificationEvents by given triggerID and interval. The events are also filtered by time range +// (`from`, `to` params). +func (connector *DbConnector) GetNotificationEvents(triggerID string, page, size, from, to int64) ([]*moira.NotificationEvent, error) { + ctx := connector.Context() + client := connector.Client() + + eventsData, err := reply.Events(client.ZRevRangeByScore(ctx, triggerEventsKey(triggerID), &redis.ZRangeBy{ + Min: strconv.FormatInt(from, 10), + Max: strconv.FormatInt(to, 10), + Offset: page * size, + Count: size, + })) if err != nil { if errors.Is(err, redis.Nil) { return make([]*moira.NotificationEvent, 0), nil } - return nil, fmt.Errorf("failed to get range for trigger events, triggerID: %s, error: %s", triggerID, err.Error()) + return nil, fmt.Errorf("failed to get range of trigger events, triggerID: %s, error: %w", triggerID, err) } - return eventsData, nil } @@ -85,7 +90,7 @@ func (connector *DbConnector) FetchNotificationEvent() (moira.NotificationEvent, } if err != nil { - return event, fmt.Errorf("failed to fetch event: %s", err.Error()) + return event, fmt.Errorf("failed to fetch event: %w", err) } event, _ = reply.BRPopToEvent(response) @@ -108,13 +113,13 @@ func (connector *DbConnector) RemoveAllNotificationEvents() error { c := *connector.client if _, err := c.Del(ctx, notificationEventsList).Result(); err != nil { - return fmt.Errorf("failed to remove %s: %s", notificationEventsList, err.Error()) + return fmt.Errorf("failed to remove %s: %w", notificationEventsList, err) } return nil } -var ( +const ( notificationEventsList = "moira-trigger-events" notificationEventsUIList = "moira-trigger-events-ui" ) diff --git a/database/redis/notification_event_test.go b/database/redis/notification_event_test.go index c6ee30dd2..269b9d023 100644 --- a/database/redis/notification_event_test.go +++ b/database/redis/notification_event_test.go @@ -33,7 +33,7 @@ func TestNotificationEvents(t *testing.T) { Convey("Notification events manipulation", t, func() { Convey("Test push-get-get count-fetch", func() { Convey("Should no events", func() { - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) @@ -57,7 +57,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -86,7 +86,7 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Should has event by triggerID after fetch", func() { - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -132,7 +132,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -148,7 +148,7 @@ func TestNotificationEvents(t *testing.T) { total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) - actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1) + actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -177,7 +177,7 @@ func TestNotificationEvents(t *testing.T) { Values: map[string]float64{}, }) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -223,7 +223,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -248,11 +248,58 @@ func TestNotificationEvents(t *testing.T) { total = dataBase.GetNotificationEventCount(triggerID3, now+1) So(total, ShouldEqual, 0) - actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1) + actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) }) + Convey("Test `from` and `to` params", func() { + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + }, true) + So(err, ShouldBeNil) + + Convey("returns event on exact time", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now, now) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + + Convey("not return event out of time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-2, now-1) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{}) + }) + + Convey("returns event in time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-1, now+1) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + }) + Convey("Test removing notification events", func() { Convey("Should remove all notifications", func() { err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ @@ -312,7 +359,7 @@ func TestNotificationEventErrorConnection(t *testing.T) { } Convey("Should throw error when no connection", t, func() { - actual1, err := dataBase.GetNotificationEvents("123", 0, 1) + actual1, err := dataBase.GetNotificationEvents("123", 0, 1, 0, time.Now().Unix()) So(actual1, ShouldBeNil) So(err, ShouldNotBeNil) diff --git a/interfaces.go b/interfaces.go index fc9693b6f..d16bf5f1e 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size int64) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, start, size, from, to int64) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index 43cb828ad..7356c8a1f 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -490,18 +490,18 @@ func (mr *MockDatabaseMockRecorder) GetNotificationEventCount(arg0, arg1 any) *g } // GetNotificationEvents mocks base method. -func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2 int64) ([]*moira.NotificationEvent, error) { +func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2, arg3, arg4 int64) ([]*moira.NotificationEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*moira.NotificationEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotificationEvents indicates an expected call of GetNotificationEvents. -func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2, arg3, arg4) } // GetNotifications mocks base method. diff --git a/mock/scheduler/scheduler.go b/mock/scheduler/scheduler.go index 8a146a314..3274a1760 100644 --- a/mock/scheduler/scheduler.go +++ b/mock/scheduler/scheduler.go @@ -48,7 +48,7 @@ func (m *MockScheduler) ScheduleNotification(arg0 moira.SchedulerParams, arg1 mo } // ScheduleNotification indicates an expected call of ScheduleNotification. -func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleNotification", reflect.TypeOf((*MockScheduler)(nil).ScheduleNotification), arg0, arg1) } diff --git a/state.go b/state.go index 8f00e8fb7..8c66624a1 100644 --- a/state.go +++ b/state.go @@ -61,6 +61,16 @@ func (state State) ToSelfState() string { return SelfStateOK } +// IsValid checks if valid State. +func (state State) IsValid() bool { + for _, allowedState := range eventStatesPriority { + if state == allowedState { + return true + } + } + return false +} + // ToMetricState is an auxiliary function to handle metric state properly. func (state TTLState) ToMetricState() State { if state == TTLStateDEL {