packages feed

prodapi-0.1.0.0: src/Prod/Status.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}

module Prod.Status (
    StatusApi,
    RenderStatus,
    defaultStatusPage,
    metricsSection,
    versionsSection,
    statusPage,
    handleStatus,
    Status (..),
    Identification (..),
    this,
)
where

import Control.Monad.IO.Class (liftIO)
import Data.Aeson (ToJSON (..), (.=))
import qualified Data.Aeson as Aeson
import Data.Foldable (traverse_)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.UUID (UUID)
import Data.UUID.V4 (nextRandom)
import Data.Version (Version, showVersion)
import GHC.Generics (Generic)
import Prod.Health as Health
import Prod.MimeTypes (HTML)
import Servant (Get, JSON, MimeRender (..), (:>))
import Servant.Server (Handler)
import System.IO.Unsafe (unsafePerformIO)

import Lucid

type StatusApi a =
    "status"
        :> Get '[HTML, JSON] (Status a)

newtype Identification = Identification Text
    deriving
        (ToJSON)
        via Text

-- | Type to render a status page.
type RenderStatus a = Status a -> Html ()

data Status a
    = Status
    { identification :: !Identification
    , liveness :: !Liveness
    , readiness :: !Readiness
    , appStatus :: !a
    , renderer :: RenderStatus a
    }

instance (ToJSON a) => ToJSON (Status a) where
    toJSON (Status i l r st _) =
        Aeson.object
            [ "id" .= i
            , "liveness" .= l
            , "readiness" .= r
            , "status" .= toJSON st
            ]

handleStatus :: Runtime -> IO a -> RenderStatus a -> Handler (Status a)
handleStatus runtime getAppStatus render =
    liftIO $
        Status this
            <$> Health.liveness runtime
            <*> Health.completeReadiness runtime
            <*> getAppStatus
            <*> (pure render)

{-# NOINLINE this #-}
this :: Identification
this = unsafePerformIO $ fmap (Identification . Text.pack . show) nextRandom

instance {-# OVERLAPPABLE #-} MimeRender HTML (Status a) where
    mimeRender _ st = renderBS $ render st
      where
        render = renderer st

defaultStatusPage :: forall a. (a -> Html ()) -> RenderStatus a
defaultStatusPage renderAppStatus = go
  where
    go :: Status a -> Html ()
    go (Status (Identification uuid) liveness readiness appStatus _) =
        html_ $ do
            head_ $ do
                title_ "status page"
                link_ [rel_ "stylesheet", type_ "text/css", href_ "status.css"]
            body_ $ do
                section_ $ do
                    h1_ "identification"
                    p_ $ toHtml uuid
                section_ $ do
                    h1_ "general status"
                    renderLiveness liveness
                    renderReadiness readiness
                    with form_ [action_ "/health/drain", method_ "post"] $
                        input_ [type_ "submit", value_ "drain me"]
                section_ $ do
                    h1_ "app status"
                    renderAppStatus appStatus
    renderLiveness :: Liveness -> Html ()
    renderLiveness Alive = p_ $ with a_ [href_ "/health/alive"] "alive"

    renderReadiness :: Readiness -> Html ()
    renderReadiness Ready = p_ $ with a_ [href_ "/health/ready"] "ready"
    renderReadiness (Ill reasons) = do
        p_ $ with a_ [href_ "/health/ready"] "not-ready"
        ul_ $ do
            traverse_ renderReason reasons

    renderReason :: Reason -> Html ()
    renderReason (Reason r) =
        li_ $ toHtml r

{- | Like defaultStatusPage but uses a type-class-defined to pass the
application-status rendering.
-}
statusPage :: (ToHtml a) => RenderStatus a
statusPage = defaultStatusPage toHtml

type MetricsJSurl = Text

-- | Section with metrics.
metricsSection :: MetricsJSurl -> RenderStatus a
metricsSection metrics_js = const $
    section_ $ do
        h1_ "metrics"
        with div_ [id_ "metrics"] $ pure ()
        toHtmlRaw @Text $ "<script async type=\"text/javascript\" src=\"" <> metrics_js <> "\"></script>"

versionsSection :: [(String, Version)] -> RenderStatus a
versionsSection pkgs = const $
    section_ $ do
        h1_ "versions"
        ul_ $
            traverse_ renderVersion pkgs
  where
    renderVersion :: (String, Version) -> Html ()
    renderVersion (pkg, ver) =
        li_ $ p_ $ toHtml $ pkg <> ":" <> showVersion ver