api: support more query filters (#1366)

* api: support more query filters

This change adds 2 new query filters to the /api/v1/alerts endpoint.

- active, filter out active alerts when set to 'false' (default: 'true').
- unprocessed, filter out unprocessed alerts when set to 'false'
 (default: 'true').

The default values ensure that the API behavior remains the same as
before when the query filters aren't provided.

Signed-off-by: Simon Pasquier <spasquie@redhat.com>

* api: address comments

Signed-off-by: Simon Pasquier <spasquie@redhat.com>
This commit is contained in:
Simon Pasquier 2018-05-07 18:07:19 +02:00 committed by stuart nelson
parent 05fb09aebd
commit 383024e63d
2 changed files with 269 additions and 42 deletions

View File

@ -267,12 +267,32 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
receiverFilter *regexp.Regexp
// Initialize result slice to prevent api returning `null` when there
// are no alerts present
res = []*dispatch.APIAlert{}
matchers = []*labels.Matcher{}
showSilenced = true
showInhibited = true
res = []*dispatch.APIAlert{}
matchers = []*labels.Matcher{}
showActive, showInhibited bool
showSilenced, showUnprocessed bool
)
getBoolParam := func(name string) (bool, error) {
v := r.FormValue(name)
if v == "" {
return true, nil
}
if v == "false" {
return false, nil
}
if v != "true" {
err := fmt.Errorf("parameter %q can either be 'true' or 'false', not %q", name, v)
api.respondError(w, apiError{
typ: errorBadData,
err: err,
}, nil)
return false, err
}
return true, nil
}
if filter := r.FormValue("filter"); filter != "" {
matchers, err = parse.Matchers(filter)
if err != nil {
@ -284,34 +304,24 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
}
}
if silencedParam := r.FormValue("silenced"); silencedParam != "" {
if silencedParam == "false" {
showSilenced = false
} else if silencedParam != "true" {
api.respondError(w, apiError{
typ: errorBadData,
err: fmt.Errorf(
"parameter 'silenced' can either be 'true' or 'false', not '%v'",
silencedParam,
),
}, nil)
return
}
showActive, err = getBoolParam("active")
if err != nil {
return
}
if inhibitedParam := r.FormValue("inhibited"); inhibitedParam != "" {
if inhibitedParam == "false" {
showInhibited = false
} else if inhibitedParam != "true" {
api.respondError(w, apiError{
typ: errorBadData,
err: fmt.Errorf(
"parameter 'inhibited' can either be 'true' or 'false', not '%v'",
inhibitedParam,
),
}, nil)
return
}
showSilenced, err = getBoolParam("silenced")
if err != nil {
return
}
showInhibited, err = getBoolParam("inhibited")
if err != nil {
return
}
showUnprocessed, err = getBoolParam("unprocessed")
if err != nil {
return
}
if receiverParam := r.FormValue("receiver"); receiverParam != "" {
@ -352,13 +362,21 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
continue
}
// Continue if alert is resolved
// Continue if the alert is resolved.
if !a.Alert.EndsAt.IsZero() && a.Alert.EndsAt.Before(time.Now()) {
continue
}
status := api.getAlertStatus(a.Fingerprint())
if !showActive && status.State == types.AlertStateActive {
continue
}
if !showUnprocessed && status.State == types.AlertStateUnprocessed {
continue
}
if !showSilenced && len(status.SilencedBy) != 0 {
continue
}

View File

@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/provider"
"github.com/prometheus/alertmanager/types"
@ -20,26 +21,72 @@ import (
"github.com/stretchr/testify/require"
)
// fakeAlerts is a struct implementing the provider.Alerts interface for tests.
type fakeAlerts struct {
putWithErr bool
fps map[model.Fingerprint]int
alerts []*types.Alert
err error
}
func newEmptyIterator() provider.AlertIterator {
return provider.NewAlertIterator(make(chan *types.Alert), make(chan struct{}), nil)
func newFakeAlerts(alerts []*types.Alert, withErr bool) *fakeAlerts {
fps := make(map[model.Fingerprint]int)
for i, a := range alerts {
fps[a.Fingerprint()] = i
}
f := &fakeAlerts{
alerts: alerts,
fps: fps,
}
if withErr {
f.err = errors.New("Error occured")
}
return f
}
func (f *fakeAlerts) Subscribe() provider.AlertIterator { return newEmptyIterator() }
func (f *fakeAlerts) GetPending() provider.AlertIterator { return newEmptyIterator() }
func (f *fakeAlerts) Subscribe() provider.AlertIterator { return nil }
func (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil }
func (f *fakeAlerts) Put(alerts ...*types.Alert) error {
if f.putWithErr {
return errors.New("Error occured")
}
return nil
return f.err
}
func (f *fakeAlerts) GetPending() provider.AlertIterator {
ch := make(chan *types.Alert)
done := make(chan struct{})
go func() {
defer close(ch)
for _, a := range f.alerts {
ch <- a
}
}()
return provider.NewAlertIterator(ch, done, f.err)
}
func groupAlerts([]*labels.Matcher) dispatch.AlertOverview { return dispatch.AlertOverview{} }
func getAlertStatus(model.Fingerprint) types.AlertStatus { return types.AlertStatus{} }
func newGetAlertStatus(f *fakeAlerts) func(model.Fingerprint) types.AlertStatus {
return func(fp model.Fingerprint) types.AlertStatus {
status := types.AlertStatus{SilencedBy: []string{}, InhibitedBy: []string{}}
i, ok := f.fps[fp]
if !ok {
return status
}
alert := f.alerts[i]
switch alert.Labels["state"] {
case "active":
status.State = types.AlertStateActive
case "unprocessed":
status.State = types.AlertStateUnprocessed
case "suppressed":
status.State = types.AlertStateSuppressed
}
if alert.Labels["silenced_by"] != "" {
status.SilencedBy = append(status.SilencedBy, string(alert.Labels["silenced_by"]))
}
if alert.Labels["inhibited_by"] != "" {
status.InhibitedBy = append(status.InhibitedBy, string(alert.Labels["inhibited_by"]))
}
return status
}
}
func TestAddAlerts(t *testing.T) {
now := func(offset int) time.Time {
@ -72,7 +119,8 @@ func TestAddAlerts(t *testing.T) {
t.Errorf("Unexpected error %v", err)
}
api := New(&fakeAlerts{putWithErr: tc.err}, nil, groupAlerts, getAlertStatus, nil, nil)
alertsProvider := newFakeAlerts([]*types.Alert{}, tc.err)
api := New(alertsProvider, nil, groupAlerts, newGetAlertStatus(alertsProvider), nil, nil)
r, err := http.NewRequest("POST", "/api/v1/alerts", bytes.NewReader(b))
w := httptest.NewRecorder()
@ -88,6 +136,167 @@ func TestAddAlerts(t *testing.T) {
}
}
func TestListAlerts(t *testing.T) {
now := time.Now()
alerts := []*types.Alert{
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{"state": "active", "alertname": "alert1"},
StartsAt: now.Add(-time.Minute),
},
},
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"},
StartsAt: now.Add(-time.Minute),
},
},
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"},
StartsAt: now.Add(-time.Minute),
},
},
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"},
StartsAt: now.Add(-time.Minute),
},
},
&types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert5"},
StartsAt: now.Add(-2 * time.Minute),
EndsAt: now.Add(-time.Minute),
},
},
}
for i, tc := range []struct {
err bool
params map[string]string
code int
anames []string
}{
{
false,
map[string]string{},
200,
[]string{"alert1", "alert2", "alert3", "alert4"},
},
{
false,
map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "true"},
200,
[]string{"alert1", "alert2", "alert3", "alert4"},
},
{
false,
map[string]string{"active": "false", "unprocessed": "true", "silenced": "true", "inhibited": "true"},
200,
[]string{"alert2", "alert3", "alert4"},
},
{
false,
map[string]string{"active": "true", "unprocessed": "false", "silenced": "true", "inhibited": "true"},
200,
[]string{"alert1", "alert3", "alert4"},
},
{
false,
map[string]string{"active": "true", "unprocessed": "true", "silenced": "false", "inhibited": "true"},
200,
[]string{"alert1", "alert2", "alert4"},
},
{
false,
map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "false"},
200,
[]string{"alert1", "alert2", "alert3"},
},
{
false,
map[string]string{"filter": "{alertname=\"alert3\""},
200,
[]string{"alert3"},
},
{
false,
map[string]string{"filter": "{alertname"},
400,
[]string{},
},
{
false,
map[string]string{"receiver": "other"},
200,
[]string{},
},
{
false,
map[string]string{"active": "invalid"},
400,
[]string{},
},
{
true,
map[string]string{},
500,
[]string{},
},
} {
alertsProvider := newFakeAlerts(alerts, tc.err)
api := New(alertsProvider, nil, groupAlerts, newGetAlertStatus(alertsProvider), nil, nil)
api.route = dispatch.NewRoute(&config.Route{Receiver: "def-receiver"}, nil)
r, err := http.NewRequest("GET", "/api/v1/alerts", nil)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
q := r.URL.Query()
for k, v := range tc.params {
q.Add(k, v)
}
r.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
api.listAlerts(w, r)
body, _ := ioutil.ReadAll(w.Result().Body)
var res response
err = json.Unmarshal(body, &res)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, response: %s", i, string(body)))
if w.Code != 200 {
continue
}
// Data needs to be serialized/deserialized to be converted to the real type.
b, err := json.Marshal(res.Data)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
retAlerts := []*dispatch.APIAlert{}
err = json.Unmarshal(b, &retAlerts)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
anames := []string{}
for _, a := range retAlerts {
name, ok := a.Labels["alertname"]
if ok {
anames = append(anames, string(name))
}
}
require.Equal(t, tc.anames, anames, fmt.Sprintf("test case: %d, alert names are not equal", i))
}
}
func TestAlertFiltering(t *testing.T) {
type test struct {
alert *model.Alert