diff --git a/go.mod b/go.mod index 733f1495ba5..2ece3db1244 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gosimple/slug v1.9.0 - github.com/grafana/alerting-api v0.0.0-20210405171311-97906879c771 + github.com/grafana/alerting-api v0.0.0-20210407150830-64bd267999d1 github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-live-sdk v0.0.4 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 diff --git a/go.sum b/go.sum index aeb509dbd5e..c805ae6e656 100644 --- a/go.sum +++ b/go.sum @@ -809,10 +809,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= -github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb h1:Hj25Whc/TRv0hSLm5VN0FJ5R4yZ6M4ycRcBgu7bsEAc= -github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= -github.com/grafana/alerting-api v0.0.0-20210405171311-97906879c771 h1:CTmKHUu2n0O9fPTSXb+s5FO8Em9Atw57Z7mvw7lt6IM= -github.com/grafana/alerting-api v0.0.0-20210405171311-97906879c771/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= +github.com/grafana/alerting-api v0.0.0-20210407150830-64bd267999d1 h1:pbG8BsRHezUvUjMxwq+uZsx1ZMEQsfSj26KSd/H3A9g= +github.com/grafana/alerting-api v0.0.0-20210407150830-64bd267999d1/go.mod h1:Ce2PwraBlFMa+P0ArBzubfB/BXZV35mfYWQjM8C/BSE= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/grafana/grafana v1.9.2-0.20210308201921-4ce0a49eac03/go.mod h1:AHRRvd4utJGY25J5nW8aL7wZzn/LcJ0z2za9oOp14j4= @@ -1440,7 +1438,6 @@ github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17 h1:VN3p3Nb github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA= github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY= github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM= github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 3a953c86688..7805db676d3 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -30,11 +30,18 @@ import ( var timeNow = time.Now type Alertmanager interface { + // Configuration ApplyConfig(config *apimodels.PostableUserConfig) error + + // Silences CreateSilence(ps *apimodels.PostableSilence) (string, error) DeleteSilence(silenceID string) error GetSilence(silenceID string) (apimodels.GettableSilence, error) - ListSilences(filters []string) (apimodels.GettableSilences, error) + ListSilences(filter []string) (apimodels.GettableSilences, error) + + // Alerts + GetAlerts(active, silenced, inhibited bool, filter []string, receiver string) (apimodels.GettableAlerts, error) + GetAlertGroups(active, silenced, inhibited bool, filter []string, receiver string) (apimodels.AlertGroups, error) } // API handlers. diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index a0c857ab4c2..453b13d1ed3 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -4,11 +4,9 @@ import ( "errors" "fmt" "net/http" - "time" "gopkg.in/yaml.v3" - "github.com/go-openapi/strfmt" apimodels "github.com/grafana/alerting-api/pkg/api" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" @@ -17,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" - amv2 "github.com/prometheus/alertmanager/api/v2/models" ) type AlertmanagerSrv struct { @@ -118,193 +115,41 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response } func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - srv.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) - now := time.Now() - result := apimodels.AlertGroups{ - &amv2.AlertGroup{ - Alerts: []*amv2.GettableAlert{ - { - Annotations: amv2.LabelSet{ - "annotation1-1": "value1", - "annotation1-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 1"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 1-1"), - }, - { - Name: stringPtr("receiver identifier 1-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 1"}, - SilencedBy: []string{"silencedBy 1"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - }, - }, - { - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, - }, - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - Receiver: &amv2.Receiver{ - Name: stringPtr("receiver identifier 2-1"), - }, - }, - &amv2.AlertGroup{ - Alerts: []*amv2.GettableAlert{ - { - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, - }, - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - Receiver: &amv2.Receiver{ - Name: stringPtr("receiver identifier 2-1"), - }, - }, + groups, err := srv.am.GetAlertGroups( + c.QueryBool("active"), + c.QueryBool("silenced"), + c.QueryBool("inhibited"), + c.QueryStrings("filter"), + c.Query("receiver"), + ) + if err != nil { + if errors.Is(err, notifier.ErrGetAlertGroupsBadPayload) { + return response.Error(http.StatusBadRequest, err.Error(), nil) + } + // any other error here should be an unexpected failure and thus an internal error + return response.Error(http.StatusInternalServerError, err.Error(), nil) } - return response.JSON(http.StatusOK, result) + + return response.JSON(http.StatusOK, groups) } func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - srv.log.Info("RouteGetAMAlerts: ", "Recipient", recipient) - now := time.Now() - result := apimodels.GettableAlerts{ - &amv2.GettableAlert{ - Annotations: amv2.LabelSet{ - "annotation1-1": "value1", - "annotation1-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 1"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 1-1"), - }, - { - Name: stringPtr("receiver identifier 1-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 1"}, - SilencedBy: []string{"silencedBy 1"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - }, - }, - &amv2.GettableAlert{ - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, + alerts, err := srv.am.GetAlerts( + c.QueryBool("active"), + c.QueryBool("silenced"), + c.QueryBool("inhibited"), + c.QueryStrings("filter"), + c.Query("receiver"), + ) + if err != nil { + if errors.Is(err, notifier.ErrGetAlertsBadPayload) { + return response.Error(http.StatusBadRequest, err.Error(), nil) + } + // any other error here should be an unexpected failure and thus an internal error + return response.Error(http.StatusInternalServerError, err.Error(), nil) } - return response.JSON(http.StatusOK, result) + + return response.JSON(http.StatusOK, alerts) } func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext) response.Response { @@ -321,8 +166,7 @@ func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext) response.Respon } func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Response { - filters := c.QueryStrings("Filter") - gettableSilences, err := srv.am.ListSilences(filters) + gettableSilences, err := srv.am.ListSilences(c.QueryStrings("filter")) if err != nil { if errors.Is(err, notifier.ErrListSilencesBadPayload) { return response.Error(http.StatusBadRequest, err.Error(), nil) diff --git a/pkg/services/ngalert/api/test-data/am-alertmanager-recipient.http b/pkg/services/ngalert/api/test-data/am-alertmanager-recipient.http index 6ca904b2837..fa0e46545d6 100644 --- a/pkg/services/ngalert/api/test-data/am-alertmanager-recipient.http +++ b/pkg/services/ngalert/api/test-data/am-alertmanager-recipient.http @@ -97,6 +97,10 @@ content-type: application/json # get AM alerts GET http://admin:admin@localhost:3000/api/alertmanager/{{alertManagerDatasourceID}}/api/v2/alerts +### +# get AM alert groups +GET http://admin:admin@localhost:3000/alertmanager/{{alertManagerDatasourceID}}/api/v2/alerts/groups + ### # get silences - no silences GET http://admin:admin@localhost:3000/api/alertmanager/{{alertManagerDatasourceID}}/api/v2/silences?Filter=foo="bar"&Filter=bar="foo" @@ -164,4 +168,4 @@ DELETE http://admin:admin@localhost:3000/api/alertmanager/{{alertManagerDatasour ### # delete silence - unknown -DELETE http://admin:admin@localhost:3000/api/alertmanager/{{alertManagerDatasourceID}}/api/v2/silence/unknown \ No newline at end of file +DELETE http://admin:admin@localhost:3000/api/alertmanager/{{alertManagerDatasourceID}}/api/v2/silence/unknown diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 151e9dc7278..bf5e0c9a56d 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" - "github.com/go-openapi/strfmt" apimodels "github.com/grafana/alerting-api/pkg/api" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" @@ -30,14 +29,6 @@ func toMacaronPath(path string) string { })) } -func timePtr(t strfmt.DateTime) *strfmt.DateTime { - return &t -} - -func stringPtr(s string) *string { - return &s -} - func backendType(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { recipient := ctx.Params("Recipient") if recipient == apimodels.GrafanaBackend.String() { diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 3a7cea81d0f..95161cb40e5 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -48,17 +48,18 @@ type Alertmanager struct { // notificationLog keeps tracks of which notifications we've fired already. notificationLog *nflog.Log // silences keeps the track of which notifications we should not fire due to user configuration. - silences *silence.Silences - marker types.Marker - alerts *AlertProvider - + silencer *silence.Silencer + silences *silence.Silences + marker types.Marker + alerts *AlertProvider + route *dispatch.Route dispatcher *dispatch.Dispatcher dispatcherWG sync.WaitGroup stageMetrics *notify.Metrics dispatcherMetrics *dispatch.DispatcherMetrics - reloadConfigMtx sync.Mutex + reloadConfigMtx sync.RWMutex } func init() { @@ -194,7 +195,8 @@ func (am *Alertmanager) applyConfig(cfg *api.PostableUserConfig) error { // Now, let's put together our notification pipeline routingStage := make(notify.RoutingStage, len(integrationsMap)) - silencingStage := notify.NewMuteStage(silence.NewSilencer(am.silences, am.marker, gokit_log.NewNopLogger())) + am.silencer = silence.NewSilencer(am.silences, am.marker, gokit_log.NewNopLogger()) + silencingStage := notify.NewMuteStage(am.silencer) for name := range integrationsMap { stage := am.createReceiverStage(name, integrationsMap[name], waitFunc, am.notificationLog) routingStage[name] = notify.MultiStage{silencingStage, stage} @@ -203,9 +205,8 @@ func (am *Alertmanager) applyConfig(cfg *api.PostableUserConfig) error { am.alerts.SetStage(routingStage) am.StopAndWait() - //TODO: Verify this is correct - route := dispatch.NewRoute(cfg.AlertmanagerConfig.Route, nil) - am.dispatcher = dispatch.NewDispatcher(am.alerts, route, routingStage, am.marker, timeoutFunc, gokit_log.NewNopLogger(), am.dispatcherMetrics) + am.route = dispatch.NewRoute(cfg.AlertmanagerConfig.Route, nil) + am.dispatcher = dispatch.NewDispatcher(am.alerts, am.route, routingStage, am.marker, timeoutFunc, gokit_log.NewNopLogger(), am.dispatcherMetrics) am.dispatcherWG.Add(1) go func() { @@ -285,7 +286,6 @@ func (am *Alertmanager) createReceiverStage(name string, integrations []notify.I var s notify.MultiStage s = append(s, notify.NewWaitStage(wait)) s = append(s, notify.NewDedupStage(&integrations[i], notificationLog, recv)) - //TODO: This probably won't work w/o the metrics s = append(s, notify.NewRetryStage(integrations[i], name, am.stageMetrics)) s = append(s, notify.NewSetNotifiesStage(notificationLog, recv)) diff --git a/pkg/services/ngalert/notifier/alerts.go b/pkg/services/ngalert/notifier/alerts.go new file mode 100644 index 00000000000..a016ee26aec --- /dev/null +++ b/pkg/services/ngalert/notifier/alerts.go @@ -0,0 +1,222 @@ +package notifier + +import ( + "regexp" + "sort" + "time" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/pkg/errors" + v2 "github.com/prometheus/alertmanager/api/v2" + "github.com/prometheus/alertmanager/dispatch" + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/alertmanager/types" + prometheus_model "github.com/prometheus/common/model" +) + +var ( + ErrGetAlertsInternal = errors.New("unable to retrieve alerts(s) due to an internal error") + ErrGetAlertsBadPayload = errors.New("unable to retrieve alerts") + ErrGetAlertGroupsBadPayload = errors.New("unable to retrieve alerts groups") +) + +func (am *Alertmanager) GetAlerts(active, silenced, inhibited bool, filter []string, receivers string) (apimodels.GettableAlerts, error) { + var ( + // Initialize result slice to prevent api returning `null` when there + // are no alerts present + res = apimodels.GettableAlerts{} + ) + + matchers, err := parseFilter(filter) + if err != nil { + am.logger.Error("failed to parse matchers", "err", err) + return nil, errors.Wrap(ErrGetAlertsBadPayload, err.Error()) + } + + receiverFilter, err := parseReceivers(receivers) + if err != nil { + am.logger.Error("failed to parse receiver regex", "err", err) + return nil, errors.Wrap(ErrGetAlertsBadPayload, err.Error()) + } + + alerts := am.alerts.GetPending() + defer alerts.Close() + + alertFilter := am.alertFilter(matchers, silenced, inhibited, active) + now := time.Now() + + am.reloadConfigMtx.RLock() + for a := range alerts.Next() { + if err = alerts.Err(); err != nil { + break + } + + routes := am.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 := v2.AlertToOpenAPIAlert(a, am.marker.Status(a.Fingerprint()), receivers) + + res = append(res, alert) + } + am.reloadConfigMtx.RUnlock() + + if err != nil { + am.logger.Error("failed to iterate through the alerts", "err", err) + return nil, errors.Wrap(ErrGetAlertsInternal, err.Error()) + } + sort.Slice(res, func(i, j int) bool { + return *res[i].Fingerprint < *res[j].Fingerprint + }) + + return res, nil +} + +func (am *Alertmanager) GetAlertGroups(active, silenced, inhibited bool, filter []string, receivers string) (apimodels.AlertGroups, error) { + matchers, err := parseFilter(filter) + if err != nil { + am.logger.Error("msg", "failed to parse matchers", "err", err) + return nil, errors.Wrap(ErrGetAlertGroupsBadPayload, err.Error()) + } + + receiverFilter, err := parseReceivers(receivers) + if err != nil { + am.logger.Error("msg", "failed to compile receiver regex", "err", err) + return nil, errors.Wrap(ErrGetAlertGroupsBadPayload, 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) + + af := am.alertFilter(matchers, silenced, inhibited, active) + alertGroups, allReceivers := am.dispatcher.Groups(rf, af) + + res := make(apimodels.AlertGroups, 0, len(alertGroups)) + + for _, alertGroup := range alertGroups { + ag := &apimodels.AlertGroup{ + Receiver: &apimodels.Receiver{Name: &alertGroup.Receiver}, + Labels: v2.ModelLabelSetToAPILabelSet(alertGroup.Labels), + Alerts: make([]*apimodels.GettableAlert, 0, len(alertGroup.Alerts)), + } + + for _, alert := range alertGroup.Alerts { + fp := alert.Fingerprint() + receivers := allReceivers[fp] + status := am.marker.Status(fp) + apiAlert := v2.AlertToOpenAPIAlert(alert, status, receivers) + ag.Alerts = append(ag.Alerts, apiAlert) + } + res = append(res, ag) + } + + return res, nil +} + +func (am *Alertmanager) 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) { + return false + } + + // Set alert's current status based on its label set. + am.silencer.Mutes(a.Labels) + + // Get alert's current status after seeing if it is suppressed. + status := am.marker.Status(a.Fingerprint()) + + if !active && status.State == types.AlertStateActive { + return false + } + + if !silenced && len(status.SilencedBy) != 0 { + return false + } + + if !inhibited && len(status.InhibitedBy) != 0 { + return false + } + + return alertMatchesFilterLabels(&a.Alert, matchers) + } +} + +func alertMatchesFilterLabels(a *prometheus_model.Alert, matchers []*labels.Matcher) bool { + sms := make(map[string]string) + for name, value := range a.Labels { + sms[string(name)] = string(value) + } + return matchFilterLabels(matchers, sms) +} + +func matchFilterLabels(matchers []*labels.Matcher, sms map[string]string) bool { + for _, m := range matchers { + v, prs := sms[m.Name] + switch m.Type { + case labels.MatchNotRegexp, labels.MatchNotEqual: + if m.Value == "" && prs { + continue + } + if !m.Matches(v) { + return false + } + default: + if m.Value == "" && !prs { + continue + } + if !m.Matches(v) { + return false + } + } + } + + return true +} + +func parseReceivers(receivers string) (*regexp.Regexp, error) { + if receivers == "" { + return nil, nil + } + + return regexp.Compile("^(?:" + receivers + ")$") +} + +func parseFilter(filter []string) ([]*labels.Matcher, error) { + matchers := make([]*labels.Matcher, 0, len(filter)) + for _, matcherString := range filter { + matcher, err := labels.ParseMatcher(matcherString) + if err != nil { + return nil, err + } + + matchers = append(matchers, matcher) + } + return matchers, nil +} + +func receiversMatchFilter(receivers []string, filter *regexp.Regexp) bool { + for _, r := range receivers { + if filter.MatchString(r) { + return true + } + } + + return false +} diff --git a/pkg/services/ngalert/notifier/silences.go b/pkg/services/ngalert/notifier/silences.go index 2ee192d5fa3..fe9e851137d 100644 --- a/pkg/services/ngalert/notifier/silences.go +++ b/pkg/services/ngalert/notifier/silences.go @@ -7,7 +7,6 @@ import ( apimodels "github.com/grafana/alerting-api/pkg/api" "github.com/pkg/errors" v2 "github.com/prometheus/alertmanager/api/v2" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/silence" ) @@ -20,16 +19,11 @@ var ( ) // ListSilences retrieves a list of stored silences. It supports a set of labels as filters. -func (am *Alertmanager) ListSilences(filters []string) (apimodels.GettableSilences, error) { - matchers := []*labels.Matcher{} - for _, matcherString := range filters { - matcher, err := labels.ParseMatcher(matcherString) - if err != nil { - am.logger.Error("failed to parse matcher", "err", err, "matcher", matcherString) - return nil, errors.Wrap(ErrListSilencesBadPayload, err.Error()) - } - - matchers = append(matchers, matcher) +func (am *Alertmanager) ListSilences(filter []string) (apimodels.GettableSilences, error) { + matchers, err := parseFilter(filter) + if err != nil { + am.logger.Error("failed to parse matchers", "err", err) + return nil, errors.Wrap(ErrListSilencesBadPayload, err.Error()) } psils, _, err := am.silences.Query() diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go new file mode 100644 index 00000000000..c6a7601b13f --- /dev/null +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -0,0 +1,132 @@ +package alerting + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/stretchr/testify/require" +) + +func TestAlertAndGroupsQuery(t *testing.T) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{"ngalert"}, + }) + store := setupDB(t, dir) + grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) + + // When there are no alerts available, it returns an empty list. + { + alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr) + // nolint:gosec + resp, err := http.Get(alertsURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + require.JSONEq(t, "[]", string(b)) + } + + // When are there no alerts available, it returns an empty list of groups. + { + alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr) + // nolint:gosec + resp, err := http.Get(alertsURL) + require.NoError(t, err) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + require.JSONEq(t, "[]", string(b)) + } +} + +func setupDB(t *testing.T, dir string) *sqlstore.SQLStore { + store := testinfra.SetUpDatabase(t, dir) + // Let's make sure we create a default configuration from which we can start. + err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := sess.Insert(&models.AlertConfiguration{ + ID: 1, + AlertmanagerConfiguration: AMConfigFixture, + ConfigurationVersion: "v1", + CreatedAt: time.Now(), + }) + return err + }) + require.NoError(t, err) + + return store +} + +var AMConfigFixture = ` +{ + "template_files": {}, + "alertmanager_config": { + "global": { + "resolve_timeout": "4m", + "http_config": { + "BasicAuth": null, + "Authorization": null, + "BearerToken": "", + "BearerTokenFile": "", + "ProxyURL": {}, + "TLSConfig": { + "CAFile": "", + "CertFile": "", + "KeyFile": "", + "ServerName": "", + "InsecureSkipVerify": false + }, + "FollowRedirects": true + }, + "smtp_from": "youraddress@example.org", + "smtp_hello": "localhost", + "smtp_smarthost": "localhost:25", + "smtp_require_tls": true, + "pagerduty_url": "https://events.pagerduty.com/v2/enqueue", + "opsgenie_api_url": "https://api.opsgenie.com/", + "wechat_api_url": "https://qyapi.weixin.qq.com/cgi-bin/", + "victorops_api_url": "https://alert.victorops.com/integrations/generic/20131114/alert/" + }, + "route": { + "receiver": "example-email" + }, + "templates": [], + "receivers": [ + { + "name": "example-email", + "email_configs": [ + { + "send_resolved": false, + "to": "youraddress@example.org", + "smarthost": "", + "html": "{{ template \"email.default.html\" . }}", + "tls_config": { + "CAFile": "", + "CertFile": "", + "KeyFile": "", + "ServerName": "", + "InsecureSkipVerify": false + } + } + ] + } + ] + } +} +` diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 5e419cf0bb5..445a44e0efa 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "testing" "github.com/grafana/grafana/pkg/infra/fs" @@ -195,6 +196,12 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { _, err = securitySect.NewKey("content_security_policy", "true") require.NoError(t, err) } + if len(o.EnableFeatureToggles) > 0 { + featureSection, err := cfg.NewSection("feature_toggles") + require.NoError(t, err) + _, err = featureSection.NewKey("enable", strings.Join(o.EnableFeatureToggles, " ")) + require.NoError(t, err) + } } cfgPath := filepath.Join(cfgDir, "test.ini") @@ -208,5 +215,6 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { } type GrafanaOpts struct { - EnableCSP bool + EnableCSP bool + EnableFeatureToggles []string }