Skip to content

Commit

Permalink
fixup! Work in progress
Browse files Browse the repository at this point in the history
Fixes #18314: Create Healthcheck webpage

Fixes #18314: Create Healthcheck webpage
  • Loading branch information
RaphaelGauthier committed Oct 23, 2020
1 parent 5a1744e commit f33cf21
Show file tree
Hide file tree
Showing 13 changed files with 518 additions and 2 deletions.
2 changes: 1 addition & 1 deletion webapp/sources/rudder/rudder-web/src/main/elm/build-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# we want that all elm-stuff stay in src/main/elm
# whatever the path from which this script is called
ELM_DIR="$( cd "$( dirname "$0" )" && pwd )"
PROJECTS=("notifications")
PROJECTS=("notifications" "healthcheck")
for PROJECT in ${PROJECTS}; do
cd ${ELM_DIR}/${PROJECT}
elm make --optimize sources/rudder-${PROJECT}.elm --output=generated/rudder-${PROJECT}.js
Expand Down
31 changes: 31 additions & 0 deletions webapp/sources/rudder/rudder-web/src/main/elm/healthcheck/elm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"type": "application",
"source-directories": [
"sources"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"ccapndave/elm-flat-map": "1.2.0",
"elm/browser": "1.0.1",
"elm/core": "1.0.0",
"elm/html": "1.0.0",
"elm/http": "1.0.0",
"elm/json": "1.1.3",
"elm-community/list-extra": "8.1.0",
"pablen/toasty": "1.2.0"
},
"indirect": {
"elm/random": "1.0.0",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module ApiCalls exposing (..)

import DataTypes exposing (Model, Msg(..))
import Http exposing (emptyBody, expectJson, jsonBody, request, send)
import JsonDecoder exposing (decodeGetRoleApiResult)

getUrl: DataTypes.Model -> String -> String
getUrl m url =
m.contextPath ++ "/secure/api" ++ url

getHealthCheck : Model -> Cmd Msg
getHealthCheck model =
let
req =
request
{ method = "GET"
, headers = []
, url = getUrl model "/healthcheck"
, body = emptyBody
, expect = expectJson decodeGetRoleApiResult
, timeout = Nothing
, withCredentials = False
}
in
send GetHealthCheckResult req
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module DataTypes exposing (..)

import Http exposing (Error)

type TabMenu
= General
| Details

type SeverityLevel
= Critical
| Warning
| CheckPassed

type alias Check =
{ name : String
, msg : String
, level : SeverityLevel
}

type alias Model =
{ contextPath : String
, healthcheck : List Check
, tab : TabMenu
, showChecks : Bool
}

type Msg
= GetHealthCheckResult (Result Error (List Check))
| ChangeTabFocus TabMenu
| CallApi (Model -> Cmd Msg)
| CheckListDisplay
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Init exposing (..)

import ApiCalls exposing (getHealthCheck)
import DataTypes exposing (Model, Msg, SeverityLevel(..), TabMenu(..))


subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none

init : { contextPath : String } -> ( Model, Cmd Msg )
init flags =
let
initModel = Model flags.contextPath [] General False
in
( initModel
, getHealthCheck initModel
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module JsonDecoder exposing (..)

import DataTypes exposing (Check, SeverityLevel(..))
import Json.Decode as D exposing (Decoder, andThen, fail, string, succeed)
import Json.Decode.Pipeline exposing (required)
import String exposing (toLower)

decodeGetRoleApiResult : Decoder (List Check)
decodeGetRoleApiResult =
D.at [ "data" ] (D.list <| decodeCheck)

decodeCheck : Decoder Check
decodeCheck =
D.succeed Check
|> required "name" D.string
|> required "msg" D.string
|> required "status" decodeSeverityLevel

stringToSeverityLevel : String -> Decoder SeverityLevel
stringToSeverityLevel str =
case toLower str of
"ok" -> succeed CheckPassed
"warning" -> succeed Warning
"critical" -> succeed Critical
_ -> fail ("Value `" ++ str ++ "` is not a SeverityLevel")

decodeSeverityLevel : Decoder SeverityLevel
decodeSeverityLevel = string|> andThen stringToSeverityLevel
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
module View exposing (..)

import DataTypes exposing (Check, Model, Msg(..), SeverityLevel(..))
import Html exposing (Html, button, div, i, span, text)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)
import List exposing (any, map, sortWith)
import List.Extra exposing (minimumWith)
import String

compareSeverityLevel: SeverityLevel -> SeverityLevel -> Order
compareSeverityLevel a b =
case (a, b) of
(Critical, Warning) -> LT
(Critical, CheckPassed) -> LT
(Warning, Critical) -> GT
(Warning, CheckPassed) -> LT
(CheckPassed, Warning) -> GT
(CheckPassed, Critical) -> GT
(Warning, Warning) -> EQ
(CheckPassed, CheckPassed) -> EQ
(Critical, Critical) -> EQ

compareCheck: Check -> Check -> Order
compareCheck a b =
let
orderLevel = compareSeverityLevel a.level b.level
in
case orderLevel of
EQ -> if(a.name > b.name) then GT else LT
_ -> orderLevel

containsSeverityLevel: SeverityLevel -> List Check -> Bool
containsSeverityLevel level checks =
any (\c -> c.level == level) checks

chooseHigherSecurityLevel: List Check -> SeverityLevel
chooseHigherSecurityLevel checks =
let
higherPriority = minimumWith (\c1 c2 -> compareSeverityLevel c1.level c2.level ) checks
in
case higherPriority of
Just c -> c.level
Nothing -> CheckPassed -- empty list

severityLevelToString: SeverityLevel -> String
severityLevelToString level =
case level of
Warning -> "warning"
CheckPassed -> "ok"
Critical -> "critical"

severityLevelToIcon: SeverityLevel -> Html Msg
severityLevelToIcon level =
case level of
Critical -> i [class "fa fa-times-circle critical-icon icon-info"][]
Warning -> i [class "fa fa-exclamation-circle warning-icon icon-info"][]
CheckPassed -> i [class "fa fa-check-circle ok-icon icon-info"][]

displayCheckListButton: Bool -> Html Msg
displayCheckListButton isOpen =
let
classNameBtn = "btn-checks btn btn-outline-secondary"
in
if isOpen then
button [class classNameBtn, onClick CheckListDisplay]
[
text "Hide check's list"
, i [class "fa fa-chevron-down"][]
]
else
button [class classNameBtn, onClick CheckListDisplay]
[
text "Show check's list"
, i [class "fa fa-chevron-right"][]
]

displayBigMessage: List Check -> Html Msg
displayBigMessage checks =
let
level = chooseHigherSecurityLevel checks
levelStr = severityLevelToString level
in
div[class "global-msg"]
[
div [class (levelStr ++ "-info")]
[
severityLevelToIcon level
, case level of
Critical -> text "Critical Error"
Warning -> text "Should be improved"
CheckPassed -> text "All check passed"
]
]

checksDisplay: Model -> List Check -> Html Msg
checksDisplay model h =
let
sortedChecks = sortWith compareCheck h
content =
map (
\c ->
let
classNameCircle = (severityLevelToString c.level) ++ "-light circle "
in
div [class "check"]
[ text c.msg, span [class classNameCircle][] ]

) sortedChecks
in
if model.showChecks then
div [class "checklist-container"]
[ div [class "checklist"] content ]
else
div[][]

view : Model -> Html Msg
view model =
let
-- dummy checks for UI testing
testCheck = [
Check "CPU Core" "12 cores available" CheckPassed
, Check "RAM available" "Only 5GB of RAM left" Warning
, Check "/var usage" "3GB left on /var" Critical
, Check "Frontend standard" "CSS alignment is terrible, good luck" Warning
, Check "Networking config" "Port 480 is open" CheckPassed
, Check "File descriptor limit" "Limited to 10_000" Critical
, Check "Certificate inspection" "Certificate is up to date" CheckPassed]
in
div []
[
div [class "header-healthcheck "][]
, div[class "content-block"]
[
displayBigMessage testCheck -- replace `testCheck` by `model.healthcheck`
, displayCheckListButton model.showChecks
, checksDisplay model testCheck -- replace `testCheck` by `model.healthcheck`
]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module Healthcheck exposing (processApiError, update)

import Browser
import DataTypes exposing (Model, Msg(..), SeverityLevel(..))
import Http exposing (Error)
import Init exposing (init, subscriptions)
import View exposing (chooseHigherSecurityLevel, view)
import Result

main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CallApi call ->
(model, call model)
GetHealthCheckResult res ->
case res of
Ok h ->
let
isWarningOrCritical = (chooseHigherSecurityLevel h == DataTypes.Warning)
|| (chooseHigherSecurityLevel h == DataTypes.Critical)
testUI = True
testCheck = [
DataTypes.Check "CPU Core" "12 cores available" CheckPassed
, DataTypes.Check "RAM available" "Only 5GB of RAM left" Warning
, DataTypes.Check "/var usage" "3GB left on /var" Critical
, DataTypes.Check "Frontend standard" "CSS alignment is terrible, good luck" Warning
, DataTypes.Check "Networking config" "Port 480 is open" CheckPassed
, DataTypes.Check "File descriptor limit" "Limited to 10_000" Critical
, DataTypes.Check "Certificate inspection" "Certificate is up to date" CheckPassed]

in
( { model |
healthcheck = testCheck -- replace `testCheck` by `h`
, showChecks = testUI -- replace `testUI` by `isWarningOrCritical`
}
, Cmd.none
)
Err err ->
processApiError err model
ChangeTabFocus newTab ->
if model.tab == newTab then
(model, Cmd.none)
else
({model | tab = newTab}, Cmd.none)
CheckListDisplay ->
if model.showChecks then
({model | showChecks = False}, Cmd.none)
else
({model | showChecks = True}, Cmd.none)

processApiError : Error -> Model -> ( Model, Cmd Msg )
processApiError err model =
--let
-- newModel =
({ model | healthcheck = []}, Cmd.none)
--in
--( newModel, Cmd.none ) |> createErrorNotification "Error while trying to fetch settings." err
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ class Boot extends Loggable {
"secure" / "utilities" / "eventLogs"
>> LocGroup("utilitiesGroup")
>> TestAccess ( () => userIsAllowed("/secure/index",AuthorizationType.Administration.Read) )
, Menu("healthCheckHome", <span>Health check</span>) /
"secure" / "utilities" / "healthcheck"
>> LocGroup("utilitiesGroup")
>> TestAccess ( () => userIsAllowed("/secure/index",AuthorizationType.Administration.Read) )
)
}

Expand Down
Loading

0 comments on commit f33cf21

Please sign in to comment.