Stn/add receiver support (#872)

Add ability to filter alerts by receiver in UI. This adds changes both in the Elm UI, as well as the Go backend.
This commit is contained in:
stuart nelson 2017-06-26 18:20:26 +02:00 committed by Max Inden
parent 6ef5ca6225
commit a7009a9db7
13 changed files with 185 additions and 55 deletions

View File

@ -17,6 +17,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"sync"
"time"
@ -75,6 +76,7 @@ type API struct {
alerts provider.Alerts
silences *silence.Silences
config *config.Config
route *dispatch.Route
resolveTimeout time.Duration
uptime time.Time
mrouter *mesh.Router
@ -119,6 +121,7 @@ func (api *API) Register(r *route.Router) {
r = r.WithPrefix("/v1")
r.Get("/status", ihf("status", api.status))
r.Get("/receivers", ihf("receivers", api.receivers))
r.Get("/alerts/groups", ihf("alert_groups", api.alertGroups))
r.Get("/alerts", ihf("list_alerts", api.listAlerts))
@ -137,6 +140,7 @@ func (api *API) Update(cfg *config.Config, resolveTimeout time.Duration) error {
api.resolveTimeout = resolveTimeout
api.config = cfg
api.route = dispatch.NewRoute(cfg.Route, nil)
return nil
}
@ -157,6 +161,18 @@ func (e *apiError) Error() string {
return fmt.Sprintf("%s: %s", e.typ, e.err)
}
func (api *API) receivers(w http.ResponseWriter, req *http.Request) {
api.mtx.RLock()
defer api.mtx.RUnlock()
receivers := make([]string, 0, len(api.config.Receivers))
for _, r := range api.config.Receivers {
receivers = append(receivers, r.Name)
}
respond(w, receivers)
}
func (api *API) status(w http.ResponseWriter, req *http.Request) {
api.mtx.RLock()
@ -217,10 +233,11 @@ func getMeshStatus(api *API) meshStatus {
return strippedStatus
}
func (api *API) alertGroups(w http.ResponseWriter, req *http.Request) {
func (api *API) alertGroups(w http.ResponseWriter, r *http.Request) {
var err error
matchers := []*labels.Matcher{}
if filter := req.FormValue("filter"); filter != "" {
if filter := r.FormValue("filter"); filter != "" {
matchers, err = parse.Matchers(filter)
if err != nil {
respondError(w, apiError{
@ -236,18 +253,13 @@ func (api *API) alertGroups(w http.ResponseWriter, req *http.Request) {
respond(w, groups)
}
type APIAlert struct {
*model.Alert
Status types.AlertStatus `json:"status"`
}
func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
var (
err error
re *regexp.Regexp
// Initialize result slice to prevent api returning `null` when there
// are no alerts present
res = []*APIAlert{}
res = []*dispatch.APIAlert{}
matchers = []*labels.Matcher{}
showSilenced = true
)
@ -278,6 +290,20 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
}
}
if receiverParam := r.FormValue("receiver"); receiverParam != "" {
re, err = regexp.Compile("^(?:" + receiverParam + ")$")
if err != nil {
respondError(w, apiError{
typ: errorBadData,
err: fmt.Errorf(
"failed to parse receiver param: %s",
receiverParam,
),
}, nil)
return
}
}
alerts := api.alerts.GetPending()
defer alerts.Close()
@ -287,6 +313,16 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
break
}
routes := api.route.Match(a.Labels)
receivers := make([]string, 0, len(routes))
for _, r := range routes {
receivers = append(receivers, r.RouteOpts.Receiver)
}
if re != nil && !regexpAny(re, receivers) {
continue
}
if !alertMatchesFilterLabels(&a.Alert, matchers) {
continue
}
@ -302,9 +338,10 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
continue
}
apiAlert := &APIAlert{
Alert: &a.Alert,
Status: status,
apiAlert := &dispatch.APIAlert{
Alert: &a.Alert,
Status: status,
Receivers: receivers,
}
res = append(res, apiAlert)
@ -320,6 +357,16 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
respond(w, res)
}
func regexpAny(re *regexp.Regexp, ss []string) bool {
for _, s := range ss {
if re.MatchString(s) {
return true
}
}
return false
}
func alertMatchesFilterLabels(a *model.Alert, matchers []*labels.Matcher) bool {
for _, m := range matchers {
if v, prs := a.Labels[model.LabelName(m.Name)]; !prs || !m.Matches(string(v)) {

View File

@ -80,7 +80,8 @@ type AlertBlock struct {
// annotated with silencing and inhibition info.
type APIAlert struct {
*model.Alert
Status types.AlertStatus `json:"status"`
Status types.AlertStatus `json:"status"`
Receivers []string `json:"receivers"`
}
// AlertGroup is a list of alert blocks grouped by the same label set.

View File

@ -6,9 +6,9 @@ route:
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'webhook'
receiver: 'web.hook'
receivers:
- name: 'webhook'
- name: 'web.hook'
webhook_configs:
- url: 'http://127.0.0.1:5001/'
inhibit_rules:

View File

@ -1,5 +1,5 @@
{{ define "__alertmanager" }}AlertManager{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }}
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
{{ define "__description" }}{{ end }}

File diff suppressed because one or more lines are too long

View File

@ -269,7 +269,7 @@ func (as Alerts) Resolved() []Alert {
// Data assembles data for template expansion.
func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data {
data := &Data{
Receiver: strings.SplitN(recv, "/", 2)[0],
Receiver: regexp.QuoteMeta(strings.SplitN(recv, "/", 2)[0]),
Status: string(types.Alerts(alerts...).Status()),
Alerts: make(Alerts, 0, len(alerts)),
GroupLabels: KV{},

View File

@ -1,17 +1,26 @@
module Alerts.Api exposing (..)
import Alerts.Types exposing (Alert, RouteOpts, Block, AlertGroup)
import Alerts.Types exposing (Alert, AlertGroup, Block, RouteOpts)
import Json.Decode as Json exposing (..)
import Utils.Api exposing (iso8601Time)
import Utils.Types exposing (ApiData)
import Utils.Filter exposing (Filter, generateQueryString)
import Utils.Types exposing (ApiData)
fetchReceivers : String -> Cmd (ApiData (List String))
fetchReceivers apiUrl =
Utils.Api.send
(Utils.Api.get
(apiUrl ++ "/receivers")
(field "data" (list string))
)
fetchAlerts : String -> Filter -> Cmd (ApiData (List Alert))
fetchAlerts apiUrl filter =
let
url =
String.join "/" [ apiUrl, "alerts" ++ (generateQueryString filter) ]
String.join "/" [ apiUrl, "alerts" ++ generateQueryString filter ]
in
Utils.Api.send (Utils.Api.get url alertsDecoder)

View File

@ -24,7 +24,7 @@ import Set
type alias Filter =
{ text : Maybe String
, group : Maybe String
, receiver : Maybe Matcher
, receiver : Maybe String
, showSilenced : Maybe Bool
}
@ -46,10 +46,10 @@ generateQueryParam name =
generateQueryString : Filter -> String
generateQueryString { receiver, showSilenced, text, group } =
let
-- TODO: Re-add receiver once it is parsed on the server side.
parts =
[ ( "silenced", Maybe.withDefault False showSilenced |> toString |> String.toLower |> Just )
, ( "filter", emptyToNothing text )
, ( "receiver", emptyToNothing receiver )
, ( "group", group )
]
|> List.filterMap (uncurry generateQueryParam)

View File

@ -7,31 +7,14 @@ import Utils.Filter exposing (Filter, parseMatcher, MatchOperator(RegexMatch))
boolParam : String -> UrlParser.QueryParser (Maybe Bool -> a) a
boolParam name =
UrlParser.customParam name
(\x ->
case x of
Nothing ->
Nothing
Just value ->
if (String.toLower value) == "false" then
Just False
else
Just True
)
(Maybe.map (String.toLower >> (/=) "false"))
alertsParser : Parser (Filter -> a) a
alertsParser =
map
(\filter group receiver silenced ->
let
parsed =
Maybe.map
(\r ->
{ key = "receiver", op = RegexMatch, value = "^(?:" ++ r ++ ")$" }
)
receiver
in
Filter filter group parsed silenced
)
(s "alerts" <?> stringParam "filter" <?> stringParam "group" <?> stringParam "receiver" <?> boolParam "silenced")
s "alerts"
<?> stringParam "filter"
<?> stringParam "group"
<?> stringParam "receiver"
<?> boolParam "silenced"
|> map Filter

View File

@ -1,13 +1,16 @@
module Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..), initAlertList)
import Utils.Types exposing (ApiData(Initial))
import Alerts.Types exposing (Alert)
import Utils.Types exposing (ApiData(Initial))
import Views.FilterBar.Types as FilterBar
import Views.GroupBar.Types as GroupBar
type AlertListMsg
= AlertsFetched (ApiData (List Alert))
| ReceiversFetched (ApiData (List String))
| ToggleReceivers Bool
| SelectReceiver (Maybe String)
| FetchAlerts
| MsgForFilterBar FilterBar.Msg
| MsgForGroupBar GroupBar.Msg
@ -23,6 +26,8 @@ type Tab
type alias Model =
{ alerts : ApiData (List Alert)
, receivers : List String
, showRecievers : Bool
, groupBar : GroupBar.Model
, filterBar : FilterBar.Model
, tab : Tab
@ -33,6 +38,8 @@ type alias Model =
initAlertList : Model
initAlertList =
{ alerts = Initial
, receivers = []
, showRecievers = False
, groupBar = GroupBar.initGroupBar
, filterBar = FilterBar.initFilterBar
, tab = FilterTab

View File

@ -7,6 +7,7 @@ import Utils.Filter exposing (Filter, parseFilter)
import Utils.Types exposing (ApiData(Initial, Loading, Success, Failure))
import Types exposing (Msg(MsgForAlertList, Noop))
import Set
import Regex
import Navigation
import Utils.Filter exposing (generateQueryString)
import Views.GroupBar.Updates as GroupBar
@ -47,9 +48,26 @@ update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
FilterBar.setMatchers filter filterBar
in
( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing }
, Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList)
, Cmd.batch
[ Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList)
, Api.fetchReceivers apiUrl |> Cmd.map (ReceiversFetched >> MsgForAlertList)
]
)
ReceiversFetched (Success receivers) ->
( { model | receivers = receivers }, Cmd.none )
ReceiversFetched _ ->
( model, Cmd.none )
ToggleReceivers show ->
( { model | showRecievers = show }, Cmd.none )
SelectReceiver receiver ->
( { model | showRecievers = False }
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | receiver = Maybe.map Regex.escape receiver })
)
ToggleSilenced showSilenced ->
( model
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced })

View File

@ -12,15 +12,16 @@ import Utils.Views
import Utils.List
import Views.AlertList.AlertView as AlertView
import Views.GroupBar.Types as GroupBar
import Views.AlertList.Types exposing (AlertListMsg(MsgForFilterBar, MsgForGroupBar, SetTab, ToggleSilenced), Model, Tab(..))
import Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..))
import Types exposing (Msg(Noop, CreateSilenceFromAlert, MsgForAlertList))
import Views.GroupBar.Views as GroupBar
import Dict exposing (Dict)
import Regex
renderSilenced : Maybe Bool -> Html Msg
renderSilenced maybeShowSilenced =
li [ class "nav-item ml-auto " ]
li [ class "nav-item" ]
[ label [ class "mt-1 custom-control custom-checkbox" ]
[ input
[ type_ "checkbox"
@ -36,7 +37,7 @@ renderSilenced maybeShowSilenced =
view : Model -> Filter -> Html Msg
view { alerts, groupBar, filterBar, tab, activeId } filter =
view { alerts, groupBar, filterBar, receivers, showRecievers, tab, activeId } filter =
div []
[ div
[ class "card mb-5" ]
@ -44,6 +45,7 @@ view { alerts, groupBar, filterBar, tab, activeId } filter =
[ ul [ class "nav nav-tabs card-header-tabs" ]
[ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text "Filter" ]
, Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ text "Group" ]
, renderReceivers filter.receiver receivers showRecievers
, renderSilenced filter.showSilenced
]
]
@ -113,3 +115,66 @@ alertList activeId labels filter alerts =
else
ul [ class "list-group mb-4" ] (List.map (AlertView.view labels activeId) alerts)
]
renderReceivers : Maybe String -> List String -> Bool -> Html Msg
renderReceivers receiver receivers opened =
let
autoCompleteClass =
if opened then
"show"
else
""
navLinkClass =
if opened then
"active"
else
""
-- Try to find the regex-escaped receiver in the list of unescaped receivers:
unescapedReceiver =
receivers
|> List.filter (Regex.escape >> Just >> (==) receiver)
|> List.map Just
|> List.head
|> Maybe.withDefault receiver
in
li
[ class ("nav-item ml-auto autocomplete-menu " ++ autoCompleteClass)
, onBlur (ToggleReceivers False |> MsgForAlertList)
, tabindex 1
, style
[ ( "position", "relative" )
, ( "outline", "none" )
]
]
[ div
[ onClick (ToggleReceivers (not opened) |> MsgForAlertList)
, class "mt-1 mr-4"
, style [ ( "cursor", "pointer" ) ]
]
[ text ("Receiver: " ++ Maybe.withDefault "All" unescapedReceiver) ]
, receivers
|> List.map Just
|> (::) Nothing
|> List.map (receiverField unescapedReceiver)
|> div [ class "dropdown-menu dropdown-menu-right" ]
]
receiverField : Maybe String -> Maybe String -> Html Msg
receiverField selected maybeReceiver =
let
attrs =
if selected == maybeReceiver then
[ class "dropdown-item active" ]
else
[ class "dropdown-item"
, style [ ( "cursor", "pointer" ) ]
, onClick (SelectReceiver maybeReceiver |> MsgForAlertList)
]
in
div
attrs
[ text (Maybe.withDefault "All" maybeReceiver) ]

File diff suppressed because one or more lines are too long