feat: minimal health check (#2092)
This commit is contained in:
@@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### 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
|
### Fixed
|
||||||
|
|
||||||
- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
|
- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
|
||||||
|
|||||||
+24
-2
@@ -30,9 +30,10 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Set as S
|
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.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 Hasql.Transaction.Sessions as SQL
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
import qualified Network.HTTP.Types.Status as HTTP
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
@@ -113,6 +114,11 @@ run installHandlers maybeRunWithSocket appState = do
|
|||||||
when configDbChannelEnabled $ listener appState
|
when configDbChannelEnabled $ listener appState
|
||||||
|
|
||||||
let app = postgrest configLogLevel appState (connectionWorker 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
|
case configServerUnixSocket of
|
||||||
Just socket ->
|
Just socket ->
|
||||||
@@ -127,6 +133,9 @@ run installHandlers maybeRunWithSocket appState = do
|
|||||||
do
|
do
|
||||||
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
||||||
Warp.runSettings (serverSettings conf) app
|
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 -> Warp.Settings
|
||||||
serverSettings AppConfig{..} =
|
serverSettings AppConfig{..} =
|
||||||
@@ -135,6 +144,19 @@ serverSettings AppConfig{..} =
|
|||||||
& setPort configServerPort
|
& setPort configServerPort
|
||||||
& setServerName ("postgrest/" <> prettyVersion)
|
& 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 application
|
||||||
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
||||||
postgrest logLev appState connWorker =
|
postgrest logLev appState connWorker =
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module PostgREST.AppState
|
|||||||
( AppState
|
( AppState
|
||||||
, getConfig
|
, getConfig
|
||||||
, getDbStructure
|
, getDbStructure
|
||||||
|
, getIsListenerOn
|
||||||
, getIsWorkerOn
|
, getIsWorkerOn
|
||||||
, getJsonDbS
|
, getJsonDbS
|
||||||
, getMainThreadId
|
, getMainThreadId
|
||||||
@@ -16,6 +17,7 @@ module PostgREST.AppState
|
|||||||
, logWithZTime
|
, logWithZTime
|
||||||
, putConfig
|
, putConfig
|
||||||
, putDbStructure
|
, putDbStructure
|
||||||
|
, putIsListenerOn
|
||||||
, putIsWorkerOn
|
, putIsWorkerOn
|
||||||
, putJsonDbS
|
, putJsonDbS
|
||||||
, putPgVersion
|
, putPgVersion
|
||||||
@@ -53,6 +55,8 @@ data AppState = AppState
|
|||||||
, stateIsWorkerOn :: IORef Bool
|
, stateIsWorkerOn :: IORef Bool
|
||||||
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
||||||
, stateListener :: MVar ()
|
, stateListener :: MVar ()
|
||||||
|
-- | State of the LISTEN channel, used for health checks
|
||||||
|
, stateIsListenerOn :: IORef Bool
|
||||||
-- | Config that can change at runtime
|
-- | Config that can change at runtime
|
||||||
, stateConf :: IORef AppConfig
|
, stateConf :: IORef AppConfig
|
||||||
-- | Time used for verifying JWT expiration
|
-- | Time used for verifying JWT expiration
|
||||||
@@ -78,6 +82,7 @@ initWithPool newPool conf =
|
|||||||
<*> newIORef mempty
|
<*> newIORef mempty
|
||||||
<*> newIORef False
|
<*> newIORef False
|
||||||
<*> newEmptyMVar
|
<*> newEmptyMVar
|
||||||
|
<*> newIORef False
|
||||||
<*> newIORef conf
|
<*> newIORef conf
|
||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
@@ -153,3 +158,9 @@ waitListener = takeMVar . stateListener
|
|||||||
-- the connectionWorker is the only mvar producer.
|
-- the connectionWorker is the only mvar producer.
|
||||||
signalListener :: AppState -> IO ()
|
signalListener :: AppState -> IO ()
|
||||||
signalListener appState = void $ tryPutMVar (stateListener appState) ()
|
signalListener appState = void $ tryPutMVar (stateListener appState) ()
|
||||||
|
|
||||||
|
getIsListenerOn :: AppState -> IO Bool
|
||||||
|
getIsListenerOn = readIORef . stateIsListenerOn
|
||||||
|
|
||||||
|
putIsListenerOn :: AppState -> Bool -> IO ()
|
||||||
|
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ exampleConfigFile =
|
|||||||
|## when none is provided, 660 is applied by default
|
|## when none is provided, 660 is applied by default
|
||||||
|# server-unix-socket-mode = "660"
|
|# 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
|
|## determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely
|
||||||
|## admitted values: follow-privileges, ignore-privileges, disabled
|
|## admitted values: follow-privileges, ignore-privileges, disabled
|
||||||
|openapi-mode = "follow-privileges"
|
|openapi-mode = "follow-privileges"
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ data AppConfig = AppConfig
|
|||||||
, configServerPort :: Int
|
, configServerPort :: Int
|
||||||
, configServerUnixSocket :: Maybe FilePath
|
, configServerUnixSocket :: Maybe FilePath
|
||||||
, configServerUnixSocketMode :: FileMode
|
, configServerUnixSocketMode :: FileMode
|
||||||
|
, configAdminServerPort :: Maybe Int
|
||||||
}
|
}
|
||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||||
@@ -147,6 +148,7 @@ toText conf =
|
|||||||
,("server-port", show . configServerPort)
|
,("server-port", show . configServerPort)
|
||||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||||
|
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- quote all app.settings
|
-- quote all app.settings
|
||||||
@@ -242,6 +244,7 @@ parser optPath env dbSettings =
|
|||||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||||
|
<*> optInt "admin-server-port"
|
||||||
where
|
where
|
||||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ listener appState = do
|
|||||||
case dbOrError of
|
case dbOrError of
|
||||||
Right db -> do
|
Right db -> do
|
||||||
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
||||||
|
AppState.putIsListenerOn appState True
|
||||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
SQL.waitForNotifications handleNotification db
|
SQL.waitForNotifications handleNotification db
|
||||||
_ ->
|
_ ->
|
||||||
@@ -208,6 +209,7 @@ listener appState = do
|
|||||||
handleFinally dbChannel _ = do
|
handleFinally dbChannel _ = do
|
||||||
-- if the thread dies, we try to recover
|
-- if the thread dies, we try to recover
|
||||||
AppState.logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
|
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
|
-- assume the pool connection was also lost, call the connection worker
|
||||||
connectionWorker appState
|
connectionWorker appState
|
||||||
-- retry the listener
|
-- retry the listener
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
|||||||
, configServerUnixSocketMode = 432
|
, configServerUnixSocketMode = 432
|
||||||
, configDbTxAllowOverride = True
|
, configDbTxAllowOverride = True
|
||||||
, configDbTxRollbackAll = True
|
, configDbTxRollbackAll = True
|
||||||
|
, configAdminServerPort = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
testCfg :: Text -> AppConfig
|
testCfg :: Text -> AppConfig
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ server-host = "!4"
|
|||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ server-host = "!4"
|
|||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ server-host = "!4"
|
|||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ server-host = "!4"
|
|||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ server-host = "0.0.0.0"
|
|||||||
server-port = 80
|
server-port = 80
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
|
admin-server-port = 3001
|
||||||
app.settings.test = "test"
|
app.settings.test = "test"
|
||||||
app.settings.test2 = "test"
|
app.settings.test2 = "test"
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ server-host = "0.0.0.0"
|
|||||||
server-port = 80
|
server-port = 80
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
|
admin-server-port = 3001
|
||||||
app.settings.test = "test"
|
app.settings.test = "test"
|
||||||
app.settings.test2 = "test"
|
app.settings.test2 = "test"
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ server-host = "0.0.0.0"
|
|||||||
server-port = 80
|
server-port = 80
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
|
admin-server-port = 3001
|
||||||
app.settings.test = "test"
|
app.settings.test = "test"
|
||||||
app.settings.test2 = "test"
|
app.settings.test2 = "test"
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ server-host = "!4"
|
|||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
|
admin-server-port = ""
|
||||||
app.settings.test = "Bool False"
|
app.settings.test = "Bool False"
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ PGRST_SERVER_HOST: 0.0.0.0
|
|||||||
PGRST_SERVER_PORT: 80
|
PGRST_SERVER_PORT: 80
|
||||||
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
||||||
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
||||||
|
PGRST_ADMIN_SERVER_PORT: 3001
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ server-host = "0.0.0.0"
|
|||||||
server-port = 80
|
server-port = 80
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
|
admin-server-port = 3001
|
||||||
app.settings.test = "test"
|
app.settings.test = "test"
|
||||||
app.settings.test2 = "test"
|
app.settings.test2 = "test"
|
||||||
|
|||||||
@@ -729,3 +729,44 @@ def test_db_prepared_statements_disable(defaultenv):
|
|||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
||||||
assert response.text == "false"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user