Skip to content

Commit

Permalink
feat(api): query parameters to trigger events (#1064)
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandrMatsko committed Aug 14, 2024
1 parent 8e70ae2 commit 8560887
Show file tree
Hide file tree
Showing 15 changed files with 506 additions and 47 deletions.
78 changes: 74 additions & 4 deletions api/controller/events.go
Original file line number Diff line number Diff line change
@@ -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)
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
122 changes: 116 additions & 6 deletions api/controller/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package controller

import (
"fmt"
"regexp"
"testing"
"time"

"github.com/gofrs/uuid"
"github.com/moira-alert/moira"
Expand All @@ -13,19 +15,37 @@ 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)
defer mockCtrl.Finish()
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}},
Expand All @@ -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),
Expand All @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions api/handler/constants.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package handler

const allMetricsPattern = ".*"

const (
eventDefaultPage = 0
eventDefaultSize = -1
eventDefaultFrom = "-3hour"
eventDefaultTo = "now"
eventDefaultMetric = allMetricsPattern
)

const (
contactEventsDefaultFrom = "-3hour"
contactEventsDefaultTo = "now"
Expand Down
47 changes: 43 additions & 4 deletions api/handler/event.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
}

Expand All @@ -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"
Expand All @@ -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
Expand Down
15 changes: 14 additions & 1 deletion api/handler/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"net/http"
"regexp"
"time"

"github.com/go-chi/chi"
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 8560887

Please sign in to comment.