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): query parameters to trigger events #1064

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0ea036c
feat: raw middlewares and db method refactor
AleksandrMatsko Jul 18, 2024
518cf9c
refactor: parsing and using query params
AleksandrMatsko Jul 22, 2024
d5368f4
fix: context key for states
AleksandrMatsko Jul 22, 2024
d5d75ca
fix: api to work
AleksandrMatsko Jul 23, 2024
fc2f714
fix: broken tests
AleksandrMatsko Jul 23, 2024
57c14f2
test: for middleware
AleksandrMatsko Jul 23, 2024
75dad93
refactor: namings, move values to vars
AleksandrMatsko Jul 24, 2024
73d2761
refactor: IsValid for State
AleksandrMatsko Jul 24, 2024
dfd63eb
refactor: move filtering to controller
AleksandrMatsko Jul 24, 2024
cb4867c
test: try to fix controller test
AleksandrMatsko Jul 24, 2024
8aacada
test: for controller
AleksandrMatsko Jul 24, 2024
6ada76d
refactor: use ContextKey for keys in middleware
AleksandrMatsko Jul 24, 2024
0805e66
refactor: namings, docs, variables usage
AleksandrMatsko Jul 25, 2024
104320d
fix: remove unnecessary `continue`, comments
AleksandrMatsko Jul 29, 2024
35af5cf
refactor: change swagger comments
AleksandrMatsko Jul 30, 2024
7e6893f
merge 'master' into feat/query-params-to-event-history
AleksandrMatsko Jul 31, 2024
8d68c29
merge master into feat/query-params-to-event-history
AleksandrMatsko Aug 6, 2024
67067a3
refactor: rename middlewares
AleksandrMatsko Aug 8, 2024
01f1114
refactor: try to fix api spec
AleksandrMatsko Aug 8, 2024
dc5226d
style: add new lines between code blocks
AleksandrMatsko Aug 8, 2024
fa0b578
Merge branch 'master' into feat/query-params-to-event-history
AleksandrMatsko Aug 9, 2024
4beb56c
fix: close `)` for `const`
AleksandrMatsko Aug 9, 2024
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
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
kissken marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 (
Tetrergeru marked this conversation as resolved.
Show resolved Hide resolved
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
Loading