Skip to content

Commit

Permalink
feat: minimal health check (#2092)
Browse files Browse the repository at this point in the history
  • Loading branch information
steve-chavez authored Dec 23, 2021
1 parent c858d15 commit ac3655d
Show file tree
Hide file tree
Showing 18 changed files with 98 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- #1933, Add a minimal health check endpoint on an admin port at the `<host>:<admin_server_port>/health` endpoint - @steve-chavez
+ For enabling this, the `admin-server-port` config must be set explictly

### Fixed

- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
Expand Down
26 changes: 24 additions & 2 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import qualified Hasql.DynamicStatements.Snippet as SQL
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
import qualified Hasql.Pool as SQL
import qualified Hasql.Transaction as SQL
import qualified Hasql.Session as SQL (sql)
import qualified Hasql.Transaction as SQL hiding (sql)
import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
Expand Down Expand Up @@ -113,6 +114,11 @@ run installHandlers maybeRunWithSocket appState = do
when configDbChannelEnabled $ listener appState

let app = postgrest configLogLevel appState (connectionWorker appState)
adminApp = postgrestAdmin appState configDbChannelEnabled

whenJust configAdminServerPort $ \adminPort -> do
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
void . forkIO $ Warp.runSettings (serverSettings conf & setPort adminPort) adminApp

case configServerUnixSocket of
Just socket ->
Expand All @@ -127,6 +133,9 @@ run installHandlers maybeRunWithSocket appState = do
do
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
Warp.runSettings (serverSettings conf) app
where
whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m ()
whenJust mg f = maybe (pure ()) f mg

serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} =
Expand All @@ -135,6 +144,19 @@ serverSettings AppConfig{..} =
& setPort configServerPort
& setServerName ("postgrest/" <> prettyVersion)

-- | PostgREST admin application
postgrestAdmin :: AppState.AppState -> Bool -> Wai.Application
postgrestAdmin appState configDbChannelEnabled req respond =
case Wai.pathInfo req of
["health"] ->
if configDbChannelEnabled then do
listenerOn <- AppState.getIsListenerOn appState
respond $ Wai.responseLBS (if listenerOn then HTTP.status200 else HTTP.status503) [] mempty
else do
result <- SQL.use (AppState.getPool appState) $ SQL.sql "SELECT 1"
respond $ Wai.responseLBS (if isRight result then HTTP.status200 else HTTP.status503) [] mempty
_ -> respond $ Wai.responseLBS HTTP.status404 [] mempty

-- | PostgREST application
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
postgrest logLev appState connWorker =
Expand Down
11 changes: 11 additions & 0 deletions src/PostgREST/AppState.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module PostgREST.AppState
( AppState
, getConfig
, getDbStructure
, getIsListenerOn
, getIsWorkerOn
, getJsonDbS
, getMainThreadId
Expand All @@ -16,6 +17,7 @@ module PostgREST.AppState
, logWithZTime
, putConfig
, putDbStructure
, putIsListenerOn
, putIsWorkerOn
, putJsonDbS
, putPgVersion
Expand Down Expand Up @@ -53,6 +55,8 @@ data AppState = AppState
, stateIsWorkerOn :: IORef Bool
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
, stateListener :: MVar ()
-- | State of the LISTEN channel, used for health checks
, stateIsListenerOn :: IORef Bool
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
Expand All @@ -78,6 +82,7 @@ initWithPool newPool conf =
<*> newIORef mempty
<*> newIORef False
<*> newEmptyMVar
<*> newIORef False
<*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
Expand Down Expand Up @@ -153,3 +158,9 @@ waitListener = takeMVar . stateListener
-- the connectionWorker is the only mvar producer.
signalListener :: AppState -> IO ()
signalListener appState = void $ tryPutMVar (stateListener appState) ()

getIsListenerOn :: AppState -> IO Bool
getIsListenerOn = readIORef . stateIsListenerOn

putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
3 changes: 3 additions & 0 deletions src/PostgREST/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ exampleConfigFile =
|## when none is provided, 660 is applied by default
|# server-unix-socket-mode = "660"
|
|## admin server for health checks, it's disabled by default unless a port is specified
|# admin-server-port = 3001
|
|## determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely
|## admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-privileges"
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ data AppConfig = AppConfig
, configServerPort :: Int
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int
}

data LogLevel = LogCrit | LogError | LogWarn | LogInfo
Expand Down Expand Up @@ -147,6 +148,7 @@ toText conf =
,("server-port", show . configServerPort)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
]

-- quote all app.settings
Expand Down Expand Up @@ -242,6 +244,7 @@ parser optPath env dbSettings =
<*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> optInt "admin-server-port"
where
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
Expand Down
2 changes: 2 additions & 0 deletions src/PostgREST/Workers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ listener appState = do
case dbOrError of
Right db -> do
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
AppState.putIsListenerOn appState True
SQL.listen db $ SQL.toPgIdentifier dbChannel
SQL.waitForNotifications handleNotification db
_ ->
Expand All @@ -208,6 +209,7 @@ listener appState = do
handleFinally dbChannel _ = do
-- if the thread dies, we try to recover
AppState.logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
AppState.putIsListenerOn appState False
-- assume the pool connection was also lost, call the connection worker
connectionWorker appState
-- retry the listener
Expand Down
1 change: 1 addition & 0 deletions test/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configServerUnixSocketMode = 432
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerPort = Nothing
}

testCfg :: Text -> AppConfig
Expand Down
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/aliases.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/boolean-numeric.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/boolean-string.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/no-defaults-with-db.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/types.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
app.settings.test = "Bool False"
1 change: 1 addition & 0 deletions test/io-tests/configs/no-defaults-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ PGRST_SERVER_HOST: 0.0.0.0
PGRST_SERVER_PORT: 80
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_ADMIN_SERVER_PORT: 3001
1 change: 1 addition & 0 deletions test/io-tests/configs/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
41 changes: 41 additions & 0 deletions test/io-tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,44 @@ def test_db_prepared_statements_disable(defaultenv):
with run(env=env) as postgrest:
response = postgrest.session.post("/rpc/uses_prepared_statements")
assert response.text == "false"


def test_admin_healthy_w_channel(defaultenv):
"Should get a success response from the admin server health endpoint when the LISTEN channel is enabled"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
"PGRST_DB_CHANNEL_ENABLED": "true",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/health")
assert response.status_code == 200


def test_admin_healthy_wo_channel(defaultenv):
"Should get a success response from the admin server health endpoint when the LISTEN channel is disabled"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
"PGRST_DB_CHANNEL_ENABLED": "false",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/health")
assert response.status_code == 200


def test_admin_not_found(defaultenv):
"Should get a not found from the admin server"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/notfound")
assert response.status_code == 404

0 comments on commit ac3655d

Please sign in to comment.