From 5f46b5331de91e37d137676773cd7121764059cb Mon Sep 17 00:00:00 2001 From: Yijie Qin Date: Thu, 19 Oct 2023 21:43:42 -0400 Subject: [PATCH] add the paginated version of list alerts api --- api/api.go | 2 +- api/v2/api.go | 231 ++++++-- api/v2/api_test.go | 346 ++++++++++-- api/v2/client/alertinfo/alertinfo_client.go | 93 ++++ .../alertinfo/get_alert_infos_parameters.go | 500 ++++++++++++++++++ .../alertinfo/get_alert_infos_responses.go | 246 +++++++++ api/v2/client/alertmanager_api_client.go | 5 + api/v2/compat.go | 32 ++ api/v2/models/gettable_alert_infos.go | 133 +++++ api/v2/openapi.yaml | 77 +++ api/v2/restapi/configure_alertmanager.go | 6 + api/v2/restapi/embedded_spec.go | 216 ++++++++ .../operations/alertinfo/get_alert_infos.go | 70 +++ .../alertinfo/get_alert_infos_parameters.go | 369 +++++++++++++ .../alertinfo/get_alert_infos_responses.go | 159 ++++++ .../alertinfo/get_alert_infos_urlbuilder.go | 203 +++++++ api/v2/restapi/operations/alertmanager_api.go | 13 + api/v2/testing.go | 27 + cmd/alertmanager/main.go | 4 +- dispatch/dispatch.go | 5 +- dispatch/dispatch_test.go | 7 +- 21 files changed, 2657 insertions(+), 87 deletions(-) create mode 100644 api/v2/client/alertinfo/alertinfo_client.go create mode 100644 api/v2/client/alertinfo/get_alert_infos_parameters.go create mode 100644 api/v2/client/alertinfo/get_alert_infos_responses.go create mode 100644 api/v2/models/gettable_alert_infos.go create mode 100644 api/v2/restapi/operations/alertinfo/get_alert_infos.go create mode 100644 api/v2/restapi/operations/alertinfo/get_alert_infos_parameters.go create mode 100644 api/v2/restapi/operations/alertinfo/get_alert_infos_responses.go create mode 100644 api/v2/restapi/operations/alertinfo/get_alert_infos_urlbuilder.go diff --git a/api/api.go b/api/api.go index cb444b7882..b48a38c324 100644 --- a/api/api.go +++ b/api/api.go @@ -75,7 +75,7 @@ type Options struct { // GroupFunc returns a list of alert groups. The alerts are grouped // according to the current active configuration. Alerts returned are // filtered by the arguments provided to the function. - GroupFunc func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) + GroupFunc func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool, func(string) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) // GroupInfoFunc returns a list of alert groups information. The alerts are grouped // according to the current active configuration. This function will not return the alerts inside each group. GroupInfoFunc func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos diff --git a/api/v2/api.go b/api/v2/api.go index 441502b5d1..3deda8acec 100644 --- a/api/v2/api.go +++ b/api/v2/api.go @@ -14,6 +14,7 @@ package v2 import ( + "context" "errors" "fmt" "net/http" @@ -42,6 +43,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/restapi/operations" alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert" alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup" + alertinfo_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertinfo" general_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/general" receiver_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver" silence_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/silence" @@ -82,7 +84,7 @@ type API struct { } type ( - groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string) + groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool, func(string) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string) groupInfosFn func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos getAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus setAlertStatusFn func(prometheus_model.LabelSet) @@ -135,6 +137,7 @@ func NewAPI( } openAPI.AlertGetAlertsHandler = alert_ops.GetAlertsHandlerFunc(api.getAlertsHandler) + openAPI.AlertinfoGetAlertInfosHandler = alertinfo_ops.GetAlertInfosHandlerFunc(api.getAlertInfosHandler) openAPI.AlertPostAlertsHandler = alert_ops.PostAlertsHandlerFunc(api.postAlertsHandler) openAPI.AlertgroupGetAlertGroupsHandler = alertgroup_ops.GetAlertGroupsHandlerFunc(api.getAlertGroupsHandler) openAPI.AlertgroupinfolistGetAlertGroupInfoListHandler = alertgroupinfolist_ops.GetAlertGroupInfoListHandlerFunc(api.getAlertGroupInfoListHandler) @@ -249,10 +252,7 @@ func (api *API) getReceiversHandler(params receiver_ops.GetReceiversParams) midd func (api *API) getAlertsHandler(params alert_ops.GetAlertsParams) middleware.Responder { var ( receiverFilter *regexp.Regexp - // Initialize result slice to prevent api returning `null` when there - // are no alerts present - res = open_api_models.GettableAlerts{} - ctx = params.HTTPRequest.Context() + ctx = params.HTTPRequest.Context() logger = api.requestLogger(params.HTTPRequest) ) @@ -275,56 +275,87 @@ func (api *API) getAlertsHandler(params alert_ops.GetAlertsParams) middleware.Re } } - alerts := api.alerts.GetPending() - defer alerts.Close() - alertFilter := api.alertFilter(matchers, *params.Silenced, *params.Inhibited, *params.Active) - now := time.Now() + alerts, err := api.getAlerts(ctx, receiverFilter, alertFilter) + if err != nil { + level.Error(logger).Log("msg", "Failed to get alerts", "err", err) + return alert_ops.NewGetAlertsInternalServerError().WithPayload(err.Error()) + } - api.mtx.RLock() - for a := range alerts.Next() { - if err = alerts.Err(); err != nil { - break - } - if err = ctx.Err(); err != nil { - break - } + callbackRes, err := api.apiCallback.V2GetAlertsCallback(alerts) + if err != nil { + level.Error(logger).Log("msg", "Failed to call api callback", "err", err) + return alert_ops.NewGetAlertsInternalServerError().WithPayload(err.Error()) + } - routes := api.route.Match(a.Labels) - receivers := make([]string, 0, len(routes)) - for _, r := range routes { - receivers = append(receivers, r.RouteOpts.Receiver) - } + return alert_ops.NewGetAlertsOK().WithPayload(callbackRes) +} - if receiverFilter != nil && !receiversMatchFilter(receivers, receiverFilter) { - continue - } +func (api *API) getAlertInfosHandler(params alertinfo_ops.GetAlertInfosParams) middleware.Responder { + var ( + alerts open_api_models.GettableAlerts + receiverFilter *regexp.Regexp + ctx = params.HTTPRequest.Context() - if !alertFilter(a, now) { - continue + logger = api.requestLogger(params.HTTPRequest) + ) + + matchers, err := parseFilter(params.Filter) + if err != nil { + level.Debug(logger).Log("msg", "Failed to parse matchers", "err", err) + return alertinfo_ops.NewGetAlertInfosBadRequest().WithPayload(err.Error()) + } + + if params.Receiver != nil { + receiverFilter, err = regexp.Compile("^(?:" + *params.Receiver + ")$") + if err != nil { + level.Debug(logger).Log("msg", "Failed to compile receiver regex", "err", err) + return alertinfo_ops. + NewGetAlertInfosBadRequest(). + WithPayload( + fmt.Sprintf("failed to parse receiver param: %v", err.Error()), + ) } + } - alert := AlertToOpenAPIAlert(a, api.getAlertStatus(a.Fingerprint()), receivers) + if err = validateMaxResult(params.MaxResults); err != nil { + level.Error(logger).Log("msg", "Failed to parse MaxResults parameter", "err", err) + return alertinfo_ops. + NewGetAlertInfosBadRequest(). + WithPayload( + fmt.Sprintf("failed to parse MaxResults param: %v", *params.MaxResults), + ) + } - res = append(res, alert) + if err = validateAlertInfoNextToken(params.NextToken); err != nil { + level.Error(logger).Log("msg", "Failed to parse NextToken parameter", "err", err) + return alertinfo_ops. + NewGetAlertInfosBadRequest(). + WithPayload( + fmt.Sprintf("failed to parse NextToken param: %v", *params.NextToken), + ) } - api.mtx.RUnlock() + alertFilter := api.alertFilter(matchers, *params.Silenced, *params.Inhibited, *params.Active) + groupIdsFilter := api.groupIDFilter(params.GroupID) + if len(params.GroupID) > 0 { + alerts, err = api.getAlertsFromAlertGroup(ctx, receiverFilter, alertFilter, groupIdsFilter) + } else { + alerts, err = api.getAlerts(ctx, receiverFilter, alertFilter) + } if err != nil { level.Error(logger).Log("msg", "Failed to get alerts", "err", err) - return alert_ops.NewGetAlertsInternalServerError().WithPayload(err.Error()) + return alertinfo_ops.NewGetAlertInfosInternalServerError().WithPayload(err.Error()) } - sort.Slice(res, func(i, j int) bool { - return *res[i].Fingerprint < *res[j].Fingerprint - }) - callbackRes, err := api.apiCallback.V2GetAlertsCallback(res) - if err != nil { - level.Error(logger).Log("msg", "Failed to call api callback", "err", err) - return alert_ops.NewGetAlertsInternalServerError().WithPayload(err.Error()) + returnAlertInfos, nextItem := AlertInfosTruncate(alerts, params.MaxResults, params.NextToken) + + response := &open_api_models.GettableAlertInfos{ + Alerts: returnAlertInfos, + NextToken: nextItem, } - return alert_ops.NewGetAlertsOK().WithPayload(callbackRes) + return alertinfo_ops.NewGetAlertInfosOK().WithPayload(response) } func (api *API) postAlertsHandler(params alert_ops.PostAlertsParams) middleware.Responder { @@ -411,18 +442,10 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams } } - rf := func(receiverFilter *regexp.Regexp) func(r *dispatch.Route) bool { - return func(r *dispatch.Route) bool { - receiver := r.RouteOpts.Receiver - if receiverFilter != nil && !receiverFilter.MatchString(receiver) { - return false - } - return true - } - }(receiverFilter) - + rf := api.routeFilter(receiverFilter) af := api.alertFilter(matchers, *params.Silenced, *params.Inhibited, *params.Active) - alertGroups, allReceivers := api.alertGroups(rf, af) + gf := api.groupIDFilter([]string{}) + alertGroups, allReceivers := api.alertGroups(rf, af, gf) res := make(open_api_models.AlertGroups, 0, len(alertGroups)) @@ -524,6 +547,32 @@ func (api *API) getAlertGroupInfoListHandler(params alertgroupinfolist_ops.GetAl return alertgroupinfolist_ops.NewGetAlertGroupInfoListOK().WithPayload(response) } +func (api *API) groupIDFilter(groupIDsFilter []string) func(groupId string) bool { + return func(groupId string) bool { + if len(groupIDsFilter) <= 0 { + return true + } + for _, groupIDFilter := range groupIDsFilter { + if groupIDFilter == groupId { + return true + } + } + return false + } +} + +func (api *API) routeFilter(receiverFilter *regexp.Regexp) func(r *dispatch.Route) bool { + return func(receiverFilter *regexp.Regexp) func(r *dispatch.Route) bool { + return func(r *dispatch.Route) bool { + receiver := r.RouteOpts.Receiver + if receiverFilter != nil && !receiverFilter.MatchString(receiver) { + return false + } + return true + } + }(receiverFilter) +} + func (api *API) alertFilter(matchers []*labels.Matcher, silenced, inhibited, active bool) func(a *types.Alert, now time.Time) bool { return func(a *types.Alert, now time.Time) bool { if !a.EndsAt.IsZero() && a.EndsAt.Before(now) { @@ -815,6 +864,78 @@ func getSwaggerSpec() (*loads.Document, *analysis.Spec, error) { return swaggerSpec, swaggerSpecAnalysisCache, nil } +func (api *API) getAlertsFromAlertGroup(ctx context.Context, receiverFilter *regexp.Regexp, alertFilter func(a *types.Alert, now time.Time) bool, groupIdsFilter func(groupId string) bool) (open_api_models.GettableAlerts, error) { + res := open_api_models.GettableAlerts{} + routeFilter := api.routeFilter(receiverFilter) + alertGroups, allReceivers := api.alertGroups(routeFilter, alertFilter, groupIdsFilter) + for _, alertGroup := range alertGroups { + for _, alert := range alertGroup.Alerts { + if err := ctx.Err(); err != nil { + break + } + fp := alert.Fingerprint() + receivers := allReceivers[fp] + status := api.getAlertStatus(fp) + apiAlert := AlertToOpenAPIAlert(alert, status, receivers) + res = append(res, apiAlert) + } + } + + sort.Slice(res, func(i, j int) bool { + return *res[i].Fingerprint < *res[j].Fingerprint + }) + + return res, nil +} + +func (api *API) getAlerts(ctx context.Context, receiverFilter *regexp.Regexp, alertFilter func(a *types.Alert, now time.Time) bool) (open_api_models.GettableAlerts, error) { + var err error + res := open_api_models.GettableAlerts{} + + alerts := api.alerts.GetPending() + defer alerts.Close() + + now := time.Now() + + api.mtx.RLock() + for a := range alerts.Next() { + if err = alerts.Err(); err != nil { + break + } + if err = ctx.Err(); err != nil { + break + } + + routes := api.route.Match(a.Labels) + receivers := make([]string, 0, len(routes)) + for _, r := range routes { + receivers = append(receivers, r.RouteOpts.Receiver) + } + + if receiverFilter != nil && !receiversMatchFilter(receivers, receiverFilter) { + continue + } + + if !alertFilter(a, now) { + continue + } + + alert := AlertToOpenAPIAlert(a, api.getAlertStatus(a.Fingerprint()), receivers) + + res = append(res, alert) + } + api.mtx.RUnlock() + + if err != nil { + return res, err + } + sort.Slice(res, func(i, j int) bool { + return *res[i].Fingerprint < *res[j].Fingerprint + }) + + return res, nil +} + func validateMaxResult(maxItem *int64) error { if maxItem != nil { if *maxItem < 0 { @@ -824,6 +945,16 @@ func validateMaxResult(maxItem *int64) error { return nil } +func validateAlertInfoNextToken(nextToken *string) error { + if nextToken != nil { + _, err := prometheus_model.ParseFingerprint(*nextToken) + if err != nil { + return err + } + } + return nil +} + func validateNextToken(nextToken *string) error { if nextToken != nil { match, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", *nextToken) diff --git a/api/v2/api_test.go b/api/v2/api_test.go index ba91cbcf8d..794a1219c1 100644 --- a/api/v2/api_test.go +++ b/api/v2/api_test.go @@ -32,6 +32,7 @@ import ( alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert" alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup" alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist" + alert_info_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertinfo" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/util/callback" @@ -127,14 +128,6 @@ func gettableSilence(id, state string, } } -func convertIntToPointerInt64(x int64) *int64 { - return &x -} - -func convertStringToPointer(x string) *string { - return &x -} - func TestGetAlertGroupInfosHandler(t *testing.T) { aginfos := dispatch.AlertGroupInfos{ &dispatch.AlertGroupInfo{ @@ -655,66 +648,106 @@ func TestListAlertsHandler(t *testing.T) { alerts := []*types.Alert{ { Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1"}, + Labels: model.LabelSet{"state": "active", "alertname": "alert1"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert2"}, + Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert3"}, + Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert4"}, + Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"}, StartsAt: now.Add(-time.Minute), }, }, { Alert: model.Alert{ Labels: model.LabelSet{"alertname": "alert5"}, - StartsAt: now.Add(-time.Minute), + StartsAt: now.Add(-2 * time.Minute), + EndsAt: now.Add(-time.Minute), }, }, } for _, tc := range []struct { - name string - expectedCode int - anames []string - callback callback.Callback + name string + booleanParams map[string]*bool + expectedCode int + anames []string + callback callback.Callback }{ { - "no call back", + "no call back, no filter", + map[string]*bool{}, 200, - []string{"alert3", "alert2", "alert1", "alert5", "alert4"}, + []string{"alert1", "alert2", "alert3", "alert4"}, callback.NoopAPICallback{}, }, { - "callback: only return 1 alert", + "callback: only return 1 alert, no filter", + map[string]*bool{}, 200, - []string{"alert3"}, + []string{"alert1"}, limitNumberOfAlertsReturnedCallback{limit: 1}, }, { - "callback: only return 3 alert", + "callback: only return 3 alert, no filter", + map[string]*bool{}, 200, - []string{"alert3", "alert2", "alert1"}, + []string{"alert1", "alert2", "alert3"}, limitNumberOfAlertsReturnedCallback{limit: 3}, }, + { + "no filter", + map[string]*bool{}, + 200, + []string{"alert1", "alert2", "alert3", "alert4"}, + callback.NoopAPICallback{}, + }, + { + "status filter", + map[string]*bool{"active": BoolPointer(true), "silenced": BoolPointer(true), "inhibited": BoolPointer(true)}, + 200, + []string{"alert1", "alert2", "alert3", "alert4"}, + callback.NoopAPICallback{}, + }, + { + "status filter - active false", + map[string]*bool{"active": BoolPointer(false), "silenced": BoolPointer(true), "inhibited": BoolPointer(true)}, + 200, + []string{"alert2", "alert3", "alert4"}, + callback.NoopAPICallback{}, + }, + { + "status filter - silenced false", + map[string]*bool{"active": BoolPointer(true), "silenced": BoolPointer(false), "inhibited": BoolPointer(true)}, + 200, + []string{"alert1", "alert2", "alert4"}, + callback.NoopAPICallback{}, + }, + { + "status filter - inhibited false", + map[string]*bool{"active": BoolPointer(true), "unprocessed": BoolPointer(true), "silenced": BoolPointer(true), "inhibited": BoolPointer(false)}, + 200, + []string{"alert1", "alert2", "alert3"}, + callback.NoopAPICallback{}, + }, } { t.Run(tc.name, func(t *testing.T) { alertsProvider := newFakeAlerts(alerts) api := API{ uptime: time.Now(), - getAlertStatus: getAlertStatus, + getAlertStatus: newGetAlertStatus(alertsProvider), logger: log.NewNopLogger(), apiCallback: tc.callback, alerts: alertsProvider, @@ -726,14 +759,23 @@ func TestListAlertsHandler(t *testing.T) { w := httptest.NewRecorder() p := runtime.TextProducer() - silence := false - inhibited := false - active := true + silence := tc.booleanParams["silenced"] + if silence == nil { + silence = BoolPointer(true) + } + inhibited := tc.booleanParams["inhibited"] + if inhibited == nil { + inhibited = BoolPointer(true) + } + active := tc.booleanParams["active"] + if active == nil { + active = BoolPointer(true) + } responder := api.getAlertsHandler(alert_ops.GetAlertsParams{ HTTPRequest: r, - Silenced: &silence, - Inhibited: &inhibited, - Active: &active, + Silenced: silence, + Inhibited: inhibited, + Active: active, }) responder.WriteResponse(w, p) body, _ := io.ReadAll(w.Result().Body) @@ -816,7 +858,7 @@ func TestGetAlertGroupsHandler(t *testing.T) { t.Run(tc.name, func(t *testing.T) { api := API{ uptime: time.Now(), - alertGroups: func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) { + alertGroups: func(f func(*dispatch.Route) bool, f2 func(*types.Alert, time.Time) bool, f3 func(string) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) { return aginfos, nil }, getAlertStatus: getAlertStatus, @@ -851,6 +893,236 @@ func TestGetAlertGroupsHandler(t *testing.T) { } } +func TestListAlertInfosHandler(t *testing.T) { + now := time.Now() + alerts := []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"state": "active", "alertname": "alert1"}, + StartsAt: now.Add(-time.Minute), + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"}, + StartsAt: now.Add(-time.Minute), + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"}, + StartsAt: now.Add(-time.Minute), + }, + }, + { + Alert: model.Alert{ + Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"}, + StartsAt: now.Add(-time.Minute), + }, + }, + } + ags := dispatch.AlertGroups{ + &dispatch.AlertGroup{ + Labels: model.LabelSet{ + "alertname": "TestingAlert", + "service": "api", + }, + Receiver: "testing", + Alerts: []*types.Alert{alerts[0], alerts[1]}, + }, + &dispatch.AlertGroup{ + Labels: model.LabelSet{ + "alertname": "HighErrorRate", + "service": "api", + "cluster": "bb", + }, + Receiver: "prod", + Alerts: []*types.Alert{alerts[2], alerts[3]}, + }, + } + + for _, tc := range []struct { + name string + booleanParams map[string]*bool + groupsFilter []string + expectedCode int + maxResult *int64 + nextToken *string + anames []string + expectNextToken string + }{ + { + "no filter", + map[string]*bool{}, + []string{}, + 200, + nil, + nil, + []string{"alert1", "alert2", "alert3", "alert4"}, + "", + }, + { + "status filter", + map[string]*bool{"active": BoolPointer(true), "silenced": BoolPointer(true), "inhibited": BoolPointer(true)}, + []string{}, + 200, + nil, + nil, + []string{"alert1", "alert2", "alert3", "alert4"}, + "", + }, + { + "status filter - active false", + map[string]*bool{"active": BoolPointer(false), "silenced": BoolPointer(true), "inhibited": BoolPointer(true)}, + []string{}, + 200, + nil, + nil, + []string{"alert2", "alert3", "alert4"}, + "", + }, + { + "status filter - silenced false", + map[string]*bool{"active": BoolPointer(true), "silenced": BoolPointer(false), "inhibited": BoolPointer(true)}, + []string{}, + 200, + nil, + nil, + []string{"alert1", "alert2", "alert4"}, + "", + }, + { + "status filter - inhibited false", + map[string]*bool{"active": BoolPointer(true), "unprocessed": BoolPointer(true), "silenced": BoolPointer(true), "inhibited": BoolPointer(false)}, + []string{}, + 200, + nil, + nil, + []string{"alert1", "alert2", "alert3"}, + "", + }, + { + "group filter", + map[string]*bool{"active": BoolPointer(true), "unprocessed": BoolPointer(true), "silenced": BoolPointer(true), "inhibited": BoolPointer(true)}, + []string{"123"}, + 200, + nil, + nil, + []string{"alert1", "alert2", "alert3", "alert4"}, + "", + }, + { + "MaxResults - only 1 alert return", + map[string]*bool{}, + []string{}, + 200, + convertIntToPointerInt64(int64(1)), + nil, + []string{"alert1"}, + alerts[0].Fingerprint().String(), + }, + { + "MaxResults - 0 alert return", + map[string]*bool{}, + []string{}, + 200, + convertIntToPointerInt64(int64(0)), + nil, + []string{}, + "", + }, + { + "MaxResults - all alert return", + map[string]*bool{}, + []string{}, + 200, + convertIntToPointerInt64(int64(8)), + nil, + []string{"alert1", "alert2", "alert3", "alert4"}, + "", + }, + { + "MaxResults - has begin next token, max 2 alerts", + map[string]*bool{}, + []string{}, + 200, + convertIntToPointerInt64(int64(2)), + convertStringToPointer(alerts[0].Fingerprint().String()), + []string{"alert2", "alert3"}, + alerts[2].Fingerprint().String(), + }, + { + "MaxResults - has begin next token, max 2 alerts, no response next token", + map[string]*bool{}, + []string{}, + 200, + convertIntToPointerInt64(int64(2)), + convertStringToPointer(alerts[1].Fingerprint().String()), + []string{"alert3", "alert4"}, + "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + alertsProvider := newFakeAlerts(alerts) + api := API{ + uptime: time.Now(), + getAlertStatus: newGetAlertStatus(alertsProvider), + logger: log.NewNopLogger(), + alerts: alertsProvider, + alertGroups: func(f func(*dispatch.Route) bool, f2 func(*types.Alert, time.Time) bool, f3 func(string) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) { + return ags, nil + }, + setAlertStatus: func(model.LabelSet) {}, + } + api.route = dispatch.NewRoute(&config.Route{Receiver: "def-receiver"}, nil) + r, err := http.NewRequest("GET", "/api/v2/alertinfos", nil) + require.NoError(t, err) + + w := httptest.NewRecorder() + p := runtime.TextProducer() + silence := tc.booleanParams["silenced"] + if silence == nil { + silence = BoolPointer(true) + } + inhibited := tc.booleanParams["inhibited"] + if inhibited == nil { + inhibited = BoolPointer(true) + } + active := tc.booleanParams["active"] + if active == nil { + active = BoolPointer(true) + } + responder := api.getAlertInfosHandler(alert_info_ops.GetAlertInfosParams{ + HTTPRequest: r, + Silenced: silence, + Inhibited: inhibited, + Active: active, + GroupID: tc.groupsFilter, + MaxResults: tc.maxResult, + NextToken: tc.nextToken, + }) + responder.WriteResponse(w, p) + body, _ := io.ReadAll(w.Result().Body) + + require.Equal(t, tc.expectedCode, w.Code) + response := open_api_models.GettableAlertInfos{} + err = json.Unmarshal(body, &response) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + anames := []string{} + for _, a := range response.Alerts { + name, ok := a.Labels["alertname"] + if ok { + anames = append(anames, string(name)) + } + } + require.Equal(t, tc.anames, anames) + require.Equal(t, tc.expectNextToken, response.NextToken) + }) + } +} + type limitNumberOfAlertsReturnedCallback struct { limit int } @@ -868,3 +1140,15 @@ func getAlertStatus(model.Fingerprint) types.AlertStatus { status.State = types.AlertStateActive return status } + +func BoolPointer(b bool) *bool { + return &b +} + +func convertIntToPointerInt64(x int64) *int64 { + return &x +} + +func convertStringToPointer(x string) *string { + return &x +} diff --git a/api/v2/client/alertinfo/alertinfo_client.go b/api/v2/client/alertinfo/alertinfo_client.go new file mode 100644 index 0000000000..851e0ff5c0 --- /dev/null +++ b/api/v2/client/alertinfo/alertinfo_client.go @@ -0,0 +1,93 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +// New creates a new alertinfo API client. +func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService { + return &Client{transport: transport, formats: formats} +} + +/* +Client for alertinfo API +*/ +type Client struct { + transport runtime.ClientTransport + formats strfmt.Registry +} + +// ClientOption is the option for Client methods +type ClientOption func(*runtime.ClientOperation) + +// ClientService is the interface for Client methods +type ClientService interface { + GetAlertInfos(params *GetAlertInfosParams, opts ...ClientOption) (*GetAlertInfosOK, error) + + SetTransport(transport runtime.ClientTransport) +} + +/* +GetAlertInfos Get a list of alert infos +*/ +func (a *Client) GetAlertInfos(params *GetAlertInfosParams, opts ...ClientOption) (*GetAlertInfosOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetAlertInfosParams() + } + op := &runtime.ClientOperation{ + ID: "getAlertInfos", + Method: "GET", + PathPattern: "/alertinfos", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetAlertInfosReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*GetAlertInfosOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for getAlertInfos: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + +// SetTransport changes the transport on the client +func (a *Client) SetTransport(transport runtime.ClientTransport) { + a.transport = transport +} diff --git a/api/v2/client/alertinfo/get_alert_infos_parameters.go b/api/v2/client/alertinfo/get_alert_infos_parameters.go new file mode 100644 index 0000000000..3f1ff567bf --- /dev/null +++ b/api/v2/client/alertinfo/get_alert_infos_parameters.go @@ -0,0 +1,500 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewGetAlertInfosParams creates a new GetAlertInfosParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetAlertInfosParams() *GetAlertInfosParams { + return &GetAlertInfosParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetAlertInfosParamsWithTimeout creates a new GetAlertInfosParams object +// with the ability to set a timeout on a request. +func NewGetAlertInfosParamsWithTimeout(timeout time.Duration) *GetAlertInfosParams { + return &GetAlertInfosParams{ + timeout: timeout, + } +} + +// NewGetAlertInfosParamsWithContext creates a new GetAlertInfosParams object +// with the ability to set a context for a request. +func NewGetAlertInfosParamsWithContext(ctx context.Context) *GetAlertInfosParams { + return &GetAlertInfosParams{ + Context: ctx, + } +} + +// NewGetAlertInfosParamsWithHTTPClient creates a new GetAlertInfosParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetAlertInfosParamsWithHTTPClient(client *http.Client) *GetAlertInfosParams { + return &GetAlertInfosParams{ + HTTPClient: client, + } +} + +/* +GetAlertInfosParams contains all the parameters to send to the API endpoint + + for the get alert infos operation. + + Typically these are written to a http.Request. +*/ +type GetAlertInfosParams struct { + + /* Active. + + Show active alerts + + Default: true + */ + Active *bool + + /* Filter. + + A list of matchers to filter alerts by + */ + Filter []string + + /* GroupID. + + A list of group IDs to filter alerts by + */ + GroupID []string + + /* Inhibited. + + Show inhibited alerts + + Default: true + */ + Inhibited *bool + + /* MaxResults. + + The maximum number of alert to return in one getAlertInfos operation. + */ + MaxResults *int64 + + /* NextToken. + + The token for the next set of items to return + */ + NextToken *string + + /* Receiver. + + A regex matching receivers to filter alerts by + */ + Receiver *string + + /* Silenced. + + Show silenced alerts + + Default: true + */ + Silenced *bool + + /* Unprocessed. + + Show unprocessed alerts + + Default: true + */ + Unprocessed *bool + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get alert infos params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetAlertInfosParams) WithDefaults() *GetAlertInfosParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get alert infos params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetAlertInfosParams) SetDefaults() { + var ( + activeDefault = bool(true) + + inhibitedDefault = bool(true) + + silencedDefault = bool(true) + + unprocessedDefault = bool(true) + ) + + val := GetAlertInfosParams{ + Active: &activeDefault, + Inhibited: &inhibitedDefault, + Silenced: &silencedDefault, + Unprocessed: &unprocessedDefault, + } + + val.timeout = o.timeout + val.Context = o.Context + val.HTTPClient = o.HTTPClient + *o = val +} + +// WithTimeout adds the timeout to the get alert infos params +func (o *GetAlertInfosParams) WithTimeout(timeout time.Duration) *GetAlertInfosParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get alert infos params +func (o *GetAlertInfosParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get alert infos params +func (o *GetAlertInfosParams) WithContext(ctx context.Context) *GetAlertInfosParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get alert infos params +func (o *GetAlertInfosParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get alert infos params +func (o *GetAlertInfosParams) WithHTTPClient(client *http.Client) *GetAlertInfosParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get alert infos params +func (o *GetAlertInfosParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithActive adds the active to the get alert infos params +func (o *GetAlertInfosParams) WithActive(active *bool) *GetAlertInfosParams { + o.SetActive(active) + return o +} + +// SetActive adds the active to the get alert infos params +func (o *GetAlertInfosParams) SetActive(active *bool) { + o.Active = active +} + +// WithFilter adds the filter to the get alert infos params +func (o *GetAlertInfosParams) WithFilter(filter []string) *GetAlertInfosParams { + o.SetFilter(filter) + return o +} + +// SetFilter adds the filter to the get alert infos params +func (o *GetAlertInfosParams) SetFilter(filter []string) { + o.Filter = filter +} + +// WithGroupID adds the groupID to the get alert infos params +func (o *GetAlertInfosParams) WithGroupID(groupID []string) *GetAlertInfosParams { + o.SetGroupID(groupID) + return o +} + +// SetGroupID adds the groupId to the get alert infos params +func (o *GetAlertInfosParams) SetGroupID(groupID []string) { + o.GroupID = groupID +} + +// WithInhibited adds the inhibited to the get alert infos params +func (o *GetAlertInfosParams) WithInhibited(inhibited *bool) *GetAlertInfosParams { + o.SetInhibited(inhibited) + return o +} + +// SetInhibited adds the inhibited to the get alert infos params +func (o *GetAlertInfosParams) SetInhibited(inhibited *bool) { + o.Inhibited = inhibited +} + +// WithMaxResults adds the maxResults to the get alert infos params +func (o *GetAlertInfosParams) WithMaxResults(maxResults *int64) *GetAlertInfosParams { + o.SetMaxResults(maxResults) + return o +} + +// SetMaxResults adds the maxResults to the get alert infos params +func (o *GetAlertInfosParams) SetMaxResults(maxResults *int64) { + o.MaxResults = maxResults +} + +// WithNextToken adds the nextToken to the get alert infos params +func (o *GetAlertInfosParams) WithNextToken(nextToken *string) *GetAlertInfosParams { + o.SetNextToken(nextToken) + return o +} + +// SetNextToken adds the nextToken to the get alert infos params +func (o *GetAlertInfosParams) SetNextToken(nextToken *string) { + o.NextToken = nextToken +} + +// WithReceiver adds the receiver to the get alert infos params +func (o *GetAlertInfosParams) WithReceiver(receiver *string) *GetAlertInfosParams { + o.SetReceiver(receiver) + return o +} + +// SetReceiver adds the receiver to the get alert infos params +func (o *GetAlertInfosParams) SetReceiver(receiver *string) { + o.Receiver = receiver +} + +// WithSilenced adds the silenced to the get alert infos params +func (o *GetAlertInfosParams) WithSilenced(silenced *bool) *GetAlertInfosParams { + o.SetSilenced(silenced) + return o +} + +// SetSilenced adds the silenced to the get alert infos params +func (o *GetAlertInfosParams) SetSilenced(silenced *bool) { + o.Silenced = silenced +} + +// WithUnprocessed adds the unprocessed to the get alert infos params +func (o *GetAlertInfosParams) WithUnprocessed(unprocessed *bool) *GetAlertInfosParams { + o.SetUnprocessed(unprocessed) + return o +} + +// SetUnprocessed adds the unprocessed to the get alert infos params +func (o *GetAlertInfosParams) SetUnprocessed(unprocessed *bool) { + o.Unprocessed = unprocessed +} + +// WriteToRequest writes these params to a swagger request +func (o *GetAlertInfosParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if o.Active != nil { + + // query param active + var qrActive bool + + if o.Active != nil { + qrActive = *o.Active + } + qActive := swag.FormatBool(qrActive) + if qActive != "" { + + if err := r.SetQueryParam("active", qActive); err != nil { + return err + } + } + } + + if o.Filter != nil { + + // binding items for filter + joinedFilter := o.bindParamFilter(reg) + + // query array param filter + if err := r.SetQueryParam("filter", joinedFilter...); err != nil { + return err + } + } + + if o.GroupID != nil { + + // binding items for groupId + joinedGroupID := o.bindParamGroupID(reg) + + // query array param groupId + if err := r.SetQueryParam("groupId", joinedGroupID...); err != nil { + return err + } + } + + if o.Inhibited != nil { + + // query param inhibited + var qrInhibited bool + + if o.Inhibited != nil { + qrInhibited = *o.Inhibited + } + qInhibited := swag.FormatBool(qrInhibited) + if qInhibited != "" { + + if err := r.SetQueryParam("inhibited", qInhibited); err != nil { + return err + } + } + } + + if o.MaxResults != nil { + + // query param maxResults + var qrMaxResults int64 + + if o.MaxResults != nil { + qrMaxResults = *o.MaxResults + } + qMaxResults := swag.FormatInt64(qrMaxResults) + if qMaxResults != "" { + + if err := r.SetQueryParam("maxResults", qMaxResults); err != nil { + return err + } + } + } + + if o.NextToken != nil { + + // query param nextToken + var qrNextToken string + + if o.NextToken != nil { + qrNextToken = *o.NextToken + } + qNextToken := qrNextToken + if qNextToken != "" { + + if err := r.SetQueryParam("nextToken", qNextToken); err != nil { + return err + } + } + } + + if o.Receiver != nil { + + // query param receiver + var qrReceiver string + + if o.Receiver != nil { + qrReceiver = *o.Receiver + } + qReceiver := qrReceiver + if qReceiver != "" { + + if err := r.SetQueryParam("receiver", qReceiver); err != nil { + return err + } + } + } + + if o.Silenced != nil { + + // query param silenced + var qrSilenced bool + + if o.Silenced != nil { + qrSilenced = *o.Silenced + } + qSilenced := swag.FormatBool(qrSilenced) + if qSilenced != "" { + + if err := r.SetQueryParam("silenced", qSilenced); err != nil { + return err + } + } + } + + if o.Unprocessed != nil { + + // query param unprocessed + var qrUnprocessed bool + + if o.Unprocessed != nil { + qrUnprocessed = *o.Unprocessed + } + qUnprocessed := swag.FormatBool(qrUnprocessed) + if qUnprocessed != "" { + + if err := r.SetQueryParam("unprocessed", qUnprocessed); err != nil { + return err + } + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindParamGetAlertInfos binds the parameter filter +func (o *GetAlertInfosParams) bindParamFilter(formats strfmt.Registry) []string { + filterIR := o.Filter + + var filterIC []string + for _, filterIIR := range filterIR { // explode []string + + filterIIV := filterIIR // string as string + filterIC = append(filterIC, filterIIV) + } + + // items.CollectionFormat: "multi" + filterIS := swag.JoinByFormat(filterIC, "multi") + + return filterIS +} + +// bindParamGetAlertInfos binds the parameter groupId +func (o *GetAlertInfosParams) bindParamGroupID(formats strfmt.Registry) []string { + groupIDIR := o.GroupID + + var groupIDIC []string + for _, groupIDIIR := range groupIDIR { // explode []string + + groupIDIIV := groupIDIIR // string as string + groupIDIC = append(groupIDIC, groupIDIIV) + } + + // items.CollectionFormat: "multi" + groupIDIS := swag.JoinByFormat(groupIDIC, "multi") + + return groupIDIS +} diff --git a/api/v2/client/alertinfo/get_alert_infos_responses.go b/api/v2/client/alertinfo/get_alert_infos_responses.go new file mode 100644 index 0000000000..1c1482db0f --- /dev/null +++ b/api/v2/client/alertinfo/get_alert_infos_responses.go @@ -0,0 +1,246 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/prometheus/alertmanager/api/v2/models" +) + +// GetAlertInfosReader is a Reader for the GetAlertInfos structure. +type GetAlertInfosReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetAlertInfosReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetAlertInfosOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewGetAlertInfosBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewGetAlertInfosInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code()) + } +} + +// NewGetAlertInfosOK creates a GetAlertInfosOK with default headers values +func NewGetAlertInfosOK() *GetAlertInfosOK { + return &GetAlertInfosOK{} +} + +/* +GetAlertInfosOK describes a response with status code 200, with default header values. + +Get alerts response +*/ +type GetAlertInfosOK struct { + Payload *models.GettableAlertInfos +} + +// IsSuccess returns true when this get alert infos o k response has a 2xx status code +func (o *GetAlertInfosOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get alert infos o k response has a 3xx status code +func (o *GetAlertInfosOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get alert infos o k response has a 4xx status code +func (o *GetAlertInfosOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get alert infos o k response has a 5xx status code +func (o *GetAlertInfosOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get alert infos o k response a status code equal to that given +func (o *GetAlertInfosOK) IsCode(code int) bool { + return code == 200 +} + +func (o *GetAlertInfosOK) Error() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosOK %+v", 200, o.Payload) +} + +func (o *GetAlertInfosOK) String() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosOK %+v", 200, o.Payload) +} + +func (o *GetAlertInfosOK) GetPayload() *models.GettableAlertInfos { + return o.Payload +} + +func (o *GetAlertInfosOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.GettableAlertInfos) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetAlertInfosBadRequest creates a GetAlertInfosBadRequest with default headers values +func NewGetAlertInfosBadRequest() *GetAlertInfosBadRequest { + return &GetAlertInfosBadRequest{} +} + +/* +GetAlertInfosBadRequest describes a response with status code 400, with default header values. + +Bad request +*/ +type GetAlertInfosBadRequest struct { + Payload string +} + +// IsSuccess returns true when this get alert infos bad request response has a 2xx status code +func (o *GetAlertInfosBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get alert infos bad request response has a 3xx status code +func (o *GetAlertInfosBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get alert infos bad request response has a 4xx status code +func (o *GetAlertInfosBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this get alert infos bad request response has a 5xx status code +func (o *GetAlertInfosBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this get alert infos bad request response a status code equal to that given +func (o *GetAlertInfosBadRequest) IsCode(code int) bool { + return code == 400 +} + +func (o *GetAlertInfosBadRequest) Error() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosBadRequest %+v", 400, o.Payload) +} + +func (o *GetAlertInfosBadRequest) String() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosBadRequest %+v", 400, o.Payload) +} + +func (o *GetAlertInfosBadRequest) GetPayload() string { + return o.Payload +} + +func (o *GetAlertInfosBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetAlertInfosInternalServerError creates a GetAlertInfosInternalServerError with default headers values +func NewGetAlertInfosInternalServerError() *GetAlertInfosInternalServerError { + return &GetAlertInfosInternalServerError{} +} + +/* +GetAlertInfosInternalServerError describes a response with status code 500, with default header values. + +Internal server error +*/ +type GetAlertInfosInternalServerError struct { + Payload string +} + +// IsSuccess returns true when this get alert infos internal server error response has a 2xx status code +func (o *GetAlertInfosInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get alert infos internal server error response has a 3xx status code +func (o *GetAlertInfosInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get alert infos internal server error response has a 4xx status code +func (o *GetAlertInfosInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this get alert infos internal server error response has a 5xx status code +func (o *GetAlertInfosInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this get alert infos internal server error response a status code equal to that given +func (o *GetAlertInfosInternalServerError) IsCode(code int) bool { + return code == 500 +} + +func (o *GetAlertInfosInternalServerError) Error() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosInternalServerError %+v", 500, o.Payload) +} + +func (o *GetAlertInfosInternalServerError) String() string { + return fmt.Sprintf("[GET /alertinfos][%d] getAlertInfosInternalServerError %+v", 500, o.Payload) +} + +func (o *GetAlertInfosInternalServerError) GetPayload() string { + return o.Payload +} + +func (o *GetAlertInfosInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/api/v2/client/alertmanager_api_client.go b/api/v2/client/alertmanager_api_client.go index e786b339ff..e1fd0161db 100644 --- a/api/v2/client/alertmanager_api_client.go +++ b/api/v2/client/alertmanager_api_client.go @@ -27,6 +27,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/client/alert" "github.com/prometheus/alertmanager/api/v2/client/alertgroup" "github.com/prometheus/alertmanager/api/v2/client/alertgroupinfolist" + "github.com/prometheus/alertmanager/api/v2/client/alertinfo" "github.com/prometheus/alertmanager/api/v2/client/general" "github.com/prometheus/alertmanager/api/v2/client/receiver" "github.com/prometheus/alertmanager/api/v2/client/silence" @@ -77,6 +78,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *Alertmanag cli.Alert = alert.New(transport, formats) cli.Alertgroup = alertgroup.New(transport, formats) cli.Alertgroupinfolist = alertgroupinfolist.New(transport, formats) + cli.Alertinfo = alertinfo.New(transport, formats) cli.General = general.New(transport, formats) cli.Receiver = receiver.New(transport, formats) cli.Silence = silence.New(transport, formats) @@ -130,6 +132,8 @@ type AlertmanagerAPI struct { Alertgroupinfolist alertgroupinfolist.ClientService + Alertinfo alertinfo.ClientService + General general.ClientService Receiver receiver.ClientService @@ -145,6 +149,7 @@ func (c *AlertmanagerAPI) SetTransport(transport runtime.ClientTransport) { c.Alert.SetTransport(transport) c.Alertgroup.SetTransport(transport) c.Alertgroupinfolist.SetTransport(transport) + c.Alertinfo.SetTransport(transport) c.General.SetTransport(transport) c.Receiver.SetTransport(transport) c.Silence.SetTransport(transport) diff --git a/api/v2/compat.go b/api/v2/compat.go index 27091649e1..9acf2eec75 100644 --- a/api/v2/compat.go +++ b/api/v2/compat.go @@ -197,6 +197,38 @@ func APILabelSetToModelLabelSet(apiLabelSet open_api_models.LabelSet) prometheus return modelLabelSet } +// AlertInfosTruncate truncate the open_api_models.GettableAlerts using maxResult and return a nextToken if there are items has been truncated. +func AlertInfosTruncate(alerts open_api_models.GettableAlerts, maxResult *int64, nextToken *string) (open_api_models.GettableAlerts, string) { + resultNumber := 0 + var previousAlertID *string + var returnPaginationToken string + returnAlerts := make(open_api_models.GettableAlerts, 0, len(alerts)) + for _, alert := range alerts { + + // Skip the alert if the next token is set and hasn't arrived the nextToken item yet. + alertFP := alert.Fingerprint + if nextToken != nil && alertFP != nil && *nextToken >= *alertFP { + continue + } + + // Add the alert to the return slice if the maxItem is not hit + if maxResult == nil || resultNumber < int(*maxResult) { + previousAlertID = alert.Fingerprint + returnAlerts = append(returnAlerts, alert) + resultNumber++ + continue + } + + // Return the next token if there is more alert + if resultNumber == int(*maxResult) && previousAlertID != nil { + returnPaginationToken = *previousAlertID + break + } + } + + return returnAlerts, returnPaginationToken +} + func AlertGroupInfoListTruncate(alertGroupInfos []*open_api_models.AlertGroupInfo, maxResult *int64) ([]*open_api_models.AlertGroupInfo, string) { resultNumber := 0 var previousAgID *string diff --git a/api/v2/models/gettable_alert_infos.go b/api/v2/models/gettable_alert_infos.go new file mode 100644 index 0000000000..cbd793d19c --- /dev/null +++ b/api/v2/models/gettable_alert_infos.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// GettableAlertInfos gettable alert infos +// +// swagger:model gettableAlertInfos +type GettableAlertInfos struct { + + // alerts + Alerts []*GettableAlert `json:"alerts"` + + // next token + NextToken string `json:"nextToken,omitempty"` +} + +// Validate validates this gettable alert infos +func (m *GettableAlertInfos) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAlerts(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *GettableAlertInfos) validateAlerts(formats strfmt.Registry) error { + if swag.IsZero(m.Alerts) { // not required + return nil + } + + for i := 0; i < len(m.Alerts); i++ { + if swag.IsZero(m.Alerts[i]) { // not required + continue + } + + if m.Alerts[i] != nil { + if err := m.Alerts[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("alerts" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("alerts" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this gettable alert infos based on the context it is used +func (m *GettableAlertInfos) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAlerts(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *GettableAlertInfos) contextValidateAlerts(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Alerts); i++ { + + if m.Alerts[i] != nil { + if err := m.Alerts[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("alerts" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("alerts" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *GettableAlertInfos) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *GettableAlertInfos) UnmarshalBinary(b []byte) error { + var res GettableAlertInfos + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/api/v2/openapi.yaml b/api/v2/openapi.yaml index 5c4059555b..83974fdc15 100644 --- a/api/v2/openapi.yaml +++ b/api/v2/openapi.yaml @@ -201,6 +201,74 @@ paths: $ref: '#/responses/InternalServerError' '400': $ref: '#/responses/BadRequest' + + /alertinfos: + get: + tags: + - alertinfo + operationId: getAlertInfos + description: Get a list of alert infos + parameters: + - in: query + name: active + type: boolean + description: Show active alerts + default: true + - in: query + name: silenced + type: boolean + description: Show silenced alerts + default: true + - in: query + name: inhibited + type: boolean + description: Show inhibited alerts + default: true + - in: query + name: unprocessed + type: boolean + description: Show unprocessed alerts + default: true + - name: filter + in: query + description: A list of matchers to filter alerts by + required: false + type: array + collectionFormat: multi + items: + type: string + - name: receiver + in: query + description: A regex matching receivers to filter alerts by + required: false + type: string + - name: groupId + in: query + description: A list of group IDs to filter alerts by + required: false + type: array + collectionFormat: multi + items: + type: string + - name: nextToken + in: query + description: The token for the next set of items to return + required: false + type: string + - name: maxResults + in: query + description: The maximum number of alert to return in one getAlertInfos operation. + required: false + type: integer + responses: + '200': + description: Get alerts response + schema: + '$ref': '#/definitions/gettableAlertInfos' + '400': + $ref: '#/responses/BadRequest' + '500': + $ref: '#/responses/InternalServerError' /alerts/groups: get: tags: @@ -450,6 +518,15 @@ definitions: type: array items: $ref: '#/definitions/gettableAlert' + gettableAlertInfos: + type: object + properties: + alerts: + type: array + items: + $ref: '#/definitions/gettableAlert' + nextToken: + type: string gettableAlert: allOf: - type: object diff --git a/api/v2/restapi/configure_alertmanager.go b/api/v2/restapi/configure_alertmanager.go index 797d2c80ca..9f6b1e9573 100644 --- a/api/v2/restapi/configure_alertmanager.go +++ b/api/v2/restapi/configure_alertmanager.go @@ -28,6 +28,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert" "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup" "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist" + "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertinfo" "github.com/prometheus/alertmanager/api/v2/restapi/operations/general" "github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver" "github.com/prometheus/alertmanager/api/v2/restapi/operations/silence" @@ -72,6 +73,11 @@ func configureAPI(api *operations.AlertmanagerAPI) http.Handler { return middleware.NotImplemented("operation alertgroup.GetAlertGroups has not yet been implemented") }) } + if api.AlertinfoGetAlertInfosHandler == nil { + api.AlertinfoGetAlertInfosHandler = alertinfo.GetAlertInfosHandlerFunc(func(params alertinfo.GetAlertInfosParams) middleware.Responder { + return middleware.NotImplemented("operation alertinfo.GetAlertInfos has not yet been implemented") + }) + } if api.AlertGetAlertsHandler == nil { api.AlertGetAlertsHandler = alert.GetAlertsHandlerFunc(func(params alert.GetAlertsParams) middleware.Responder { return middleware.NotImplemented("operation alert.GetAlerts has not yet been implemented") diff --git a/api/v2/restapi/embedded_spec.go b/api/v2/restapi/embedded_spec.go index 6a7f7db376..56123a3da7 100644 --- a/api/v2/restapi/embedded_spec.go +++ b/api/v2/restapi/embedded_spec.go @@ -93,6 +93,97 @@ func init() { } } }, + "/alertinfos": { + "get": { + "description": "Get a list of alert infos", + "tags": [ + "alertinfo" + ], + "operationId": "getAlertInfos", + "parameters": [ + { + "type": "boolean", + "default": true, + "description": "Show active alerts", + "name": "active", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show silenced alerts", + "name": "silenced", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show inhibited alerts", + "name": "inhibited", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show unprocessed alerts", + "name": "unprocessed", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "A list of matchers to filter alerts by", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "description": "A regex matching receivers to filter alerts by", + "name": "receiver", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "A list of group IDs to filter alerts by", + "name": "groupId", + "in": "query" + }, + { + "type": "string", + "description": "The token for the next set of items to return", + "name": "nextToken", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of alert to return in one getAlertInfos operation.", + "name": "maxResults", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Get alerts response", + "schema": { + "$ref": "#/definitions/gettableAlertInfos" + } + }, + "400": { + "$ref": "#/responses/BadRequest" + }, + "500": { + "$ref": "#/responses/InternalServerError" + } + } + } + }, "/alerts": { "get": { "description": "Get a list of alerts", @@ -642,6 +733,20 @@ func init() { } ] }, + "gettableAlertInfos": { + "type": "object", + "properties": { + "alerts": { + "type": "array", + "items": { + "$ref": "#/definitions/gettableAlert" + } + }, + "nextToken": { + "type": "string" + } + } + }, "gettableAlerts": { "type": "array", "items": { @@ -965,6 +1070,103 @@ func init() { } } }, + "/alertinfos": { + "get": { + "description": "Get a list of alert infos", + "tags": [ + "alertinfo" + ], + "operationId": "getAlertInfos", + "parameters": [ + { + "type": "boolean", + "default": true, + "description": "Show active alerts", + "name": "active", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show silenced alerts", + "name": "silenced", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show inhibited alerts", + "name": "inhibited", + "in": "query" + }, + { + "type": "boolean", + "default": true, + "description": "Show unprocessed alerts", + "name": "unprocessed", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "A list of matchers to filter alerts by", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "description": "A regex matching receivers to filter alerts by", + "name": "receiver", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "A list of group IDs to filter alerts by", + "name": "groupId", + "in": "query" + }, + { + "type": "string", + "description": "The token for the next set of items to return", + "name": "nextToken", + "in": "query" + }, + { + "type": "integer", + "description": "The maximum number of alert to return in one getAlertInfos operation.", + "name": "maxResults", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Get alerts response", + "schema": { + "$ref": "#/definitions/gettableAlertInfos" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, "/alerts": { "get": { "description": "Get a list of alerts", @@ -1547,6 +1749,20 @@ func init() { } ] }, + "gettableAlertInfos": { + "type": "object", + "properties": { + "alerts": { + "type": "array", + "items": { + "$ref": "#/definitions/gettableAlert" + } + }, + "nextToken": { + "type": "string" + } + } + }, "gettableAlerts": { "type": "array", "items": { diff --git a/api/v2/restapi/operations/alertinfo/get_alert_infos.go b/api/v2/restapi/operations/alertinfo/get_alert_infos.go new file mode 100644 index 0000000000..66b5a29a48 --- /dev/null +++ b/api/v2/restapi/operations/alertinfo/get_alert_infos.go @@ -0,0 +1,70 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// GetAlertInfosHandlerFunc turns a function with the right signature into a get alert infos handler +type GetAlertInfosHandlerFunc func(GetAlertInfosParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetAlertInfosHandlerFunc) Handle(params GetAlertInfosParams) middleware.Responder { + return fn(params) +} + +// GetAlertInfosHandler interface for that can handle valid get alert infos params +type GetAlertInfosHandler interface { + Handle(GetAlertInfosParams) middleware.Responder +} + +// NewGetAlertInfos creates a new http.Handler for the get alert infos operation +func NewGetAlertInfos(ctx *middleware.Context, handler GetAlertInfosHandler) *GetAlertInfos { + return &GetAlertInfos{Context: ctx, Handler: handler} +} + +/* + GetAlertInfos swagger:route GET /alertinfos alertinfo getAlertInfos + +Get a list of alert infos +*/ +type GetAlertInfos struct { + Context *middleware.Context + Handler GetAlertInfosHandler +} + +func (o *GetAlertInfos) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetAlertInfosParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/api/v2/restapi/operations/alertinfo/get_alert_infos_parameters.go b/api/v2/restapi/operations/alertinfo/get_alert_infos_parameters.go new file mode 100644 index 0000000000..ea0c47ff24 --- /dev/null +++ b/api/v2/restapi/operations/alertinfo/get_alert_infos_parameters.go @@ -0,0 +1,369 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewGetAlertInfosParams creates a new GetAlertInfosParams object +// with the default values initialized. +func NewGetAlertInfosParams() GetAlertInfosParams { + + var ( + // initialize parameters with default values + + activeDefault = bool(true) + + inhibitedDefault = bool(true) + + silencedDefault = bool(true) + unprocessedDefault = bool(true) + ) + + return GetAlertInfosParams{ + Active: &activeDefault, + + Inhibited: &inhibitedDefault, + + Silenced: &silencedDefault, + + Unprocessed: &unprocessedDefault, + } +} + +// GetAlertInfosParams contains all the bound params for the get alert infos operation +// typically these are obtained from a http.Request +// +// swagger:parameters getAlertInfos +type GetAlertInfosParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*Show active alerts + In: query + Default: true + */ + Active *bool + /*A list of matchers to filter alerts by + In: query + Collection Format: multi + */ + Filter []string + /*A list of group IDs to filter alerts by + In: query + Collection Format: multi + */ + GroupID []string + /*Show inhibited alerts + In: query + Default: true + */ + Inhibited *bool + /*The maximum number of alert to return in one getAlertInfos operation. + In: query + */ + MaxResults *int64 + /*The token for the next set of items to return + In: query + */ + NextToken *string + /*A regex matching receivers to filter alerts by + In: query + */ + Receiver *string + /*Show silenced alerts + In: query + Default: true + */ + Silenced *bool + /*Show unprocessed alerts + In: query + Default: true + */ + Unprocessed *bool +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetAlertInfosParams() beforehand. +func (o *GetAlertInfosParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + qActive, qhkActive, _ := qs.GetOK("active") + if err := o.bindActive(qActive, qhkActive, route.Formats); err != nil { + res = append(res, err) + } + + qFilter, qhkFilter, _ := qs.GetOK("filter") + if err := o.bindFilter(qFilter, qhkFilter, route.Formats); err != nil { + res = append(res, err) + } + + qGroupID, qhkGroupID, _ := qs.GetOK("groupId") + if err := o.bindGroupID(qGroupID, qhkGroupID, route.Formats); err != nil { + res = append(res, err) + } + + qInhibited, qhkInhibited, _ := qs.GetOK("inhibited") + if err := o.bindInhibited(qInhibited, qhkInhibited, route.Formats); err != nil { + res = append(res, err) + } + + qMaxResults, qhkMaxResults, _ := qs.GetOK("maxResults") + if err := o.bindMaxResults(qMaxResults, qhkMaxResults, route.Formats); err != nil { + res = append(res, err) + } + + qNextToken, qhkNextToken, _ := qs.GetOK("nextToken") + if err := o.bindNextToken(qNextToken, qhkNextToken, route.Formats); err != nil { + res = append(res, err) + } + + qReceiver, qhkReceiver, _ := qs.GetOK("receiver") + if err := o.bindReceiver(qReceiver, qhkReceiver, route.Formats); err != nil { + res = append(res, err) + } + + qSilenced, qhkSilenced, _ := qs.GetOK("silenced") + if err := o.bindSilenced(qSilenced, qhkSilenced, route.Formats); err != nil { + res = append(res, err) + } + + qUnprocessed, qhkUnprocessed, _ := qs.GetOK("unprocessed") + if err := o.bindUnprocessed(qUnprocessed, qhkUnprocessed, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindActive binds and validates parameter Active from query. +func (o *GetAlertInfosParams) bindActive(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetAlertInfosParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("active", "query", "bool", raw) + } + o.Active = &value + + return nil +} + +// bindFilter binds and validates array parameter Filter from query. +// +// Arrays are parsed according to CollectionFormat: "multi" (defaults to "csv" when empty). +func (o *GetAlertInfosParams) bindFilter(rawData []string, hasKey bool, formats strfmt.Registry) error { + // CollectionFormat: multi + filterIC := rawData + if len(filterIC) == 0 { + return nil + } + + var filterIR []string + for _, filterIV := range filterIC { + filterI := filterIV + + filterIR = append(filterIR, filterI) + } + + o.Filter = filterIR + + return nil +} + +// bindGroupID binds and validates array parameter GroupID from query. +// +// Arrays are parsed according to CollectionFormat: "multi" (defaults to "csv" when empty). +func (o *GetAlertInfosParams) bindGroupID(rawData []string, hasKey bool, formats strfmt.Registry) error { + // CollectionFormat: multi + groupIDIC := rawData + if len(groupIDIC) == 0 { + return nil + } + + var groupIDIR []string + for _, groupIDIV := range groupIDIC { + groupIDI := groupIDIV + + groupIDIR = append(groupIDIR, groupIDI) + } + + o.GroupID = groupIDIR + + return nil +} + +// bindInhibited binds and validates parameter Inhibited from query. +func (o *GetAlertInfosParams) bindInhibited(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetAlertInfosParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("inhibited", "query", "bool", raw) + } + o.Inhibited = &value + + return nil +} + +// bindMaxResults binds and validates parameter MaxResults from query. +func (o *GetAlertInfosParams) bindMaxResults(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("maxResults", "query", "int64", raw) + } + o.MaxResults = &value + + return nil +} + +// bindNextToken binds and validates parameter NextToken from query. +func (o *GetAlertInfosParams) bindNextToken(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.NextToken = &raw + + return nil +} + +// bindReceiver binds and validates parameter Receiver from query. +func (o *GetAlertInfosParams) bindReceiver(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.Receiver = &raw + + return nil +} + +// bindSilenced binds and validates parameter Silenced from query. +func (o *GetAlertInfosParams) bindSilenced(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetAlertInfosParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("silenced", "query", "bool", raw) + } + o.Silenced = &value + + return nil +} + +// bindUnprocessed binds and validates parameter Unprocessed from query. +func (o *GetAlertInfosParams) bindUnprocessed(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetAlertInfosParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("unprocessed", "query", "bool", raw) + } + o.Unprocessed = &value + + return nil +} diff --git a/api/v2/restapi/operations/alertinfo/get_alert_infos_responses.go b/api/v2/restapi/operations/alertinfo/get_alert_infos_responses.go new file mode 100644 index 0000000000..e0ff2075c3 --- /dev/null +++ b/api/v2/restapi/operations/alertinfo/get_alert_infos_responses.go @@ -0,0 +1,159 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/prometheus/alertmanager/api/v2/models" +) + +// GetAlertInfosOKCode is the HTTP code returned for type GetAlertInfosOK +const GetAlertInfosOKCode int = 200 + +/* +GetAlertInfosOK Get alerts response + +swagger:response getAlertInfosOK +*/ +type GetAlertInfosOK struct { + + /* + In: Body + */ + Payload *models.GettableAlertInfos `json:"body,omitempty"` +} + +// NewGetAlertInfosOK creates GetAlertInfosOK with default headers values +func NewGetAlertInfosOK() *GetAlertInfosOK { + + return &GetAlertInfosOK{} +} + +// WithPayload adds the payload to the get alert infos o k response +func (o *GetAlertInfosOK) WithPayload(payload *models.GettableAlertInfos) *GetAlertInfosOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get alert infos o k response +func (o *GetAlertInfosOK) SetPayload(payload *models.GettableAlertInfos) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAlertInfosOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetAlertInfosBadRequestCode is the HTTP code returned for type GetAlertInfosBadRequest +const GetAlertInfosBadRequestCode int = 400 + +/* +GetAlertInfosBadRequest Bad request + +swagger:response getAlertInfosBadRequest +*/ +type GetAlertInfosBadRequest struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewGetAlertInfosBadRequest creates GetAlertInfosBadRequest with default headers values +func NewGetAlertInfosBadRequest() *GetAlertInfosBadRequest { + + return &GetAlertInfosBadRequest{} +} + +// WithPayload adds the payload to the get alert infos bad request response +func (o *GetAlertInfosBadRequest) WithPayload(payload string) *GetAlertInfosBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get alert infos bad request response +func (o *GetAlertInfosBadRequest) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAlertInfosBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// GetAlertInfosInternalServerErrorCode is the HTTP code returned for type GetAlertInfosInternalServerError +const GetAlertInfosInternalServerErrorCode int = 500 + +/* +GetAlertInfosInternalServerError Internal server error + +swagger:response getAlertInfosInternalServerError +*/ +type GetAlertInfosInternalServerError struct { + + /* + In: Body + */ + Payload string `json:"body,omitempty"` +} + +// NewGetAlertInfosInternalServerError creates GetAlertInfosInternalServerError with default headers values +func NewGetAlertInfosInternalServerError() *GetAlertInfosInternalServerError { + + return &GetAlertInfosInternalServerError{} +} + +// WithPayload adds the payload to the get alert infos internal server error response +func (o *GetAlertInfosInternalServerError) WithPayload(payload string) *GetAlertInfosInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get alert infos internal server error response +func (o *GetAlertInfosInternalServerError) SetPayload(payload string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAlertInfosInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/api/v2/restapi/operations/alertinfo/get_alert_infos_urlbuilder.go b/api/v2/restapi/operations/alertinfo/get_alert_infos_urlbuilder.go new file mode 100644 index 0000000000..b22a5fb658 --- /dev/null +++ b/api/v2/restapi/operations/alertinfo/get_alert_infos_urlbuilder.go @@ -0,0 +1,203 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package alertinfo + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + + "github.com/go-openapi/swag" +) + +// GetAlertInfosURL generates an URL for the get alert infos operation +type GetAlertInfosURL struct { + Active *bool + Filter []string + GroupID []string + Inhibited *bool + MaxResults *int64 + NextToken *string + Receiver *string + Silenced *bool + Unprocessed *bool + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAlertInfosURL) WithBasePath(bp string) *GetAlertInfosURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAlertInfosURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetAlertInfosURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/alertinfos" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v2/" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + var activeQ string + if o.Active != nil { + activeQ = swag.FormatBool(*o.Active) + } + if activeQ != "" { + qs.Set("active", activeQ) + } + + var filterIR []string + for _, filterI := range o.Filter { + filterIS := filterI + if filterIS != "" { + filterIR = append(filterIR, filterIS) + } + } + + filter := swag.JoinByFormat(filterIR, "multi") + + for _, qsv := range filter { + qs.Add("filter", qsv) + } + + var groupIDIR []string + for _, groupIDI := range o.GroupID { + groupIDIS := groupIDI + if groupIDIS != "" { + groupIDIR = append(groupIDIR, groupIDIS) + } + } + + groupID := swag.JoinByFormat(groupIDIR, "multi") + + for _, qsv := range groupID { + qs.Add("groupId", qsv) + } + + var inhibitedQ string + if o.Inhibited != nil { + inhibitedQ = swag.FormatBool(*o.Inhibited) + } + if inhibitedQ != "" { + qs.Set("inhibited", inhibitedQ) + } + + var maxResultsQ string + if o.MaxResults != nil { + maxResultsQ = swag.FormatInt64(*o.MaxResults) + } + if maxResultsQ != "" { + qs.Set("maxResults", maxResultsQ) + } + + var nextTokenQ string + if o.NextToken != nil { + nextTokenQ = *o.NextToken + } + if nextTokenQ != "" { + qs.Set("nextToken", nextTokenQ) + } + + var receiverQ string + if o.Receiver != nil { + receiverQ = *o.Receiver + } + if receiverQ != "" { + qs.Set("receiver", receiverQ) + } + + var silencedQ string + if o.Silenced != nil { + silencedQ = swag.FormatBool(*o.Silenced) + } + if silencedQ != "" { + qs.Set("silenced", silencedQ) + } + + var unprocessedQ string + if o.Unprocessed != nil { + unprocessedQ = swag.FormatBool(*o.Unprocessed) + } + if unprocessedQ != "" { + qs.Set("unprocessed", unprocessedQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetAlertInfosURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetAlertInfosURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetAlertInfosURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetAlertInfosURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetAlertInfosURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetAlertInfosURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/api/v2/restapi/operations/alertmanager_api.go b/api/v2/restapi/operations/alertmanager_api.go index 5c41351eb3..7c6bd2f1da 100644 --- a/api/v2/restapi/operations/alertmanager_api.go +++ b/api/v2/restapi/operations/alertmanager_api.go @@ -36,6 +36,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert" "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup" "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist" + "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertinfo" "github.com/prometheus/alertmanager/api/v2/restapi/operations/general" "github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver" "github.com/prometheus/alertmanager/api/v2/restapi/operations/silence" @@ -72,6 +73,9 @@ func NewAlertmanagerAPI(spec *loads.Document) *AlertmanagerAPI { AlertgroupGetAlertGroupsHandler: alertgroup.GetAlertGroupsHandlerFunc(func(params alertgroup.GetAlertGroupsParams) middleware.Responder { return middleware.NotImplemented("operation alertgroup.GetAlertGroups has not yet been implemented") }), + AlertinfoGetAlertInfosHandler: alertinfo.GetAlertInfosHandlerFunc(func(params alertinfo.GetAlertInfosParams) middleware.Responder { + return middleware.NotImplemented("operation alertinfo.GetAlertInfos has not yet been implemented") + }), AlertGetAlertsHandler: alert.GetAlertsHandlerFunc(func(params alert.GetAlertsParams) middleware.Responder { return middleware.NotImplemented("operation alert.GetAlerts has not yet been implemented") }), @@ -135,6 +139,8 @@ type AlertmanagerAPI struct { AlertgroupinfolistGetAlertGroupInfoListHandler alertgroupinfolist.GetAlertGroupInfoListHandler // AlertgroupGetAlertGroupsHandler sets the operation handler for the get alert groups operation AlertgroupGetAlertGroupsHandler alertgroup.GetAlertGroupsHandler + // AlertinfoGetAlertInfosHandler sets the operation handler for the get alert infos operation + AlertinfoGetAlertInfosHandler alertinfo.GetAlertInfosHandler // AlertGetAlertsHandler sets the operation handler for the get alerts operation AlertGetAlertsHandler alert.GetAlertsHandler // ReceiverGetReceiversHandler sets the operation handler for the get receivers operation @@ -235,6 +241,9 @@ func (o *AlertmanagerAPI) Validate() error { if o.AlertgroupGetAlertGroupsHandler == nil { unregistered = append(unregistered, "alertgroup.GetAlertGroupsHandler") } + if o.AlertinfoGetAlertInfosHandler == nil { + unregistered = append(unregistered, "alertinfo.GetAlertInfosHandler") + } if o.AlertGetAlertsHandler == nil { unregistered = append(unregistered, "alert.GetAlertsHandler") } @@ -359,6 +368,10 @@ func (o *AlertmanagerAPI) initHandlerCache() { if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } + o.handlers["GET"]["/alertinfos"] = alertinfo.NewGetAlertInfos(o.context, o.AlertinfoGetAlertInfosHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } o.handlers["GET"]["/alerts"] = alert.NewGetAlerts(o.context, o.AlertGetAlertsHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) diff --git a/api/v2/testing.go b/api/v2/testing.go index c7bcbe627f..fbb38a9d53 100644 --- a/api/v2/testing.go +++ b/api/v2/testing.go @@ -110,3 +110,30 @@ func (f *fakeAlerts) GetPending() provider.AlertIterator { }() return provider.NewAlertIterator(ch, done, f.err) } + +func newGetAlertStatus(f *fakeAlerts) func(model.Fingerprint) types.AlertStatus { + return func(fp model.Fingerprint) types.AlertStatus { + status := types.AlertStatus{SilencedBy: []string{}, InhibitedBy: []string{}} + + i, ok := f.fps[fp] + if !ok { + return status + } + alert := f.alerts[i] + switch alert.Labels["state"] { + case "active": + status.State = types.AlertStateActive + case "unprocessed": + status.State = types.AlertStateUnprocessed + case "suppressed": + status.State = types.AlertStateSuppressed + } + if alert.Labels["silenced_by"] != "" { + status.SilencedBy = append(status.SilencedBy, string(alert.Labels["silenced_by"])) + } + if alert.Labels["inhibited_by"] != "" { + status.InhibitedBy = append(status.InhibitedBy, string(alert.Labels["inhibited_by"])) + } + return status + } +} diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index b2938189d5..531708acdf 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -312,8 +312,8 @@ func run() int { disp.Stop() }() - groupFn := func(routeFilter func(*dispatch.Route) bool, alertFilter func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) { - return disp.Groups(routeFilter, alertFilter) + groupFn := func(routeFilter func(*dispatch.Route) bool, alertFilter func(*types.Alert, time.Time) bool, groupIdsFilter func(string) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) { + return disp.Groups(routeFilter, alertFilter, groupIdsFilter) } // An interface value that holds a nil concrete value is non-nil. diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go index f8b770885f..eca9221527 100644 --- a/dispatch/dispatch.go +++ b/dispatch/dispatch.go @@ -219,7 +219,7 @@ func (ag AlertGroups) Less(i, j int) bool { func (ag AlertGroups) Len() int { return len(ag) } // Groups returns a slice of AlertGroups from the dispatcher's internal state. -func (d *Dispatcher) Groups(routeFilter func(*Route) bool, alertFilter func(*types.Alert, time.Time) bool) (AlertGroups, map[model.Fingerprint][]string) { +func (d *Dispatcher) Groups(routeFilter func(*Route) bool, alertFilter func(*types.Alert, time.Time) bool, groupIDFilter func(groupId string) bool) (AlertGroups, map[model.Fingerprint][]string) { groups := AlertGroups{} d.mtx.RLock() @@ -237,6 +237,9 @@ func (d *Dispatcher) Groups(routeFilter func(*Route) bool, alertFilter func(*typ } for _, ag := range ags { + if !groupIDFilter(ag.GroupID()) { + continue + } receiver := route.RouteOpts.Receiver alertGroup := &AlertGroup{ Labels: ag.labels, diff --git a/dispatch/dispatch_test.go b/dispatch/dispatch_test.go index 6933d4fcda..546405f797 100644 --- a/dispatch/dispatch_test.go +++ b/dispatch/dispatch_test.go @@ -405,6 +405,8 @@ route: return true }, func(*types.Alert, time.Time) bool { return true + }, func(string) bool { + return true }, ) @@ -545,8 +547,9 @@ route: routeFilter := func(*Route) bool { return true } alertFilter := func(*types.Alert, time.Time) bool { return true } + groupFilter := func(string) bool { return true } - alertGroups, _ := dispatcher.Groups(routeFilter, alertFilter) + alertGroups, _ := dispatcher.Groups(routeFilter, alertFilter, groupFilter) require.Len(t, alertGroups, 6) require.Equal(t, 0.0, testutil.ToFloat64(m.aggrGroupLimitReached)) @@ -564,7 +567,7 @@ route: require.Equal(t, 1.0, testutil.ToFloat64(m.aggrGroupLimitReached)) // Verify there are still only 6 groups. - alertGroups, _ = dispatcher.Groups(routeFilter, alertFilter) + alertGroups, _ = dispatcher.Groups(routeFilter, alertFilter, groupFilter) require.Len(t, alertGroups, 6) }