Show muted alerts in Alert Groups API (#3797)

This commit updates /api/v2/alerts/groups to show if an alert is
suppressed from one or more active or mute time intervals. While
the muted by field can be found in /api/v2/alerts, it is not
used here because /api/v2/alerts does not take aggregation
or routing into consideration.

It also updates the UI to support filtering muted alerts via the
Muted checkbox.

Signed-off-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
George Robinson 2024-10-23 16:42:21 +01:00 committed by GitHub
parent 8572fe849c
commit 5979dff9dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 273 additions and 51 deletions

View File

@ -45,16 +45,20 @@ type API struct {
inFlightSem chan struct{} inFlightSem chan struct{}
} }
// Options for the creation of an API object. Alerts, Silences, and StatusFunc // Options for the creation of an API object. Alerts, Silences, AlertStatusFunc
// are mandatory to set. The zero value for everything else is a safe default. // and GroupMutedFunc are mandatory. The zero value for everything else is a safe
// default.
type Options struct { type Options struct {
// Alerts to be used by the API. Mandatory. // Alerts to be used by the API. Mandatory.
Alerts provider.Alerts Alerts provider.Alerts
// Silences to be used by the API. Mandatory. // Silences to be used by the API. Mandatory.
Silences *silence.Silences Silences *silence.Silences
// StatusFunc is used be the API to retrieve the AlertStatus of an // AlertStatusFunc is used be the API to retrieve the AlertStatus of an
// alert. Mandatory. // alert. Mandatory.
StatusFunc func(model.Fingerprint) types.AlertStatus AlertStatusFunc func(model.Fingerprint) types.AlertStatus
// GroupMutedFunc is used be the API to know if an alert is muted.
// Mandatory.
GroupMutedFunc func(routeID, groupKey string) ([]string, bool)
// Peer from the gossip cluster. If nil, no clustering will be used. // Peer from the gossip cluster. If nil, no clustering will be used.
Peer cluster.ClusterPeer Peer cluster.ClusterPeer
// Timeout for all HTTP connections. The zero value (and negative // Timeout for all HTTP connections. The zero value (and negative
@ -83,8 +87,11 @@ func (o Options) validate() error {
if o.Silences == nil { if o.Silences == nil {
return errors.New("mandatory field Silences not set") return errors.New("mandatory field Silences not set")
} }
if o.StatusFunc == nil { if o.AlertStatusFunc == nil {
return errors.New("mandatory field StatusFunc not set") return errors.New("mandatory field AlertStatusFunc not set")
}
if o.GroupMutedFunc == nil {
return errors.New("mandatory field GroupMutedFunc not set")
} }
if o.GroupFunc == nil { if o.GroupFunc == nil {
return errors.New("mandatory field GroupFunc not set") return errors.New("mandatory field GroupFunc not set")
@ -113,7 +120,8 @@ func New(opts Options) (*API, error) {
v2, err := apiv2.NewAPI( v2, err := apiv2.NewAPI(
opts.Alerts, opts.Alerts,
opts.GroupFunc, opts.GroupFunc,
opts.StatusFunc, opts.AlertStatusFunc,
opts.GroupMutedFunc,
opts.Silences, opts.Silences,
opts.Peer, opts.Peer,
log.With(l, "version", "v2"), log.With(l, "version", "v2"),

View File

@ -60,6 +60,7 @@ type API struct {
alerts provider.Alerts alerts provider.Alerts
alertGroups groupsFn alertGroups groupsFn
getAlertStatus getAlertStatusFn getAlertStatus getAlertStatusFn
groupMutedFunc groupMutedFunc
uptime time.Time uptime time.Time
// mtx protects alertmanagerConfig, setAlertStatus and route. // mtx protects alertmanagerConfig, setAlertStatus and route.
@ -78,6 +79,7 @@ type API struct {
type ( type (
groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string) groupsFn func(func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string)
groupMutedFunc func(routeID, groupKey string) ([]string, bool)
getAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus getAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus
setAlertStatusFn func(prometheus_model.LabelSet) setAlertStatusFn func(prometheus_model.LabelSet)
) )
@ -86,7 +88,8 @@ type (
func NewAPI( func NewAPI(
alerts provider.Alerts, alerts provider.Alerts,
gf groupsFn, gf groupsFn,
sf getAlertStatusFn, asf getAlertStatusFn,
gmf groupMutedFunc,
silences *silence.Silences, silences *silence.Silences,
peer cluster.ClusterPeer, peer cluster.ClusterPeer,
l log.Logger, l log.Logger,
@ -94,8 +97,9 @@ func NewAPI(
) (*API, error) { ) (*API, error) {
api := API{ api := API{
alerts: alerts, alerts: alerts,
getAlertStatus: sf, getAlertStatus: asf,
alertGroups: gf, alertGroups: gf,
groupMutedFunc: gmf,
peer: peer, peer: peer,
silences: silences, silences: silences,
logger: l, logger: l,
@ -290,7 +294,7 @@ func (api *API) getAlertsHandler(params alert_ops.GetAlertsParams) middleware.Re
continue continue
} }
alert := AlertToOpenAPIAlert(a, api.getAlertStatus(a.Fingerprint()), receivers) alert := AlertToOpenAPIAlert(a, api.getAlertStatus(a.Fingerprint()), receivers, nil)
res = append(res, alert) res = append(res, alert)
} }
@ -407,6 +411,11 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
res := make(open_api_models.AlertGroups, 0, len(alertGroups)) res := make(open_api_models.AlertGroups, 0, len(alertGroups))
for _, alertGroup := range alertGroups { for _, alertGroup := range alertGroups {
mutedBy, isMuted := api.groupMutedFunc(alertGroup.RouteID, alertGroup.GroupKey)
if !*params.Muted && isMuted {
continue
}
ag := &open_api_models.AlertGroup{ ag := &open_api_models.AlertGroup{
Receiver: &open_api_models.Receiver{Name: &alertGroup.Receiver}, Receiver: &open_api_models.Receiver{Name: &alertGroup.Receiver},
Labels: ModelLabelSetToAPILabelSet(alertGroup.Labels), Labels: ModelLabelSetToAPILabelSet(alertGroup.Labels),
@ -417,7 +426,7 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams
fp := alert.Fingerprint() fp := alert.Fingerprint()
receivers := allReceivers[fp] receivers := allReceivers[fp]
status := api.getAlertStatus(fp) status := api.getAlertStatus(fp)
apiAlert := AlertToOpenAPIAlert(alert, status, receivers) apiAlert := AlertToOpenAPIAlert(alert, status, receivers, mutedBy)
ag.Alerts = append(ag.Alerts, apiAlert) ag.Alerts = append(ag.Alerts, apiAlert)
} }
res = append(res, ag) res = append(res, ag)

View File

@ -484,17 +484,12 @@ func TestAlertToOpenAPIAlert(t *testing.T) {
UpdatedAt: updated, UpdatedAt: updated,
} }
) )
openAPIAlert := AlertToOpenAPIAlert(alert, types.AlertStatus{State: types.AlertStateActive}, receivers) openAPIAlert := AlertToOpenAPIAlert(alert, types.AlertStatus{State: types.AlertStateActive}, receivers, nil)
require.Equal(t, &open_api_models.GettableAlert{ require.Equal(t, &open_api_models.GettableAlert{
Annotations: open_api_models.LabelSet{}, Annotations: open_api_models.LabelSet{},
Alert: open_api_models.Alert{ Alert: open_api_models.Alert{
Labels: open_api_models.LabelSet{"severity": "critical", "alertname": "alert1"}, Labels: open_api_models.LabelSet{"severity": "critical", "alertname": "alert1"},
}, },
Status: &open_api_models.AlertStatus{
State: &active,
InhibitedBy: []string{},
SilencedBy: []string{},
},
StartsAt: convertDateTime(start), StartsAt: convertDateTime(start),
EndsAt: convertDateTime(time.Time{}), EndsAt: convertDateTime(time.Time{}),
UpdatedAt: convertDateTime(updated), UpdatedAt: convertDateTime(updated),
@ -503,6 +498,12 @@ func TestAlertToOpenAPIAlert(t *testing.T) {
{Name: &receivers[0]}, {Name: &receivers[0]},
{Name: &receivers[1]}, {Name: &receivers[1]},
}, },
Status: &open_api_models.AlertStatus{
State: &active,
InhibitedBy: []string{},
SilencedBy: []string{},
MutedBy: []string{},
},
}, openAPIAlert) }, openAPIAlert)
} }

View File

@ -98,6 +98,14 @@ type GetAlertGroupsParams struct {
*/ */
Inhibited *bool Inhibited *bool
/* Muted.
Show muted alerts
Default: true
*/
Muted *bool
/* Receiver. /* Receiver.
A regex matching receivers to filter alerts by A regex matching receivers to filter alerts by
@ -134,12 +142,15 @@ func (o *GetAlertGroupsParams) SetDefaults() {
inhibitedDefault = bool(true) inhibitedDefault = bool(true)
mutedDefault = bool(true)
silencedDefault = bool(true) silencedDefault = bool(true)
) )
val := GetAlertGroupsParams{ val := GetAlertGroupsParams{
Active: &activeDefault, Active: &activeDefault,
Inhibited: &inhibitedDefault, Inhibited: &inhibitedDefault,
Muted: &mutedDefault,
Silenced: &silencedDefault, Silenced: &silencedDefault,
} }
@ -215,6 +226,17 @@ func (o *GetAlertGroupsParams) SetInhibited(inhibited *bool) {
o.Inhibited = inhibited o.Inhibited = inhibited
} }
// WithMuted adds the muted to the get alert groups params
func (o *GetAlertGroupsParams) WithMuted(muted *bool) *GetAlertGroupsParams {
o.SetMuted(muted)
return o
}
// SetMuted adds the muted to the get alert groups params
func (o *GetAlertGroupsParams) SetMuted(muted *bool) {
o.Muted = muted
}
// WithReceiver adds the receiver to the get alert groups params // WithReceiver adds the receiver to the get alert groups params
func (o *GetAlertGroupsParams) WithReceiver(receiver *string) *GetAlertGroupsParams { func (o *GetAlertGroupsParams) WithReceiver(receiver *string) *GetAlertGroupsParams {
o.SetReceiver(receiver) o.SetReceiver(receiver)
@ -290,6 +312,23 @@ func (o *GetAlertGroupsParams) WriteToRequest(r runtime.ClientRequest, reg strfm
} }
} }
if o.Muted != nil {
// query param muted
var qrMuted bool
if o.Muted != nil {
qrMuted = *o.Muted
}
qMuted := swag.FormatBool(qrMuted)
if qMuted != "" {
if err := r.SetQueryParam("muted", qMuted); err != nil {
return err
}
}
}
if o.Receiver != nil { if o.Receiver != nil {
// query param receiver // query param receiver

View File

@ -117,7 +117,7 @@ func PostableSilenceToProto(s *open_api_models.PostableSilence) (*silencepb.Sile
} }
// AlertToOpenAPIAlert converts internal alerts, alert types, and receivers to *open_api_models.GettableAlert. // AlertToOpenAPIAlert converts internal alerts, alert types, and receivers to *open_api_models.GettableAlert.
func AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers []string) *open_api_models.GettableAlert { func AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers, mutedBy []string) *open_api_models.GettableAlert {
startsAt := strfmt.DateTime(alert.StartsAt) startsAt := strfmt.DateTime(alert.StartsAt)
updatedAt := strfmt.DateTime(alert.UpdatedAt) updatedAt := strfmt.DateTime(alert.UpdatedAt)
endsAt := strfmt.DateTime(alert.EndsAt) endsAt := strfmt.DateTime(alert.EndsAt)
@ -128,7 +128,13 @@ func AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers
} }
fp := alert.Fingerprint().String() fp := alert.Fingerprint().String()
state := string(status.State) state := string(status.State)
if len(mutedBy) > 0 {
// If the alert is muted, change the state to suppressed.
state = open_api_models.AlertStatusStateSuppressed
}
aa := &open_api_models.GettableAlert{ aa := &open_api_models.GettableAlert{
Alert: open_api_models.Alert{ Alert: open_api_models.Alert{
GeneratorURL: strfmt.URI(alert.GeneratorURL), GeneratorURL: strfmt.URI(alert.GeneratorURL),
@ -144,6 +150,7 @@ func AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers
State: &state, State: &state,
SilencedBy: status.SilencedBy, SilencedBy: status.SilencedBy,
InhibitedBy: status.InhibitedBy, InhibitedBy: status.InhibitedBy,
MutedBy: mutedBy,
}, },
} }
@ -155,6 +162,10 @@ func AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers
aa.Status.InhibitedBy = []string{} aa.Status.InhibitedBy = []string{}
} }
if aa.Status.MutedBy == nil {
aa.Status.MutedBy = []string{}
}
return aa return aa
} }

View File

@ -38,6 +38,10 @@ type AlertStatus struct {
// Required: true // Required: true
InhibitedBy []string `json:"inhibitedBy"` InhibitedBy []string `json:"inhibitedBy"`
// muted by
// Required: true
MutedBy []string `json:"mutedBy"`
// silenced by // silenced by
// Required: true // Required: true
SilencedBy []string `json:"silencedBy"` SilencedBy []string `json:"silencedBy"`
@ -56,6 +60,10 @@ func (m *AlertStatus) Validate(formats strfmt.Registry) error {
res = append(res, err) res = append(res, err)
} }
if err := m.validateMutedBy(formats); err != nil {
res = append(res, err)
}
if err := m.validateSilencedBy(formats); err != nil { if err := m.validateSilencedBy(formats); err != nil {
res = append(res, err) res = append(res, err)
} }
@ -79,6 +87,15 @@ func (m *AlertStatus) validateInhibitedBy(formats strfmt.Registry) error {
return nil return nil
} }
func (m *AlertStatus) validateMutedBy(formats strfmt.Registry) error {
if err := validate.Required("mutedBy", "body", m.MutedBy); err != nil {
return err
}
return nil
}
func (m *AlertStatus) validateSilencedBy(formats strfmt.Registry) error { func (m *AlertStatus) validateSilencedBy(formats strfmt.Registry) error {
if err := validate.Required("silencedBy", "body", m.SilencedBy); err != nil { if err := validate.Required("silencedBy", "body", m.SilencedBy); err != nil {

View File

@ -223,6 +223,11 @@ paths:
type: boolean type: boolean
description: Show inhibited alerts description: Show inhibited alerts
default: true default: true
- in: query
name: muted
type: boolean
description: Show muted alerts
default: true
- name: filter - name: filter
in: query in: query
description: A list of matchers to filter alerts by description: A list of matchers to filter alerts by
@ -501,10 +506,15 @@ definitions:
type: array type: array
items: items:
type: string type: string
mutedBy:
type: array
items:
type: string
required: required:
- state - state
- silencedBy - silencedBy
- inhibitedBy - inhibitedBy
- mutedBy
receiver: receiver:
type: object type: object
properties: properties:

View File

@ -177,6 +177,13 @@ func init() {
"name": "inhibited", "name": "inhibited",
"in": "query" "in": "query"
}, },
{
"type": "boolean",
"default": true,
"description": "Show muted alerts",
"name": "muted",
"in": "query"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@ -433,7 +440,8 @@ func init() {
"required": [ "required": [
"state", "state",
"silencedBy", "silencedBy",
"inhibitedBy" "inhibitedBy",
"mutedBy"
], ],
"properties": { "properties": {
"inhibitedBy": { "inhibitedBy": {
@ -442,6 +450,12 @@ func init() {
"type": "string" "type": "string"
} }
}, },
"mutedBy": {
"type": "array",
"items": {
"type": "string"
}
},
"silencedBy": { "silencedBy": {
"type": "array", "type": "array",
"items": { "items": {
@ -979,6 +993,13 @@ func init() {
"name": "inhibited", "name": "inhibited",
"in": "query" "in": "query"
}, },
{
"type": "boolean",
"default": true,
"description": "Show muted alerts",
"name": "muted",
"in": "query"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@ -1256,7 +1277,8 @@ func init() {
"required": [ "required": [
"state", "state",
"silencedBy", "silencedBy",
"inhibitedBy" "inhibitedBy",
"mutedBy"
], ],
"properties": { "properties": {
"inhibitedBy": { "inhibitedBy": {
@ -1265,6 +1287,12 @@ func init() {
"type": "string" "type": "string"
} }
}, },
"mutedBy": {
"type": "array",
"items": {
"type": "string"
}
},
"silencedBy": { "silencedBy": {
"type": "array", "type": "array",
"items": { "items": {

View File

@ -39,6 +39,7 @@ func NewGetAlertGroupsParams() GetAlertGroupsParams {
activeDefault = bool(true) activeDefault = bool(true)
inhibitedDefault = bool(true) inhibitedDefault = bool(true)
mutedDefault = bool(true)
silencedDefault = bool(true) silencedDefault = bool(true)
) )
@ -48,6 +49,8 @@ func NewGetAlertGroupsParams() GetAlertGroupsParams {
Inhibited: &inhibitedDefault, Inhibited: &inhibitedDefault,
Muted: &mutedDefault,
Silenced: &silencedDefault, Silenced: &silencedDefault,
} }
} }
@ -76,6 +79,11 @@ type GetAlertGroupsParams struct {
Default: true Default: true
*/ */
Inhibited *bool Inhibited *bool
/*Show muted alerts
In: query
Default: true
*/
Muted *bool
/*A regex matching receivers to filter alerts by /*A regex matching receivers to filter alerts by
In: query In: query
*/ */
@ -113,6 +121,11 @@ func (o *GetAlertGroupsParams) BindRequest(r *http.Request, route *middleware.Ma
res = append(res, err) res = append(res, err)
} }
qMuted, qhkMuted, _ := qs.GetOK("muted")
if err := o.bindMuted(qMuted, qhkMuted, route.Formats); err != nil {
res = append(res, err)
}
qReceiver, qhkReceiver, _ := qs.GetOK("receiver") qReceiver, qhkReceiver, _ := qs.GetOK("receiver")
if err := o.bindReceiver(qReceiver, qhkReceiver, route.Formats); err != nil { if err := o.bindReceiver(qReceiver, qhkReceiver, route.Formats); err != nil {
res = append(res, err) res = append(res, err)
@ -198,6 +211,30 @@ func (o *GetAlertGroupsParams) bindInhibited(rawData []string, hasKey bool, form
return nil return nil
} }
// bindMuted binds and validates parameter Muted from query.
func (o *GetAlertGroupsParams) bindMuted(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewGetAlertGroupsParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("muted", "query", "bool", raw)
}
o.Muted = &value
return nil
}
// bindReceiver binds and validates parameter Receiver from query. // bindReceiver binds and validates parameter Receiver from query.
func (o *GetAlertGroupsParams) bindReceiver(rawData []string, hasKey bool, formats strfmt.Registry) error { func (o *GetAlertGroupsParams) bindReceiver(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string var raw string

View File

@ -32,6 +32,7 @@ type GetAlertGroupsURL struct {
Active *bool Active *bool
Filter []string Filter []string
Inhibited *bool Inhibited *bool
Muted *bool
Receiver *string Receiver *string
Silenced *bool Silenced *bool
@ -99,6 +100,14 @@ func (o *GetAlertGroupsURL) Build() (*url.URL, error) {
qs.Set("inhibited", inhibitedQ) qs.Set("inhibited", inhibitedQ)
} }
var mutedQ string
if o.Muted != nil {
mutedQ = swag.FormatBool(*o.Muted)
}
if mutedQ != "" {
qs.Set("muted", mutedQ)
}
var receiverQ string var receiverQ string
if o.Receiver != nil { if o.Receiver != nil {
receiverQ = *o.Receiver receiverQ = *o.Receiver

File diff suppressed because one or more lines are too long

View File

@ -368,15 +368,16 @@ func run() int {
} }
api, err := api.New(api.Options{ api, err := api.New(api.Options{
Alerts: alerts, Alerts: alerts,
Silences: silences, Silences: silences,
StatusFunc: marker.Status, AlertStatusFunc: marker.Status,
Peer: clusterPeer, GroupMutedFunc: marker.Muted,
Timeout: *httpTimeout, Peer: clusterPeer,
Concurrency: *getConcurrency, Timeout: *httpTimeout,
Logger: log.With(logger, "component", "api"), Concurrency: *getConcurrency,
Registry: prometheus.DefaultRegisterer, Logger: log.With(logger, "component", "api"),
GroupFunc: groupFn, Registry: prometheus.DefaultRegisterer,
GroupFunc: groupFn,
}) })
if err != nil { if err != nil {
level.Error(logger).Log("err", fmt.Errorf("failed to create API: %w", err)) level.Error(logger).Log("err", fmt.Errorf("failed to create API: %w", err))

View File

@ -206,6 +206,8 @@ type AlertGroup struct {
Alerts types.AlertSlice Alerts types.AlertSlice
Labels model.LabelSet Labels model.LabelSet
Receiver string Receiver string
GroupKey string
RouteID string
} }
type AlertGroups []*AlertGroup type AlertGroups []*AlertGroup
@ -242,6 +244,8 @@ func (d *Dispatcher) Groups(routeFilter func(*Route) bool, alertFilter func(*typ
alertGroup := &AlertGroup{ alertGroup := &AlertGroup{
Labels: ag.labels, Labels: ag.labels,
Receiver: receiver, Receiver: receiver,
GroupKey: ag.GroupKey(),
RouteID: ag.routeID,
} }
alerts := ag.alerts.List() alerts := ag.alerts.List()

View File

@ -439,6 +439,8 @@ route:
"alertname": "OtherAlert", "alertname": "OtherAlert",
}, },
Receiver: "prod", Receiver: "prod",
GroupKey: "{}:{alertname=\"OtherAlert\"}",
RouteID: "{}",
}, },
&AlertGroup{ &AlertGroup{
Alerts: []*types.Alert{inputAlerts[1]}, Alerts: []*types.Alert{inputAlerts[1]},
@ -447,6 +449,8 @@ route:
"service": "api", "service": "api",
}, },
Receiver: "testing", Receiver: "testing",
GroupKey: "{}/{env=\"testing\"}:{alertname=\"TestingAlert\", service=\"api\"}",
RouteID: "{}/{env=\"testing\"}/0",
}, },
&AlertGroup{ &AlertGroup{
Alerts: []*types.Alert{inputAlerts[2], inputAlerts[3]}, Alerts: []*types.Alert{inputAlerts[2], inputAlerts[3]},
@ -456,6 +460,8 @@ route:
"cluster": "aa", "cluster": "aa",
}, },
Receiver: "prod", Receiver: "prod",
GroupKey: "{}/{env=\"prod\"}:{alertname=\"HighErrorRate\", cluster=\"aa\", service=\"api\"}",
RouteID: "{}/{env=\"prod\"}/1",
}, },
&AlertGroup{ &AlertGroup{
Alerts: []*types.Alert{inputAlerts[4]}, Alerts: []*types.Alert{inputAlerts[4]},
@ -465,6 +471,8 @@ route:
"cluster": "bb", "cluster": "bb",
}, },
Receiver: "prod", Receiver: "prod",
GroupKey: "{}/{env=\"prod\"}:{alertname=\"HighErrorRate\", cluster=\"bb\", service=\"api\"}",
RouteID: "{}/{env=\"prod\"}/1",
}, },
&AlertGroup{ &AlertGroup{
Alerts: []*types.Alert{inputAlerts[5]}, Alerts: []*types.Alert{inputAlerts[5]},
@ -474,6 +482,8 @@ route:
"cluster": "bb", "cluster": "bb",
}, },
Receiver: "kafka", Receiver: "kafka",
GroupKey: "{}/{kafka=\"yes\"}:{alertname=\"HighLatency\", cluster=\"bb\", service=\"db\"}",
RouteID: "{}/{kafka=\"yes\"}/2",
}, },
&AlertGroup{ &AlertGroup{
Alerts: []*types.Alert{inputAlerts[5]}, Alerts: []*types.Alert{inputAlerts[5]},
@ -483,6 +493,8 @@ route:
"cluster": "bb", "cluster": "bb",
}, },
Receiver: "prod", Receiver: "prod",
GroupKey: "{}/{env=\"prod\"}:{alertname=\"HighLatency\", cluster=\"bb\", service=\"db\"}",
RouteID: "{}/{env=\"prod\"}/1",
}, },
}, alertGroups) }, alertGroups)
require.Equal(t, map[model.Fingerprint][]string{ require.Equal(t, map[model.Fingerprint][]string{

View File

@ -22,6 +22,7 @@ type alias AlertStatus =
{ state : State { state : State
, silencedBy : List String , silencedBy : List String
, inhibitedBy : List String , inhibitedBy : List String
, mutedBy : List String
} }
@ -37,6 +38,7 @@ decoder =
|> required "state" stateDecoder |> required "state" stateDecoder
|> required "silencedBy" (Decode.list Decode.string) |> required "silencedBy" (Decode.list Decode.string)
|> required "inhibitedBy" (Decode.list Decode.string) |> required "inhibitedBy" (Decode.list Decode.string)
|> required "mutedBy" (Decode.list Decode.string)
encoder : AlertStatus -> Encode.Value encoder : AlertStatus -> Encode.Value
@ -45,6 +47,7 @@ encoder model =
[ ( "state", stateEncoder model.state ) [ ( "state", stateEncoder model.state )
, ( "silencedBy", Encode.list Encode.string model.silencedBy ) , ( "silencedBy", Encode.list Encode.string model.silencedBy )
, ( "inhibitedBy", Encode.list Encode.string model.inhibitedBy ) , ( "inhibitedBy", Encode.list Encode.string model.inhibitedBy )
, ( "mutedBy", Encode.list Encode.string model.mutedBy )
] ]

View File

@ -34,6 +34,7 @@ type alias Filter =
, receiver : Maybe String , receiver : Maybe String
, showSilenced : Maybe Bool , showSilenced : Maybe Bool
, showInhibited : Maybe Bool , showInhibited : Maybe Bool
, showMuted : Maybe Bool
, showActive : Maybe Bool , showActive : Maybe Bool
} }
@ -46,6 +47,7 @@ nullFilter =
, receiver = Nothing , receiver = Nothing
, showSilenced = Nothing , showSilenced = Nothing
, showInhibited = Nothing , showInhibited = Nothing
, showMuted = Nothing
, showActive = Nothing , showActive = Nothing
} }
@ -56,11 +58,12 @@ generateQueryParam name =
toUrl : String -> Filter -> String toUrl : String -> Filter -> String
toUrl baseUrl { receiver, customGrouping, showSilenced, showInhibited, showActive, text, group } = toUrl baseUrl { receiver, customGrouping, showSilenced, showInhibited, showMuted, showActive, text, group } =
let let
parts = parts =
[ ( "silenced", Maybe.withDefault False showSilenced |> boolToString |> Just ) [ ( "silenced", Maybe.withDefault False showSilenced |> boolToString |> Just )
, ( "inhibited", Maybe.withDefault False showInhibited |> boolToString |> Just ) , ( "inhibited", Maybe.withDefault False showInhibited |> boolToString |> Just )
, ( "muted", Maybe.withDefault False showMuted |> boolToString |> Just )
, ( "active", Maybe.withDefault True showActive |> boolToString |> Just ) , ( "active", Maybe.withDefault True showActive |> boolToString |> Just )
, ( "filter", emptyToNothing text ) , ( "filter", emptyToNothing text )
, ( "receiver", emptyToNothing receiver ) , ( "receiver", emptyToNothing receiver )
@ -81,7 +84,7 @@ toUrl baseUrl { receiver, customGrouping, showSilenced, showInhibited, showActiv
generateAPIQueryString : Filter -> String generateAPIQueryString : Filter -> String
generateAPIQueryString { receiver, showSilenced, showInhibited, showActive, text, group } = generateAPIQueryString { receiver, showSilenced, showInhibited, showMuted, showActive, text, group } =
let let
filter_ = filter_ =
case parseFilter (Maybe.withDefault "" text) of case parseFilter (Maybe.withDefault "" text) of
@ -95,6 +98,7 @@ generateAPIQueryString { receiver, showSilenced, showInhibited, showActive, text
filter_ filter_
++ [ ( "silenced", Maybe.withDefault False showSilenced |> boolToString |> Just ) ++ [ ( "silenced", Maybe.withDefault False showSilenced |> boolToString |> Just )
, ( "inhibited", Maybe.withDefault False showInhibited |> boolToString |> Just ) , ( "inhibited", Maybe.withDefault False showInhibited |> boolToString |> Just )
, ( "muted", Maybe.withDefault False showMuted |> boolToString |> Just )
, ( "active", Maybe.withDefault True showActive |> boolToString |> Just ) , ( "active", Maybe.withDefault True showActive |> boolToString |> Just )
, ( "receiver", emptyToNothing receiver ) , ( "receiver", emptyToNothing receiver )
, ( "group", group ) , ( "group", group )
@ -375,6 +379,7 @@ silencePreviewFilter apiMatchers =
|> Just |> Just
, showSilenced = Just True , showSilenced = Just True
, showInhibited = Just True , showInhibited = Just True
, showMuted = Just True
, showActive = Just True , showActive = Just True
} }

View File

@ -48,6 +48,7 @@ view labels maybeActiveId alert =
text "" text ""
, silenceButton alert , silenceButton alert
, inhibitedIcon alert , inhibitedIcon alert
, mutedIcon alert
, linkButton alert , linkButton alert
] ]
, if maybeActiveId == Just alert.fingerprint then , if maybeActiveId == Just alert.fingerprint then
@ -145,8 +146,8 @@ inhibitedIcon : GettableAlert -> Html Msg
inhibitedIcon alert = inhibitedIcon alert =
case List.head alert.status.inhibitedBy of case List.head alert.status.inhibitedBy of
Just _ -> Just _ ->
a span
[ class "btn btn-outline-info border-0 text-info" [ class "btn btn-outline-danger border-0"
] ]
[ i [ class "fa fa-eye-slash mr-2" ] [] [ i [ class "fa fa-eye-slash mr-2" ] []
, text "Inhibited" , text "Inhibited"
@ -154,3 +155,18 @@ inhibitedIcon alert =
Nothing -> Nothing ->
text "" text ""
mutedIcon : GettableAlert -> Html Msg
mutedIcon alert =
case List.head alert.status.mutedBy of
Just _ ->
span
[ class "btn btn-outline-danger border-0"
]
[ i [ class "fa fa-bell-slash mr-2" ] []
, text "Muted"
]
Nothing ->
text ""

View File

@ -25,5 +25,6 @@ alertsParser =
<?> Query.string "receiver" <?> Query.string "receiver"
<?> maybeBoolParam "silenced" <?> maybeBoolParam "silenced"
<?> maybeBoolParam "inhibited" <?> maybeBoolParam "inhibited"
<?> maybeBoolParam "muted"
<?> maybeBoolParam "active" <?> maybeBoolParam "active"
|> map Filter |> map Filter

View File

@ -24,6 +24,7 @@ type AlertListMsg
| MsgForGroupBar GroupBar.Msg | MsgForGroupBar GroupBar.Msg
| ToggleSilenced Bool | ToggleSilenced Bool
| ToggleInhibited Bool | ToggleInhibited Bool
| ToggleMuted Bool
| SetActive (Maybe String) | SetActive (Maybe String)
| ActiveGroups Int | ActiveGroups Int
| SetTab Tab | SetTab Tab

View File

@ -120,6 +120,11 @@ update msg ({ groupBar, alerts, filterBar, receiverBar, alertGroups } as model)
, Navigation.pushUrl model.key (filteredUrl { filter | showInhibited = Just showInhibited }) , Navigation.pushUrl model.key (filteredUrl { filter | showInhibited = Just showInhibited })
) )
ToggleMuted showMuted ->
( model
, Navigation.pushUrl model.key (filteredUrl { filter | showMuted = Just showMuted })
)
SetTab tab -> SetTab tab ->
( { model | tab = tab }, Cmd.none ) ( { model | tab = tab }, Cmd.none )

View File

@ -59,6 +59,7 @@ view { alertGroups, groupBar, filterBar, receiverBar, tab, activeId, activeGroup
|> Html.map (MsgForReceiverBar >> MsgForAlertList) |> Html.map (MsgForReceiverBar >> MsgForAlertList)
, renderCheckbox "Silenced" filter.showSilenced ToggleSilenced , renderCheckbox "Silenced" filter.showSilenced ToggleSilenced
, renderCheckbox "Inhibited" filter.showInhibited ToggleInhibited , renderCheckbox "Inhibited" filter.showInhibited ToggleInhibited
, renderCheckbox "Muted" filter.showMuted ToggleMuted
] ]
] ]
, div [ class "card-block" ] , div [ class "card-block" ]

View File

@ -9,6 +9,6 @@ silenceListParser : Parser (Filter -> a) a
silenceListParser = silenceListParser =
map map
(\t -> (\t ->
Filter t Nothing False Nothing Nothing Nothing Nothing Filter t Nothing False Nothing Nothing Nothing Nothing Nothing
) )
(s "silences" <?> Query.string "filter") (s "silences" <?> Query.string "filter")

View File

@ -33,34 +33,38 @@ parseMatcher =
toUrl : Test toUrl : Test
toUrl = toUrl =
describe "toUrl" describe "toUrl"
[ test "should not render keys with Nothing value except the silenced, inhibited, and active parameters, which default to false, false, true, respectively." <| [ test "should not render keys with Nothing value except the silenced, inhibited, muted and active parameters, which default to false, false, false, true, respectively." <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=false&active=true" Expect.equal "/alerts?silenced=false&inhibited=false&muted=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })
, test "should not render filter key with empty value" <| , test "should not render filter key with empty value" <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=false&active=true" Expect.equal "/alerts?silenced=false&inhibited=false&muted=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "", showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })
, test "should render filter key with values" <| , test "should render filter key with values" <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=false&active=true&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D" Expect.equal "/alerts?silenced=false&inhibited=false&muted=false&active=true&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "{foo=\"bar\", baz=~\"quux.*\"}", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "{foo=\"bar\", baz=~\"quux.*\"}", showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })
, test "should render silenced key with bool" <| , test "should render silenced key with bool" <|
\() -> \() ->
Expect.equal "/alerts?silenced=true&inhibited=false&active=true" Expect.equal "/alerts?silenced=true&inhibited=false&muted=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Just True, showInhibited = Nothing, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Just True, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })
, test "should render inhibited key with bool" <| , test "should render inhibited key with bool" <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=true&active=true" Expect.equal "/alerts?silenced=false&inhibited=true&muted=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Just True, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Just True, showMuted = Nothing, showActive = Nothing })
, test "should render muted key with bool" <|
\() ->
Expect.equal "/alerts?silenced=false&inhibited=false&muted=true&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Just True, showActive = Nothing })
, test "should render active key with bool" <| , test "should render active key with bool" <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=false&active=false" Expect.equal "/alerts?silenced=false&inhibited=false&muted=false&active=false"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Just False }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Just False })
, test "should add customGrouping key" <| , test "should add customGrouping key" <|
\() -> \() ->
Expect.equal "/alerts?silenced=false&inhibited=false&active=true&customGrouping=true" Expect.equal "/alerts?silenced=false&inhibited=false&muted=false&active=true&customGrouping=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = True, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing }) (Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = True, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })
] ]