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" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"sync" "sync"
"time" "time"
@ -75,6 +76,7 @@ type API struct {
alerts provider.Alerts alerts provider.Alerts
silences *silence.Silences silences *silence.Silences
config *config.Config config *config.Config
route *dispatch.Route
resolveTimeout time.Duration resolveTimeout time.Duration
uptime time.Time uptime time.Time
mrouter *mesh.Router mrouter *mesh.Router
@ -119,6 +121,7 @@ func (api *API) Register(r *route.Router) {
r = r.WithPrefix("/v1") r = r.WithPrefix("/v1")
r.Get("/status", ihf("status", api.status)) 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/groups", ihf("alert_groups", api.alertGroups))
r.Get("/alerts", ihf("list_alerts", api.listAlerts)) 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.resolveTimeout = resolveTimeout
api.config = cfg api.config = cfg
api.route = dispatch.NewRoute(cfg.Route, nil)
return nil return nil
} }
@ -157,6 +161,18 @@ func (e *apiError) Error() string {
return fmt.Sprintf("%s: %s", e.typ, e.err) 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) { func (api *API) status(w http.ResponseWriter, req *http.Request) {
api.mtx.RLock() api.mtx.RLock()
@ -217,10 +233,11 @@ func getMeshStatus(api *API) meshStatus {
return strippedStatus 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 var err error
matchers := []*labels.Matcher{} matchers := []*labels.Matcher{}
if filter := req.FormValue("filter"); filter != "" {
if filter := r.FormValue("filter"); filter != "" {
matchers, err = parse.Matchers(filter) matchers, err = parse.Matchers(filter)
if err != nil { if err != nil {
respondError(w, apiError{ respondError(w, apiError{
@ -236,18 +253,13 @@ func (api *API) alertGroups(w http.ResponseWriter, req *http.Request) {
respond(w, groups) respond(w, groups)
} }
type APIAlert struct {
*model.Alert
Status types.AlertStatus `json:"status"`
}
func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) { func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
var ( var (
err error err error
re *regexp.Regexp
// Initialize result slice to prevent api returning `null` when there // Initialize result slice to prevent api returning `null` when there
// are no alerts present // are no alerts present
res = []*APIAlert{} res = []*dispatch.APIAlert{}
matchers = []*labels.Matcher{} matchers = []*labels.Matcher{}
showSilenced = true 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() alerts := api.alerts.GetPending()
defer alerts.Close() defer alerts.Close()
@ -287,6 +313,16 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
break 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) { if !alertMatchesFilterLabels(&a.Alert, matchers) {
continue continue
} }
@ -302,9 +338,10 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
continue continue
} }
apiAlert := &APIAlert{ apiAlert := &dispatch.APIAlert{
Alert: &a.Alert, Alert: &a.Alert,
Status: status, Status: status,
Receivers: receivers,
} }
res = append(res, apiAlert) res = append(res, apiAlert)
@ -320,6 +357,16 @@ func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) {
respond(w, res) 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 { func alertMatchesFilterLabels(a *model.Alert, matchers []*labels.Matcher) bool {
for _, m := range matchers { for _, m := range matchers {
if v, prs := a.Labels[model.LabelName(m.Name)]; !prs || !m.Matches(string(v)) { 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. // annotated with silencing and inhibition info.
type APIAlert struct { type APIAlert struct {
*model.Alert *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. // AlertGroup is a list of alert blocks grouped by the same label set.

View File

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

View File

@ -1,5 +1,5 @@
{{ define "__alertmanager" }}AlertManager{{ end }} {{ 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 "__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 }} {{ 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. // Data assembles data for template expansion.
func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data { func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data {
data := &Data{ data := &Data{
Receiver: strings.SplitN(recv, "/", 2)[0], Receiver: regexp.QuoteMeta(strings.SplitN(recv, "/", 2)[0]),
Status: string(types.Alerts(alerts...).Status()), Status: string(types.Alerts(alerts...).Status()),
Alerts: make(Alerts, 0, len(alerts)), Alerts: make(Alerts, 0, len(alerts)),
GroupLabels: KV{}, GroupLabels: KV{},

View File

@ -1,17 +1,26 @@
module Alerts.Api exposing (..) 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 Json.Decode as Json exposing (..)
import Utils.Api exposing (iso8601Time) import Utils.Api exposing (iso8601Time)
import Utils.Types exposing (ApiData)
import Utils.Filter exposing (Filter, generateQueryString) 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 : String -> Filter -> Cmd (ApiData (List Alert))
fetchAlerts apiUrl filter = fetchAlerts apiUrl filter =
let let
url = url =
String.join "/" [ apiUrl, "alerts" ++ (generateQueryString filter) ] String.join "/" [ apiUrl, "alerts" ++ generateQueryString filter ]
in in
Utils.Api.send (Utils.Api.get url alertsDecoder) Utils.Api.send (Utils.Api.get url alertsDecoder)

View File

@ -24,7 +24,7 @@ import Set
type alias Filter = type alias Filter =
{ text : Maybe String { text : Maybe String
, group : Maybe String , group : Maybe String
, receiver : Maybe Matcher , receiver : Maybe String
, showSilenced : Maybe Bool , showSilenced : Maybe Bool
} }
@ -46,10 +46,10 @@ generateQueryParam name =
generateQueryString : Filter -> String generateQueryString : Filter -> String
generateQueryString { receiver, showSilenced, text, group } = generateQueryString { receiver, showSilenced, text, group } =
let let
-- TODO: Re-add receiver once it is parsed on the server side.
parts = parts =
[ ( "silenced", Maybe.withDefault False showSilenced |> toString |> String.toLower |> Just ) [ ( "silenced", Maybe.withDefault False showSilenced |> toString |> String.toLower |> Just )
, ( "filter", emptyToNothing text ) , ( "filter", emptyToNothing text )
, ( "receiver", emptyToNothing receiver )
, ( "group", group ) , ( "group", group )
] ]
|> List.filterMap (uncurry generateQueryParam) |> 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 : String -> UrlParser.QueryParser (Maybe Bool -> a) a
boolParam name = boolParam name =
UrlParser.customParam name UrlParser.customParam name
(\x -> (Maybe.map (String.toLower >> (/=) "false"))
case x of
Nothing ->
Nothing
Just value ->
if (String.toLower value) == "false" then
Just False
else
Just True
)
alertsParser : Parser (Filter -> a) a alertsParser : Parser (Filter -> a) a
alertsParser = alertsParser =
map s "alerts"
(\filter group receiver silenced -> <?> stringParam "filter"
let <?> stringParam "group"
parsed = <?> stringParam "receiver"
Maybe.map <?> boolParam "silenced"
(\r -> |> map Filter
{ key = "receiver", op = RegexMatch, value = "^(?:" ++ r ++ ")$" }
)
receiver
in
Filter filter group parsed silenced
)
(s "alerts" <?> stringParam "filter" <?> stringParam "group" <?> stringParam "receiver" <?> boolParam "silenced")

View File

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

View File

@ -7,6 +7,7 @@ import Utils.Filter exposing (Filter, parseFilter)
import Utils.Types exposing (ApiData(Initial, Loading, Success, Failure)) import Utils.Types exposing (ApiData(Initial, Loading, Success, Failure))
import Types exposing (Msg(MsgForAlertList, Noop)) import Types exposing (Msg(MsgForAlertList, Noop))
import Set import Set
import Regex
import Navigation import Navigation
import Utils.Filter exposing (generateQueryString) import Utils.Filter exposing (generateQueryString)
import Views.GroupBar.Updates as GroupBar import Views.GroupBar.Updates as GroupBar
@ -47,9 +48,26 @@ update msg ({ groupBar, filterBar } as model) filter apiUrl basePath =
FilterBar.setMatchers filter filterBar FilterBar.setMatchers filter filterBar
in in
( { model | alerts = Loading, filterBar = newFilterBar, groupBar = newGroupBar, activeId = Nothing } ( { 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 -> ToggleSilenced showSilenced ->
( model ( model
, Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced }) , Navigation.newUrl (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced })

View File

@ -12,15 +12,16 @@ import Utils.Views
import Utils.List import Utils.List
import Views.AlertList.AlertView as AlertView import Views.AlertList.AlertView as AlertView
import Views.GroupBar.Types as GroupBar 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 Types exposing (Msg(Noop, CreateSilenceFromAlert, MsgForAlertList))
import Views.GroupBar.Views as GroupBar import Views.GroupBar.Views as GroupBar
import Dict exposing (Dict) import Dict exposing (Dict)
import Regex
renderSilenced : Maybe Bool -> Html Msg renderSilenced : Maybe Bool -> Html Msg
renderSilenced maybeShowSilenced = renderSilenced maybeShowSilenced =
li [ class "nav-item ml-auto " ] li [ class "nav-item" ]
[ label [ class "mt-1 custom-control custom-checkbox" ] [ label [ class "mt-1 custom-control custom-checkbox" ]
[ input [ input
[ type_ "checkbox" [ type_ "checkbox"
@ -36,7 +37,7 @@ renderSilenced maybeShowSilenced =
view : Model -> Filter -> Html Msg view : Model -> Filter -> Html Msg
view { alerts, groupBar, filterBar, tab, activeId } filter = view { alerts, groupBar, filterBar, receivers, showRecievers, tab, activeId } filter =
div [] div []
[ div [ div
[ class "card mb-5" ] [ class "card mb-5" ]
@ -44,6 +45,7 @@ view { alerts, groupBar, filterBar, tab, activeId } filter =
[ ul [ class "nav nav-tabs card-header-tabs" ] [ ul [ class "nav nav-tabs card-header-tabs" ]
[ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text "Filter" ] [ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text "Filter" ]
, Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ text "Group" ] , Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ text "Group" ]
, renderReceivers filter.receiver receivers showRecievers
, renderSilenced filter.showSilenced , renderSilenced filter.showSilenced
] ]
] ]
@ -113,3 +115,66 @@ alertList activeId labels filter alerts =
else else
ul [ class "list-group mb-4" ] (List.map (AlertView.view labels activeId) alerts) 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