Skip to content

Commit

Permalink
Add the alert group info API
Browse files Browse the repository at this point in the history
  • Loading branch information
qinxx108 committed Apr 9, 2024
1 parent 26e1f94 commit 248bfd9
Show file tree
Hide file tree
Showing 20 changed files with 2,145 additions and 22 deletions.
5 changes: 4 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,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
// APICallback define the callback function that each api call will perform before returned.
APICallback callback.Callback
}
Expand Down Expand Up @@ -118,6 +120,7 @@ func New(opts Options) (*API, error) {
v2, err := apiv2.NewAPI(
opts.Alerts,
opts.GroupFunc,
opts.GroupInfoFunc,
opts.StatusFunc,
opts.Silences,
opts.APICallback,
Expand Down
132 changes: 115 additions & 17 deletions api/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"sync"
"time"

"github.com/prometheus/alertmanager/util/callback"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/go-openapi/analysis"
Expand All @@ -34,6 +33,9 @@ import (
"github.com/prometheus/common/version"
"github.com/rs/cors"

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

"github.com/prometheus/alertmanager/api/metrics"
open_api_models "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/api/v2/restapi"
Expand All @@ -56,13 +58,14 @@ import (

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

// mtx protects alertmanagerConfig, setAlertStatus and route.
mtx sync.RWMutex
Expand All @@ -80,6 +83,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 @@ -88,6 +92,7 @@ type (
func NewAPI(
alerts provider.Alerts,
gf groupsFn,
gif groupInfosFn,
sf getAlertStatusFn,
silences *silence.Silences,
apiCallback callback.Callback,
Expand All @@ -99,15 +104,16 @@ func NewAPI(
apiCallback = callback.NoopAPICallback{}
}
api := API{
alerts: alerts,
getAlertStatus: sf,
alertGroups: gf,
peer: peer,
silences: silences,
apiCallback: apiCallback,
logger: l,
m: metrics.NewAlerts(r),
uptime: time.Now(),
alerts: alerts,
getAlertStatus: sf,
alertGroups: gf,
alertGroupInfos: gif,
peer: peer,
silences: silences,
apiCallback: apiCallback,
logger: l,
m: metrics.NewAlerts(r),
uptime: time.Now(),
}

// Load embedded swagger file.
Expand All @@ -131,6 +137,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 @@ -445,6 +452,78 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
return alertgroup_ops.NewGetAlertGroupsOK().WithPayload(callbackRes)
}

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) {
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 @@ -735,3 +814,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 {
if nextToken != nil {
match, _ := regexp.MatchString("^[a-fA-F0-9]{40}$", *nextToken)
if !match {
return fmt.Errorf("invalid nextToken: %s", *nextToken)
}
}
return nil
}
143 changes: 139 additions & 4 deletions api/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ import (
"strconv"
"testing"
"time"
"github.com/prometheus/alertmanager/dispatch"
alert_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alert"
alertgroup_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup"
"github.com/prometheus/alertmanager/util/callback"

"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"

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"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/util/callback"

open_api_models "github.com/prometheus/alertmanager/api/v2/models"
general_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/general"
receiver_ops "github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver"
Expand Down Expand Up @@ -124,6 +127,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

0 comments on commit 248bfd9

Please sign in to comment.