Skip to content

Commit 57e5a0a

Browse files
qinxx108alexqyle
authored andcommitted
Add the alert group info API
1 parent 49db3cf commit 57e5a0a

22 files changed

+2230
-22
lines changed

api/api.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ type Options struct {
7676
// according to the current active configuration. Alerts returned are
7777
// filtered by the arguments provided to the function.
7878
GroupFunc func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string)
79-
79+
// GroupInfoFunc returns a list of alert groups information. The alerts are grouped
80+
// according to the current active configuration. This function will not return the alerts inside each group.
81+
GroupInfoFunc func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos
8082
// APICallback define the callback function that each api call will perform before returned.
8183
APICallback callback.Callback
8284
}
@@ -127,6 +129,7 @@ func New(opts Options) (*API, error) {
127129
v2, err := apiv2.NewAPI(
128130
opts.Alerts,
129131
opts.GroupFunc,
132+
opts.GroupInfoFunc,
130133
opts.StatusFunc,
131134
opts.Silences,
132135
opts.APICallback,

api/v2/api.go

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
package v2
1515

1616
import (
17+
"errors"
1718
"fmt"
1819
"net/http"
1920
"regexp"
2021
"sort"
2122
"sync"
2223
"time"
2324

24-
"github.com/prometheus/alertmanager/util/callback"
2525
"github.com/go-kit/log"
2626
"github.com/go-kit/log/level"
2727
"github.com/go-openapi/analysis"
@@ -33,6 +33,9 @@ import (
3333
"github.com/prometheus/common/version"
3434
"github.com/rs/cors"
3535

36+
alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"
37+
"github.com/prometheus/alertmanager/util/callback"
38+
3639
"github.com/prometheus/alertmanager/api/metrics"
3740
open_api_models "github.com/prometheus/alertmanager/api/v2/models"
3841
"github.com/prometheus/alertmanager/api/v2/restapi"
@@ -54,13 +57,14 @@ import (
5457

5558
// API represents an Alertmanager API v2
5659
type API struct {
57-
peer cluster.ClusterPeer
58-
silences *silence.Silences
59-
alerts provider.Alerts
60-
alertGroups groupsFn
61-
getAlertStatus getAlertStatusFn
62-
apiCallback callback.Callback
63-
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+
apiCallback callback.Callback
67+
uptime time.Time
6468

6569
// mtx protects alertmanagerConfig, setAlertStatus and route.
6670
mtx sync.RWMutex
@@ -78,6 +82,7 @@ type API struct {
7882

7983
type (
8084
groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string)
85+
groupInfosFn func(func(*dispatch.Route) bool) dispatch.AlertGroupInfos
8186
getAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus
8287
setAlertStatusFn func(prometheus_model.LabelSet)
8388
)
@@ -86,6 +91,7 @@ type (
8691
func NewAPI(
8792
alerts provider.Alerts,
8893
gf groupsFn,
94+
gif groupInfosFn,
8995
sf getAlertStatusFn,
9096
silences *silence.Silences,
9197
apiCallback callback.Callback,
@@ -97,15 +103,16 @@ func NewAPI(
97103
apiCallback = callback.NoopAPICallback{}
98104
}
99105
api := API{
100-
alerts: alerts,
101-
getAlertStatus: sf,
102-
alertGroups: gf,
103-
peer: peer,
104-
silences: silences,
105-
apiCallback: apiCallback,
106-
logger: l,
107-
m: metrics.NewAlerts("v2", r),
108-
uptime: time.Now(),
106+
alerts: alerts,
107+
getAlertStatus: sf,
108+
alertGroups: gf,
109+
alertGroupInfos: gif,
110+
peer: peer,
111+
silences: silences,
112+
apiCallback: apiCallback,
113+
logger: l,
114+
m: metrics.NewAlerts("v2", r),
115+
uptime: time.Now(),
109116
}
110117

111118
// Load embedded swagger file.
@@ -129,6 +136,7 @@ func NewAPI(
129136
openAPI.AlertGetAlertsHandler = alert_ops.GetAlertsHandlerFunc(api.getAlertsHandler)
130137
openAPI.AlertPostAlertsHandler = alert_ops.PostAlertsHandlerFunc(api.postAlertsHandler)
131138
openAPI.AlertgroupGetAlertGroupsHandler = alertgroup_ops.GetAlertGroupsHandlerFunc(api.getAlertGroupsHandler)
139+
openAPI.AlertgroupinfolistGetAlertGroupInfoListHandler = alertgroupinfolist_ops.GetAlertGroupInfoListHandlerFunc(api.getAlertGroupInfoListHandler)
132140
openAPI.GeneralGetStatusHandler = general_ops.GetStatusHandlerFunc(api.getStatusHandler)
133141
openAPI.ReceiverGetReceiversHandler = receiver_ops.GetReceiversHandlerFunc(api.getReceiversHandler)
134142
openAPI.SilenceDeleteSilenceHandler = silence_ops.DeleteSilenceHandlerFunc(api.deleteSilenceHandler)
@@ -443,6 +451,78 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
443451
return alertgroup_ops.NewGetAlertGroupsOK().WithPayload(callbackRes)
444452
}
445453

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

api/v2/api_test.go

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ 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/strfmt"
3229
"github.com/prometheus/common/model"
3330
"github.com/stretchr/testify/require"
3431

32+
alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert"
33+
alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup"
34+
alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"
35+
"github.com/prometheus/alertmanager/dispatch"
36+
"github.com/prometheus/alertmanager/util/callback"
37+
3538
open_api_models "github.com/prometheus/alertmanager/api/v2/models"
3639
general_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/general"
3740
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)