Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding the alert group info API #3364

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ type Options struct {
// 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)
// 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
}

func (o Options) validate() error {
Expand Down Expand Up @@ -122,6 +125,7 @@ func New(opts Options) (*API, error) {
v2, err := apiv2.NewAPI(
opts.Alerts,
opts.GroupFunc,
opts.GroupInfoFunc,
opts.StatusFunc,
opts.Silences,
opts.Peer,
Expand Down
127 changes: 113 additions & 14 deletions api/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
package v2

import (
"errors"
"fmt"
"net/http"
"regexp"
"sort"
"sync"
"time"

alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/go-openapi/analysis"
Expand Down Expand Up @@ -53,12 +56,13 @@ import (

// API represents an Alertmanager API v2
type API struct {
peer cluster.ClusterPeer
silences *silence.Silences
alerts provider.Alerts
alertGroups groupsFn
getAlertStatus getAlertStatusFn
uptime time.Time
peer cluster.ClusterPeer
silences *silence.Silences
alerts provider.Alerts
alertGroups groupsFn
alertGroupInfos groupInfosFn
getAlertStatus getAlertStatusFn
uptime time.Time

// mtx protects alertmanagerConfig, setAlertStatus and route.
mtx sync.RWMutex
Expand All @@ -76,6 +80,7 @@ type API struct {

type (
groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) 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)
)
Expand All @@ -84,21 +89,23 @@ type (
func NewAPI(
alerts provider.Alerts,
gf groupsFn,
gif groupInfosFn,
sf getAlertStatusFn,
silences *silence.Silences,
peer cluster.ClusterPeer,
l log.Logger,
r prometheus.Registerer,
) (*API, error) {
api := API{
alerts: alerts,
getAlertStatus: sf,
alertGroups: gf,
peer: peer,
silences: silences,
logger: l,
m: metrics.NewAlerts("v2", r),
uptime: time.Now(),
alerts: alerts,
getAlertStatus: sf,
alertGroups: gf,
alertGroupInfos: gif,
peer: peer,
silences: silences,
logger: l,
m: metrics.NewAlerts("v2", r),
uptime: time.Now(),
}

// Load embedded swagger file.
Expand All @@ -122,6 +129,7 @@ func NewAPI(
openAPI.AlertGetAlertsHandler = alert_ops.GetAlertsHandlerFunc(api.getAlertsHandler)
openAPI.AlertPostAlertsHandler = alert_ops.PostAlertsHandlerFunc(api.postAlertsHandler)
openAPI.AlertgroupGetAlertGroupsHandler = alertgroup_ops.GetAlertGroupsHandlerFunc(api.getAlertGroupsHandler)
openAPI.AlertgroupinfolistGetAlertGroupInfoListHandler = alertgroupinfolist_ops.GetAlertGroupInfoListHandlerFunc(api.getAlertGroupInfoListHandler)
openAPI.GeneralGetStatusHandler = general_ops.GetStatusHandlerFunc(api.getStatusHandler)
openAPI.ReceiverGetReceiversHandler = receiver_ops.GetReceiversHandlerFunc(api.getReceiversHandler)
openAPI.SilenceDeleteSilenceHandler = silence_ops.DeleteSilenceHandlerFunc(api.deleteSilenceHandler)
Expand Down Expand Up @@ -424,6 +432,78 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
return alertgroup_ops.NewGetAlertGroupsOK().WithPayload(res)
}

func (api *API) getAlertGroupInfoListHandler(params alertgroupinfolist_ops.GetAlertGroupInfoListParams) middleware.Responder {
logger := api.requestLogger(params.HTTPRequest)

var receiverFilter *regexp.Regexp
var err error
if params.Receiver != nil {
receiverFilter, err = regexp.Compile("^(?:" + *params.Receiver + ")$")
if err != nil {
level.Error(logger).Log("msg", "Failed to compile receiver regex", "err", err)
return alertgroupinfolist_ops.
NewGetAlertGroupInfoListBadRequest().
WithPayload(
fmt.Sprintf("failed to parse receiver param: %v", err.Error()),
)
}
}

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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, do we need to use regex here? That means the input params.Receiver contains multiple receivers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more about support regex search for receivers, i am following convention of receiver filter parameter in the other apis
See https://github.com/prometheus/alertmanager/blob/main/api/v2/api.go#L250-L260

return false
}
return true
}
}(receiverFilter)

if err = validateNextToken(params.NextToken); err != nil {
level.Error(logger).Log("msg", "Failed to parse NextToken parameter", "err", err)
return alertgroupinfolist_ops.
NewGetAlertGroupInfoListBadRequest().
WithPayload(
fmt.Sprintf("failed to parse NextToken param: %v", *params.NextToken),
)
}

if err = validateMaxResult(params.MaxResults); err != nil {
level.Error(logger).Log("msg", "Failed to parse MaxResults parameter", "err", err)
return alertgroupinfolist_ops.
NewGetAlertGroupInfoListBadRequest().
WithPayload(
fmt.Sprintf("failed to parse MaxResults param: %v", *params.MaxResults),
)
}

ags := api.alertGroupInfos(rf)
alertGroupInfos := make([]*open_api_models.AlertGroupInfo, 0, len(ags))
for _, alertGroup := range ags {

// Skip the aggregation group if the next token is set and hasn't arrived the nextToken item yet.
if params.NextToken != nil && *params.NextToken >= alertGroup.ID {
continue
}

ag := &open_api_models.AlertGroupInfo{
Receiver: &open_api_models.Receiver{Name: &alertGroup.Receiver},
Labels: ModelLabelSetToAPILabelSet(alertGroup.Labels),
ID: &alertGroup.ID,
}
alertGroupInfos = append(alertGroupInfos, ag)
}

returnAlertGroupInfos, nextItem := AlertGroupInfoListTruncate(alertGroupInfos, params.MaxResults)

response := &open_api_models.AlertGroupInfoList{
AlertGroupInfoList: returnAlertGroupInfos,
NextToken: nextItem,
}

return alertgroupinfolist_ops.NewGetAlertGroupInfoListOK().WithPayload(response)
}

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) {
Expand Down Expand Up @@ -721,3 +801,22 @@ func getSwaggerSpec() (*loads.Document, *analysis.Spec, error) {
swaggerSpecAnalysisCache = analysis.New(swaggerSpec.Spec())
return swaggerSpec, swaggerSpecAnalysisCache, nil
}

func validateMaxResult(maxItem *int64) error {
if maxItem != nil {
if *maxItem < 0 {
return errors.New("the maxItem need to be larger than or equal to 0")
}
}
return nil
}

func validateNextToken(nextToken *string) error {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use string here as param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to escape the validation when nextToken is nil, kind of want to keep that if into this function rather in main function

if nextToken != nil {
match, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", *nextToken)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's compile the regex once and match here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the nextToken != ""

if !match {
return fmt.Errorf("invalid nextToken: %s", *nextToken)
}
}
return nil
}
135 changes: 135 additions & 0 deletions api/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (
"testing"
"time"

alertgroupinfolist_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroupinfolist"
"github.com/prometheus/alertmanager/dispatch"

"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/prometheus/common/model"
Expand Down Expand Up @@ -120,6 +123,138 @@ 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{
Labels: model.LabelSet{
"alertname": "TestingAlert",
"service": "api",
},
Receiver: "testing",
ID: "478b4114226224a35910d449fdba8186ebfb441f",
},
&dispatch.AlertGroupInfo{
Labels: model.LabelSet{
"alertname": "HighErrorRate",
"service": "api",
"cluster": "bb",
},
Receiver: "prod",
ID: "7f4084a078a3fe29d6de82fad15af8f1411e803f",
},
&dispatch.AlertGroupInfo{
Labels: model.LabelSet{
"alertname": "OtherAlert",
},
Receiver: "prod",
ID: "d525244929240cbdb75a497913c1890ab8de1962",
},
&dispatch.AlertGroupInfo{
Labels: model.LabelSet{
"alertname": "HighErrorRate",
"service": "api",
"cluster": "aa",
},
Receiver: "prod",
ID: "d73984d43949112ae1ea59dcc5af4af7b630a5b1",
},
}
for _, tc := range []struct {
maxResult *int64
nextToken *string
body string
expectedCode int
}{
// Invalid next token.
{
convertIntToPointerInt64(int64(1)),
convertStringToPointer("$$$"),
`failed to parse NextToken param: $$$`,
400,
},
// Invalid next token.
{
convertIntToPointerInt64(int64(1)),
convertStringToPointer("1234s"),
`failed to parse NextToken param: 1234s`,
400,
},
// Invalid MaxResults.
{
convertIntToPointerInt64(int64(-1)),
convertStringToPointer("478b4114226224a35910d449fdba8186ebfb441f"),
`failed to parse MaxResults param: -1`,
400,
},
// One item to return, no next token.
{
convertIntToPointerInt64(int64(1)),
nil,
`{"alertGroupInfoList":[{"id":"478b4114226224a35910d449fdba8186ebfb441f","labels":{"alertname":"TestingAlert","service":"api"},"receiver":{"name":"testing"}}],"nextToken":"478b4114226224a35910d449fdba8186ebfb441f"}`,
200,
},
// One item to return, has next token.
{
convertIntToPointerInt64(int64(1)),
convertStringToPointer("478b4114226224a35910d449fdba8186ebfb441f"),
`{"alertGroupInfoList":[{"id":"7f4084a078a3fe29d6de82fad15af8f1411e803f","labels":{"alertname":"HighErrorRate","cluster":"bb","service":"api"},"receiver":{"name":"prod"}}],"nextToken":"7f4084a078a3fe29d6de82fad15af8f1411e803f"}`,
200,
},
// Five item to return, has next token.
{
convertIntToPointerInt64(int64(5)),
convertStringToPointer("7f4084a078a3fe29d6de82fad15af8f1411e803f"),
`{"alertGroupInfoList":[{"id":"d525244929240cbdb75a497913c1890ab8de1962","labels":{"alertname":"OtherAlert"},"receiver":{"name":"prod"}},{"id":"d73984d43949112ae1ea59dcc5af4af7b630a5b1","labels":{"alertname":"HighErrorRate","cluster":"aa","service":"api"},"receiver":{"name":"prod"}}]}`,
200,
},
// Return all results.
{
nil,
nil,
`{"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"}}]}`,
200,
},
// return 0 result
{
convertIntToPointerInt64(int64(0)),
nil,
`{"alertGroupInfoList":[]}`,
200,
},
} {
api := API{
uptime: time.Now(),
alertGroupInfos: func(f func(*dispatch.Route) bool) dispatch.AlertGroupInfos {
return aginfos
},
logger: log.NewNopLogger(),
}
r, err := http.NewRequest("GET", "/api/v2/alertgroups", nil)
require.NoError(t, err)

w := httptest.NewRecorder()
p := runtime.TextProducer()
responder := api.getAlertGroupInfoListHandler(alertgroupinfolist_ops.GetAlertGroupInfoListParams{
MaxResults: tc.maxResult,
NextToken: tc.nextToken,
HTTPRequest: r,
})
responder.WriteResponse(w, p)
body, _ := io.ReadAll(w.Result().Body)

require.Equal(t, tc.expectedCode, w.Code)
require.Equal(t, tc.body, string(body))
}
}

func TestGetSilencesHandler(t *testing.T) {
updateTime := "2019-01-01T12:00:00+00:00"
silences := []*open_api_models.GettableSilence{
Expand Down
Loading