HealthReport

This commit is contained in:
Gregor Kleen 2019-04-30 17:19:42 +02:00
parent 4bd26a2dae
commit 25badbe030
13 changed files with 229 additions and 3 deletions

View File

@ -30,6 +30,8 @@ session-timeout: 7200
jwt-expiration: 604800 jwt-expiration: 604800
jwt-encoding: HS256 jwt-encoding: HS256
maximum-content-length: 52428800 maximum-content-length: 52428800
health-check-interval: "_env:HEALTHCHECK_INTERVAL:60"
health-check-http: "_env:HEALTHCHECK_HTTP:true"
log-settings: log-settings:
detailed: "_env:DETAILED_LOGGING:false" detailed: "_env:DETAILED_LOGGING:false"

View File

@ -876,3 +876,9 @@ TutorialCreated tutn@TutorialName: Tutorium #{tutn} erfolgreich angelegt
TutorialEditHeading tutn@TutorialName: #{tutn} bearbeiten TutorialEditHeading tutn@TutorialName: #{tutn} bearbeiten
MassInputTip: Es können mehrere Werte angegeben werden. Werte müssen mit + zur Liste hinzugefügt werden und können mit - wieder entfernt werden. Die Liste wird zunächst nur lokal in Ihrem Browser gespeichert und muss noch zusammen mit dem Rest des Formulars Gesendet werden. MassInputTip: Es können mehrere Werte angegeben werden. Werte müssen mit + zur Liste hinzugefügt werden und können mit - wieder entfernt werden. Die Liste wird zunächst nur lokal in Ihrem Browser gespeichert und muss noch zusammen mit dem Rest des Formulars Gesendet werden.
HealthReport: Instanz-Zustand
InstanceIdentification: Instanz-Identifikation
InstanceId: Instanz-Nummer
ClusterId: Cluster-Nummer

2
routes
View File

@ -50,6 +50,8 @@
/admin/test AdminTestR GET POST /admin/test AdminTestR GET POST
/admin/errMsg AdminErrMsgR GET POST /admin/errMsg AdminErrMsgR GET POST
/health HealthR GET !free
/instance InstanceR GET !free
/info InfoR GET !free /info InfoR GET !free
/info/lecturer InfoLecturerR GET !lecturer /info/lecturer InfoLecturerR GET !lecturer
/info/data DataProtR GET !free /info/data DataProtR GET !free

View File

@ -95,6 +95,7 @@ import Handler.Tutorial
import Handler.Corrections import Handler.Corrections
import Handler.CryptoIDDispatch import Handler.CryptoIDDispatch
import Handler.SystemMessage import Handler.SystemMessage
import Handler.Health
-- This line actually creates our YesodDispatch instance. It is the second half -- This line actually creates our YesodDispatch instance. It is the second half
@ -141,13 +142,14 @@ makeFoundation appSettings'@AppSettings{..} = do
appJobCtl <- liftIO $ newTVarIO Map.empty appJobCtl <- liftIO $ newTVarIO Map.empty
appCronThread <- liftIO newEmptyTMVarIO appCronThread <- liftIO newEmptyTMVarIO
appHealthReport <- liftIO $ newTVarIO Nothing
-- We need a log function to create a connection pool. We need a connection -- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a -- pool to create our foundation. And we need our foundation to get a
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function -- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet = UniWorX {..} let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID = UniWorX {..}
-- The UniWorX {..} syntax is an example of record wild cards. For more -- The UniWorX {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
@ -160,6 +162,7 @@ makeFoundation appSettings'@AppSettings{..} = do
(error "secretBoxKey forced in tempFoundation") (error "secretBoxKey forced in tempFoundation")
(error "widgetMemcached forced in tempFoundation") (error "widgetMemcached forced in tempFoundation")
(error "JSONWebKeySet forced in tempFoundation") (error "JSONWebKeySet forced in tempFoundation")
(error "ClusterID forced in tempFoundation")
runAppLoggingT tempFoundation $ do runAppLoggingT tempFoundation $ do
$logInfoS "InstanceID" $ UUID.toText appInstanceID $logInfoS "InstanceID" $ UUID.toText appInstanceID
@ -191,8 +194,9 @@ makeFoundation appSettings'@AppSettings{..} = do
appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool
appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool
appJSONWebKeySet <- clusterSetting (Proxy :: Proxy 'ClusterJSONWebKeySet) `runSqlPool` sqlPool appJSONWebKeySet <- clusterSetting (Proxy :: Proxy 'ClusterJSONWebKeySet) `runSqlPool` sqlPool
appClusterID <- clusterSetting (Proxy :: Proxy 'ClusterId) `runSqlPool` sqlPool
let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID
$logDebugS "setup" "Job-Handling" $logDebugS "setup" "Job-Handling"
handleJobs foundation handleJobs foundation

View File

@ -118,12 +118,14 @@ data UniWorX = UniWorX
, appLogger :: (ReleaseKey, TVar Logger) , appLogger :: (ReleaseKey, TVar Logger)
, appLogSettings :: TVar LogSettings , appLogSettings :: TVar LogSettings
, appCryptoIDKey :: CryptoIDKey , appCryptoIDKey :: CryptoIDKey
, appClusterID :: ClusterId
, appInstanceID :: InstanceId , appInstanceID :: InstanceId
, appJobCtl :: TVar (Map ThreadId (TMChan JobCtl)) , appJobCtl :: TVar (Map ThreadId (TMChan JobCtl))
, appCronThread :: TMVar (ReleaseKey, ThreadId) , appCronThread :: TMVar (ReleaseKey, ThreadId)
, appSessionKey :: ClientSession.Key , appSessionKey :: ClientSession.Key
, appSecretBoxKey :: SecretBox.Key , appSecretBoxKey :: SecretBox.Key
, appJSONWebKeySet :: Jose.JwkSet , appJSONWebKeySet :: Jose.JwkSet
, appHealthReport :: TVar (Maybe (UTCTime, HealthReport))
} }
makeLenses_ ''UniWorX makeLenses_ ''UniWorX
@ -1346,6 +1348,10 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb HelpR = return ("Hilfe" , Just HomeR) breadcrumb HelpR = return ("Hilfe" , Just HomeR)
breadcrumb HealthR = return ("Status" , Nothing)
breadcrumb InstanceR = return ("Identifikation", Nothing)
breadcrumb ProfileR = return ("User" , Just HomeR) breadcrumb ProfileR = return ("User" , Just HomeR)
breadcrumb ProfileDataR = return ("Profile" , Just ProfileR) breadcrumb ProfileDataR = return ("Profile" , Just ProfileR)
breadcrumb AuthPredsR = return ("Authentifizierung", Just ProfileR) breadcrumb AuthPredsR = return ("Authentifizierung", Just ProfileR)

55
src/Handler/Health.hs Normal file
View File

@ -0,0 +1,55 @@
module Handler.Health where
import Import
import qualified Data.Aeson.Encode.Pretty as Aeson
import qualified Data.Text.Lazy.Builder as Builder
import Utils.Lens
getHealthR :: Handler TypedContent
getHealthR = do
healthReport' <- liftIO . readTVarIO =<< getsYesod appHealthReport
case healthReport' of
Nothing -> sendResponseStatus noContent204 ()
Just (lastUpdated, healthReport) -> do
rContent <- selectRep $ do
provideRep $
siteLayoutMsg MsgHealthReport $ do
setTitleI MsgHealthReport
let report' = Aeson.encodePrettyToTextBuilder healthReport
[whamlet|
<pre style="font-family: monospace; white-space: pre-wrap">
#{report'}
|]
provideJson healthReport
provideRep . return . Builder.toLazyText $ Aeson.encodePrettyToTextBuilder healthReport
interval <- getsYesod $ view _appHealthCheckInterval
expiresAt $ interval `addUTCTime` lastUpdated
addHeader "Last-Modified" $ formatRFC1123 lastUpdated
let
status
| HealthSuccess <- classifyHealthReport healthReport
= ok200
| otherwise
= internalServerError500
sendResponseStatus status rContent
getInstanceR :: Handler TypedContent
getInstanceR = do
instanceInfo@(clusterId, instanceId) <- getsYesod $ (,) <$> appClusterID <*> appInstanceID
selectRep $ do
provideRep $
siteLayoutMsg MsgInstanceIdentification $ do
setTitleI MsgInstanceIdentification
[whamlet|
<dl .deflist>
<dt .deflist__dt>_{MsgClusterId}
<dd .deflist__dd style="font-family: monospace">#{tshow clusterId}
<dt .deflist__dt>_{MsgInstanceId}
<dd .deflist__dd style="font-family: monospace">#{tshow instanceId}
|]
provideJson instanceInfo
provideRep . return $ tshow instanceInfo

View File

@ -51,6 +51,8 @@ import Data.Time.Zones
import Control.Concurrent.STM (retry) import Control.Concurrent.STM (retry)
import qualified System.Systemd.Daemon as Systemd
import Jobs.Handler.SendNotification import Jobs.Handler.SendNotification
import Jobs.Handler.SendTestEmail import Jobs.Handler.SendTestEmail
@ -62,6 +64,8 @@ import Jobs.Handler.SendCourseCommunication
import Jobs.Handler.LecturerInvitation import Jobs.Handler.LecturerInvitation
import Jobs.Handler.CorrectorInvitation import Jobs.Handler.CorrectorInvitation
import Jobs.HealthReport
data JobQueueException = JInvalid QueuedJobId QueuedJob data JobQueueException = JInvalid QueuedJobId QueuedJob
| JLocked QueuedJobId InstanceId UTCTime | JLocked QueuedJobId InstanceId UTCTime
@ -280,6 +284,21 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
-- logDebugS logIdent $ tshow newCTab -- logDebugS logIdent $ tshow newCTab
mapReaderT (liftIO . atomically) $ mapReaderT (liftIO . atomically) $
lift . void . flip swapTMVar newCTab =<< asks jobCrontab lift . void . flip swapTMVar newCTab =<< asks jobCrontab
handleCmd JobCtlGenerateHealthReport = do
hrStorage <- getsYesod appHealthReport
newReport@(classifyHealthReport -> newStatus) <- lift generateHealthReport
$logInfoS "HealthReport" $ toPathPiece newStatus
unless (newStatus == HealthSuccess) $ do
$logErrorS "HealthReport" $ tshow newReport
liftIO $ do
now <- getCurrentTime
atomically . writeTVar hrStorage $ Just (now, newReport)
void . Systemd.notifyStatus . unpack $ toPathPiece newStatus
when (newStatus == HealthSuccess) $
void Systemd.notifyWatchdog
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
jLocked jId act = do jLocked jId act = do

View File

@ -44,6 +44,15 @@ determineCrontab = execWriterT $ do
, cronNotAfter = Right CronNotScheduled , cronNotAfter = Right CronNotScheduled
} }
tell $ HashMap.singleton
JobCtlGenerateHealthReport
Cron
{ cronInitial = CronAsap
, cronRepeat = CronRepeatScheduled CronAsap
, cronRateLimit = appHealthCheckInterval
, cronNotAfter = Right CronNotScheduled
}
let let
sheetJobs (Entity nSheet Sheet{..}) = do sheetJobs (Entity nSheet Sheet{..}) = do
tell $ HashMap.singleton tell $ HashMap.singleton

71
src/Jobs/HealthReport.hs Normal file
View File

@ -0,0 +1,71 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
module Jobs.HealthReport
( generateHealthReport
) where
import Import
import qualified Data.Aeson as Aeson
import Data.Proxy (Proxy(..))
import qualified Data.ByteArray as ByteArray
import Utils.Lens
import Network.HTTP.Simple (httpJSON)
import qualified Network.HTTP.Simple as HTTP
generateHealthReport :: Handler HealthReport
generateHealthReport = HealthReport
<$> matchingClusterConfig
<*> httpReachable
matchingClusterConfig :: Handler Bool
-- ^ Can the cluster configuration be read from the database and does it match our configuration?
matchingClusterConfig = runDB $ and <$> forM universeF clusterSettingMatches
where
clusterSettingMatches ClusterCryptoIDKey = do
ourSetting <- getsYesod appCryptoIDKey
dbSetting <- clusterSetting @'ClusterCryptoIDKey
return $ ((==) `on` fmap (ByteArray.convert :: CryptoIDKey -> ByteString)) (Just ourSetting) dbSetting
clusterSettingMatches ClusterClientSessionKey = do
ourSetting <- getsYesod appSessionKey
dbSetting <- clusterSetting @'ClusterClientSessionKey
return $ Just ourSetting == dbSetting
clusterSettingMatches ClusterSecretBoxKey = do
ourSetting <- getsYesod appSecretBoxKey
dbSetting <- clusterSetting @'ClusterSecretBoxKey
return $ Just ourSetting == dbSetting
clusterSettingMatches ClusterJSONWebKeySet = do
ourSetting <- getsYesod appJSONWebKeySet
dbSetting <- clusterSetting @'ClusterJSONWebKeySet
return $ Just ourSetting == dbSetting
clusterSettingMatches ClusterId = do
ourSetting <- getsYesod appClusterID
dbSetting <- clusterSetting @'ClusterId
return $ Just ourSetting == dbSetting
clusterSetting :: forall key.
( ClusterSetting key
)
=> DB (Maybe (ClusterSettingValue key))
clusterSetting = do
current' <- get . ClusterConfigKey $ knownClusterSetting (Proxy @key)
case Aeson.fromJSON . clusterConfigValue <$> current' of
Just (Aeson.Success c) -> return $ Just c
_other -> return Nothing
httpReachable :: Handler (Maybe Bool)
httpReachable = do
staticAppRoot <- getsYesod $ view _appRoot
for staticAppRoot $ \_textAppRoot -> do
url <- getUrlRender <*> pure InstanceR
baseRequest <- HTTP.parseRequest $ unpack url
httpManager <- getsYesod appHttpManager
let httpRequest = baseRequest
& HTTP.setRequestManager httpManager
(clusterId, _ :: InstanceId) <- responseBody <$> httpJSON httpRequest
getsYesod $ (== clusterId) . appClusterID

View File

@ -69,6 +69,7 @@ data JobCtl = JobCtlFlush
| JobCtlPerform QueuedJobId | JobCtlPerform QueuedJobId
| JobCtlDetermineCrontab | JobCtlDetermineCrontab
| JobCtlQueue Job | JobCtlQueue Job
| JobCtlGenerateHealthReport
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Hashable JobCtl instance Hashable JobCtl

View File

@ -87,6 +87,10 @@ import qualified Data.Binary as Binary
import Time.Types (WeekDay(..)) import Time.Types (WeekDay(..))
import Data.Time.LocalTime (LocalTime, TimeOfDay) import Data.Time.LocalTime (LocalTime, TimeOfDay)
import Data.Semigroup (Min(..))
import Control.Monad.Trans.Writer (execWriter)
import Control.Monad.Writer.Class (MonadWriter(..))
instance PathPiece UUID where instance PathPiece UUID where
fromPathPiece = UUID.fromString . unpack fromPathPiece = UUID.fromString . unpack
@ -922,6 +926,37 @@ deriveJSON defaultOptions
derivePersistFieldJSON ''Occurences derivePersistFieldJSON ''Occurences
data HealthReport = HealthReport
{ healthMatchingClusterConfig :: Bool
-- ^ Is the database-stored configuration we're running under still up to date?
, healthHTTPReachable :: Maybe Bool
-- ^ Can we reach a uni2work-instance with the same `ClusterId` under our configured `approot` via HTTP?
--
-- Can be nothing if we don't have a static configuration setting `appRoot` or if check is disabled in settings
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, omitNothingFields = True
} ''HealthReport
data HealthStatus = HealthFailure | HealthWarning | HealthSuccess
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe HealthStatus
instance Finite HealthStatus
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''HealthStatus
nullaryPathPiece ''HealthStatus $ camelToPathPiece' 1
classifyHealthReport :: HealthReport -> HealthStatus
classifyHealthReport HealthReport{..} = getMin . execWriter $ do
unless healthMatchingClusterConfig . tell $ Min HealthFailure
unless (fromMaybe True healthHTTPReachable) . tell $ Min HealthFailure
-- Type synonyms -- Type synonyms
type Email = Text type Email = Text
@ -936,5 +971,6 @@ type TutorialName = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID type InstanceId = UUID
type ClusterId = UUID
type TokenId = UUID type TokenId = UUID
type TermCandidateIncidence = UUID type TermCandidateIncidence = UUID

View File

@ -111,6 +111,8 @@ data AppSettings = AppSettings
, appMaximumContentLength :: Maybe Word64 , appMaximumContentLength :: Maybe Word64
, appJwtExpiration :: Maybe NominalDiffTime , appJwtExpiration :: Maybe NominalDiffTime
, appJwtEncoding :: JwtEncoding , appJwtEncoding :: JwtEncoding
, appHealthCheckInterval :: NominalDiffTime
, appHealthCheckHTTP :: Bool
, appInitialLogSettings :: LogSettings , appInitialLogSettings :: LogSettings
@ -378,6 +380,9 @@ instance FromJSON AppSettings where
appJwtExpiration <- o .:? "jwt-expiration" appJwtExpiration <- o .:? "jwt-expiration"
appJwtEncoding <- o .: "jwt-encoding" appJwtEncoding <- o .: "jwt-encoding"
appHealthCheckInterval <- o .: "health-check-interval"
appHealthCheckHTTP <- o .: "health-check-http"
appSessionTimeout <- o .: "session-timeout" appSessionTimeout <- o .: "session-timeout"
appMaximumContentLength <- o .: "maximum-content-length" appMaximumContentLength <- o .: "maximum-content-length"

View File

@ -36,12 +36,16 @@ import qualified Jose.Jwa as Jose
import qualified Jose.Jwk as Jose import qualified Jose.Jwk as Jose
import qualified Jose.Jwt as Jose import qualified Jose.Jwt as Jose
import Data.UUID (UUID)
import Control.Monad.Random.Class (MonadRandom(..))
data ClusterSettingsKey data ClusterSettingsKey
= ClusterCryptoIDKey = ClusterCryptoIDKey
| ClusterClientSessionKey | ClusterClientSessionKey
| ClusterSecretBoxKey | ClusterSecretBoxKey
| ClusterJSONWebKeySet | ClusterJSONWebKeySet
| ClusterId
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
instance Universe ClusterSettingsKey instance Universe ClusterSettingsKey
@ -134,3 +138,9 @@ instance ClusterSetting 'ClusterJSONWebKeySet where
jwkSig <- Jose.generateSymmetricKey 32 (Jose.UTCKeyId now) Jose.Sig (Just $ Jose.Signed Jose.HS256) jwkSig <- Jose.generateSymmetricKey 32 (Jose.UTCKeyId now) Jose.Sig (Just $ Jose.Signed Jose.HS256)
return $ Jose.JwkSet [jwkSig] return $ Jose.JwkSet [jwkSig]
knownClusterSetting _ = ClusterJSONWebKeySet knownClusterSetting _ = ClusterJSONWebKeySet
instance ClusterSetting 'ClusterId where
type ClusterSettingValue 'ClusterId = UUID
initClusterSetting _ = liftIO getRandom
knownClusterSetting _ = ClusterId