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:
parent
6ef5ca6225
commit
a7009a9db7
71
api/api.go
71
api/api.go
|
@ -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)) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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{},
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue