Skip to content

Commit b3868a3

Browse files
qinxx108yeya24
authored andcommitted
Add the alert group info API
1 parent a23237f commit b3868a3

20 files changed

+2146
-23
lines changed

api/api.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ type Options struct {
8080
// according to the current active configuration. Alerts returned are
8181
// filtered by the arguments provided to the function.
8282
GroupFunc func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string)
83-
83+
// GroupInfoFunc returns a list of alert groups information. The alerts are grouped
84+
// according to the current active configuration. This function will not return the alerts inside each group.
85+
GroupInfoFunc func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos
8486
// APICallback define the callback function that each api call will perform before returned.
8587
APICallback callback.Callback
8688
}
@@ -125,6 +127,7 @@ func New(opts Options) (*API, error) {
125127
v2, err := apiv2.NewAPI(
126128
opts.Alerts,
127129
opts.GroupFunc,
130+
opts.GroupInfoFunc,
128131
opts.AlertStatusFunc,
129132
opts.GroupMutedFunc,
130133
opts.Silences,

api/v2/api.go

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232
"github.com/prometheus/common/version"
3333
"github.com/rs/cors"
3434

35+
alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"
36+
3537
"github.com/prometheus/alertmanager/api/metrics"
3638
open_api_models "github.com/prometheus/alertmanager/api/v2/models"
3739
"github.com/prometheus/alertmanager/api/v2/restapi"
@@ -55,14 +57,15 @@ import (
5557

5658
// API represents an Alertmanager API v2.
5759
type API struct {
58-
peer cluster.ClusterPeer
59-
silences *silence.Silences
60-
alerts provider.Alerts
61-
alertGroups groupsFn
62-
getAlertStatus getAlertStatusFn
63-
groupMutedFunc groupMutedFunc
64-
apiCallback callback.Callback
65-
uptime time.Time
60+
peer cluster.ClusterPeer
61+
silences *silence.Silences
62+
alerts provider.Alerts
63+
alertGroups groupsFn
64+
alertGroupInfos groupInfosFn
65+
getAlertStatus getAlertStatusFn
66+
groupMutedFunc groupMutedFunc
67+
apiCallback callback.Callback
68+
uptime time.Time
6669

6770
// mtx protects alertmanagerConfig, setAlertStatus and route.
6871
mtx sync.RWMutex
@@ -81,6 +84,7 @@ type API struct {
8184
type (
8285
groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string)
8386
groupMutedFunc func(routeID, groupKey string) ([]string, bool)
87+
groupInfosFn func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos
8488
getAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus
8589
setAlertStatusFn func(prometheus_model.LabelSet)
8690
)
@@ -89,6 +93,7 @@ type (
8993
func NewAPI(
9094
alerts provider.Alerts,
9195
gf groupsFn,
96+
gif groupInfosFn,
9297
asf getAlertStatusFn,
9398
gmf groupMutedFunc,
9499
silences *silence.Silences,
@@ -101,16 +106,17 @@ func NewAPI(
101106
apiCallback = callback.NoopAPICallback{}
102107
}
103108
api := API{
104-
alerts: alerts,
105-
getAlertStatus: asf,
106-
alertGroups: gf,
107-
groupMutedFunc: gmf,
108-
peer: peer,
109-
silences: silences,
110-
apiCallback: apiCallback,
111-
logger: l,
112-
m: metrics.NewAlerts(r),
113-
uptime: time.Now(),
109+
alerts: alerts,
110+
getAlertStatus: asf,
111+
alertGroups: gf,
112+
alertGroupInfos: gif,
113+
groupMutedFunc: gmf,
114+
peer: peer,
115+
silences: silences,
116+
apiCallback: apiCallback,
117+
logger: l,
118+
m: metrics.NewAlerts(r),
119+
uptime: time.Now(),
114120
}
115121

116122
// Load embedded swagger file.
@@ -134,6 +140,7 @@ func NewAPI(
134140
openAPI.AlertGetAlertsHandler = alert_ops.GetAlertsHandlerFunc(api.getAlertsHandler)
135141
openAPI.AlertPostAlertsHandler = alert_ops.PostAlertsHandlerFunc(api.postAlertsHandler)
136142
openAPI.AlertgroupGetAlertGroupsHandler = alertgroup_ops.GetAlertGroupsHandlerFunc(api.getAlertGroupsHandler)
143+
openAPI.AlertgroupinfolistGetAlertGroupInfoListHandler = alertgroupinfolist_ops.GetAlertGroupInfoListHandlerFunc(api.getAlertGroupInfoListHandler)
137144
openAPI.GeneralGetStatusHandler = general_ops.GetStatusHandlerFunc(api.getStatusHandler)
138145
openAPI.ReceiverGetReceiversHandler = receiver_ops.GetReceiversHandlerFunc(api.getReceiversHandler)
139146
openAPI.SilenceDeleteSilenceHandler = silence_ops.DeleteSilenceHandlerFunc(api.deleteSilenceHandler)
@@ -453,6 +460,78 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
453460
return alertgroup_ops.NewGetAlertGroupsOK().WithPayload(callbackRes)
454461
}
455462

463+
func (api *API) getAlertGroupInfoListHandler(params alertgroupinfolist_ops.GetAlertGroupInfoListParams) middleware.Responder {
464+
logger := api.requestLogger(params.HTTPRequest)
465+
466+
var receiverFilter *regexp.Regexp
467+
var err error
468+
if params.Receiver != nil {
469+
receiverFilter, err = regexp.Compile("^(?:" + *params.Receiver + ")$")
470+
if err != nil {
471+
logger.Error("Failed to compile receiver regex", "err", err)
472+
return alertgroupinfolist_ops.
473+
NewGetAlertGroupInfoListBadRequest().
474+
WithPayload(
475+
fmt.Sprintf("failed to parse receiver param: %v", err.Error()),
476+
)
477+
}
478+
}
479+
480+
rf := func(receiverFilter *regexp.Regexp) func(r *dispatch.Route) bool {
481+
return func(r *dispatch.Route) bool {
482+
receiver := r.RouteOpts.Receiver
483+
if receiverFilter != nil && !receiverFilter.MatchString(receiver) {
484+
return false
485+
}
486+
return true
487+
}
488+
}(receiverFilter)
489+
490+
if err = validateNextToken(params.NextToken); err != nil {
491+
logger.Error("Failed to parse NextToken parameter", "err", err)
492+
return alertgroupinfolist_ops.
493+
NewGetAlertGroupInfoListBadRequest().
494+
WithPayload(
495+
fmt.Sprintf("failed to parse NextToken param: %v", *params.NextToken),
496+
)
497+
}
498+
499+
if err = validateMaxResult(params.MaxResults); err != nil {
500+
logger.Error("Failed to parse MaxResults parameter", "err", err)
501+
return alertgroupinfolist_ops.
502+
NewGetAlertGroupInfoListBadRequest().
503+
WithPayload(
504+
fmt.Sprintf("failed to parse MaxResults param: %v", *params.MaxResults),
505+
)
506+
}
507+
508+
ags := api.alertGroupInfos(rf)
509+
alertGroupInfos := make([]*open_api_models.AlertGroupInfo, 0, len(ags))
510+
for _, alertGroup := range ags {
511+
512+
// Skip the aggregation group if the next token is set and hasn't arrived the nextToken item yet.
513+
if params.NextToken != nil && *params.NextToken >= alertGroup.ID {
514+
continue
515+
}
516+
517+
ag := &open_api_models.AlertGroupInfo{
518+
Receiver: &open_api_models.Receiver{Name: &alertGroup.Receiver},
519+
Labels: ModelLabelSetToAPILabelSet(alertGroup.Labels),
520+
ID: &alertGroup.ID,
521+
}
522+
alertGroupInfos = append(alertGroupInfos, ag)
523+
}
524+
525+
returnAlertGroupInfos, nextItem := AlertGroupInfoListTruncate(alertGroupInfos, params.MaxResults)
526+
527+
response := &open_api_models.AlertGroupInfoList{
528+
AlertGroupInfoList: returnAlertGroupInfos,
529+
NextToken: nextItem,
530+
}
531+
532+
return alertgroupinfolist_ops.NewGetAlertGroupInfoListOK().WithPayload(response)
533+
}
534+
456535
func (api *API) alertFilter(matchers []*labels.Matcher, silenced, inhibited, active bool) func(a *types.Alert, now time.Time) bool {
457536
return func(a *types.Alert, now time.Time) bool {
458537
if !a.EndsAt.IsZero() && a.EndsAt.Before(now) {
@@ -742,3 +821,22 @@ func getSwaggerSpec() (*loads.Document, *analysis.Spec, error) {
742821
swaggerSpecAnalysisCache = analysis.New(swaggerSpec.Spec())
743822
return swaggerSpec, swaggerSpecAnalysisCache, nil
744823
}
824+
825+
func validateMaxResult(maxItem *int64) error {
826+
if maxItem != nil {
827+
if *maxItem < 0 {
828+
return errors.New("the maxItem need to be larger than or equal to 0")
829+
}
830+
}
831+
return nil
832+
}
833+
834+
func validateNextToken(nextToken *string) error {
835+
if nextToken != nil {
836+
match, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", *nextToken)
837+
if !match {
838+
return fmt.Errorf("invalid nextToken: %s", *nextToken)
839+
}
840+
}
841+
return nil
842+
}

api/v2/api_test.go

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@ import (
2323
"strconv"
2424
"testing"
2525
"time"
26-
"github.com/prometheus/alertmanager/dispatch"
27-
alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert"
28-
alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup"
29-
"github.com/prometheus/alertmanager/util/callback"
26+
3027
"github.com/go-openapi/runtime"
3128
"github.com/go-openapi/runtime/middleware"
3229
"github.com/go-openapi/strfmt"
3330
"github.com/prometheus/common/model"
3431
"github.com/prometheus/common/promslog"
3532
"github.com/stretchr/testify/require"
3633

34+
alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert"
35+
alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup"
36+
alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"
37+
"github.com/prometheus/alertmanager/dispatch"
38+
"github.com/prometheus/alertmanager/util/callback"
39+
3740
open_api_models "github.com/prometheus/alertmanager/api/v2/models"
3841
general_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/general"
3942
receiver_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver"
@@ -124,6 +127,138 @@ func gettableSilence(id, state string,
124127
}
125128
}
126129

130+
func convertIntToPointerInt64(x int64) *int64 {
131+
return &x
132+
}
133+
134+
func convertStringToPointer(x string) *string {
135+
return &x
136+
}
137+
138+
func TestGetAlertGroupInfosHandler(t *testing.T) {
139+
aginfos := dispatch.AlertGroupInfos{
140+
&dispatch.AlertGroupInfo{
141+
Labels: model.LabelSet{
142+
"alertname": "TestingAlert",
143+
"service": "api",
144+
},
145+
Receiver: "testing",
146+
ID: "478b4114226224a35910d449fdba8186ebfb441f",
147+
},
148+
&dispatch.AlertGroupInfo{
149+
Labels: model.LabelSet{
150+
"alertname": "HighErrorRate",
151+
"service": "api",
152+
"cluster": "bb",
153+
},
154+
Receiver: "prod",
155+
ID: "7f4084a078a3fe29d6de82fad15af8f1411e803f",
156+
},
157+
&dispatch.AlertGroupInfo{
158+
Labels: model.LabelSet{
159+
"alertname": "OtherAlert",
160+
},
161+
Receiver: "prod",
162+
ID: "d525244929240cbdb75a497913c1890ab8de1962",
163+
},
164+
&dispatch.AlertGroupInfo{
165+
Labels: model.LabelSet{
166+
"alertname": "HighErrorRate",
167+
"service": "api",
168+
"cluster": "aa",
169+
},
170+
Receiver: "prod",
171+
ID: "d73984d43949112ae1ea59dcc5af4af7b630a5b1",
172+
},
173+
}
174+
for _, tc := range []struct {
175+
maxResult *int64
176+
nextToken *string
177+
body string
178+
expectedCode int
179+
}{
180+
// Invalid next token.
181+
{
182+
convertIntToPointerInt64(int64(1)),
183+
convertStringToPointer("$$$"),
184+
`failed to parse NextToken param: $$$`,
185+
400,
186+
},
187+
// Invalid next token.
188+
{
189+
convertIntToPointerInt64(int64(1)),
190+
convertStringToPointer("1234s"),
191+
`failed to parse NextToken param: 1234s`,
192+
400,
193+
},
194+
// Invalid MaxResults.
195+
{
196+
convertIntToPointerInt64(int64(-1)),
197+
convertStringToPointer("478b4114226224a35910d449fdba8186ebfb441f"),
198+
`failed to parse MaxResults param: -1`,
199+
400,
200+
},
201+
// One item to return, no next token.
202+
{
203+
convertIntToPointerInt64(int64(1)),
204+
nil,
205+
`{"alertGroupInfoList":[{"id":"478b4114226224a35910d449fdba8186ebfb441f","labels":{"alertname":"TestingAlert","service":"api"},"receiver":{"name":"testing"}}],"nextToken":"478b4114226224a35910d449fdba8186ebfb441f"}`,
206+
200,
207+
},
208+
// One item to return, has next token.
209+
{
210+
convertIntToPointerInt64(int64(1)),
211+
convertStringToPointer("478b4114226224a35910d449fdba8186ebfb441f"),
212+
`{"alertGroupInfoList":[{"id":"7f4084a078a3fe29d6de82fad15af8f1411e803f","labels":{"alertname":"HighErrorRate","cluster":"bb","service":"api"},"receiver":{"name":"prod"}}],"nextToken":"7f4084a078a3fe29d6de82fad15af8f1411e803f"}`,
213+
200,
214+
},
215+
// Five item to return, has next token.
216+
{
217+
convertIntToPointerInt64(int64(5)),
218+
convertStringToPointer("7f4084a078a3fe29d6de82fad15af8f1411e803f"),
219+
`{"alertGroupInfoList":[{"id":"d525244929240cbdb75a497913c1890ab8de1962","labels":{"alertname":"OtherAlert"},"receiver":{"name":"prod"}},{"id":"d73984d43949112ae1ea59dcc5af4af7b630a5b1","labels":{"alertname":"HighErrorRate","cluster":"aa","service":"api"},"receiver":{"name":"prod"}}]}`,
220+
200,
221+
},
222+
// Return all results.
223+
{
224+
nil,
225+
nil,
226+
`{"alertGroupInfoList":[{"id":"478b4114226224a35910d449fdba8186ebfb441f","labels":{"alertname":"TestingAlert","service":"api"},"receiver":{"name":"testing"}},{"id":"7f4084a078a3fe29d6de82fad15af8f1411e803f","labels":{"alertname":"HighErrorRate","cluster":"bb","service":"api"},"receiver":{"name":"prod"}},{"id":"d525244929240cbdb75a497913c1890ab8de1962","labels":{"alertname":"OtherAlert"},"receiver":{"name":"prod"}},{"id":"d73984d43949112ae1ea59dcc5af4af7b630a5b1","labels":{"alertname":"HighErrorRate","cluster":"aa","service":"api"},"receiver":{"name":"prod"}}]}`,
227+
200,
228+
},
229+
// return 0 result
230+
{
231+
convertIntToPointerInt64(int64(0)),
232+
nil,
233+
`{"alertGroupInfoList":[]}`,
234+
200,
235+
},
236+
} {
237+
api := API{
238+
uptime: time.Now(),
239+
alertGroupInfos: func(f func(*dispatch.Route) bool) dispatch.AlertGroupInfos {
240+
return aginfos
241+
},
242+
logger: log.NewNopLogger(),
243+
}
244+
r, err := http.NewRequest("GET", "/api/v2/alertgroups", nil)
245+
require.NoError(t, err)
246+
247+
w := httptest.NewRecorder()
248+
p := runtime.TextProducer()
249+
responder := api.getAlertGroupInfoListHandler(alertgroupinfolist_ops.GetAlertGroupInfoListParams{
250+
MaxResults: tc.maxResult,
251+
NextToken: tc.nextToken,
252+
HTTPRequest: r,
253+
})
254+
responder.WriteResponse(w, p)
255+
body, _ := io.ReadAll(w.Result().Body)
256+
257+
require.Equal(t, tc.expectedCode, w.Code)
258+
require.Equal(t, tc.body, string(body))
259+
}
260+
}
261+
127262
func TestGetSilencesHandler(t *testing.T) {
128263
updateTime := "2019-01-01T12:00:00+00:00"
129264
silences := []*open_api_models.GettableSilence{

0 commit comments

Comments
 (0)