feat: connection pool metrics in admin server

This commit is contained in:
steve-chavez
2024-04-23 19:08:37 -05:00
committed by Steve Chavez
parent 29cd7d195c
commit 653c7955b2
10 changed files with 116 additions and 20 deletions
+2
View File
@@ -15,6 +15,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2676, Performance improvement on bulk json inserts, around 10% increase on requests per second by removing `json_typeof` from write queries - @steve-chavez - #2676, Performance improvement on bulk json inserts, around 10% increase on requests per second by removing `json_typeof` from write queries - @steve-chavez
- #3214, Log connection pool events on log-level=info - @steve-chavez - #3214, Log connection pool events on log-level=info - @steve-chavez
- #3435, Add log-level=debug, for development purposes - @steve-chavez - #3435, Add log-level=debug, for development purposes - @steve-chavez
- #1526, Add `/metrics` endpoint on admin server - @steve-chavez
- Exposes connection pool metrics
### Fixed ### Fixed
+2
View File
@@ -64,6 +64,7 @@ library
PostgREST.Error PostgREST.Error
PostgREST.Logger PostgREST.Logger
PostgREST.MediaType PostgREST.MediaType
PostgREST.Metrics
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
@@ -124,6 +125,7 @@ library
, optparse-applicative >= 0.13 && < 0.19 , optparse-applicative >= 0.13 && < 0.19
, parsec >= 3.1.11 && < 3.2 , parsec >= 3.1.11 && < 3.2
, postgresql-libpq >= 0.10 , postgresql-libpq >= 0.10
, prometheus-client >= 1.1.1 && < 1.2.0
, protolude >= 0.3.1 && < 0.4 , protolude >= 0.3.1 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.4 , regex-tdfa >= 1.2.2 && < 1.4
, retry >= 0.7.4 && < 0.10 , retry >= 0.7.4 && < 0.10
+5
View File
@@ -19,11 +19,13 @@ import Network.Socket.ByteString
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Metrics (metricsToText)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config import qualified PostgREST.Config as Config
import Protolude import Protolude
runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO () runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO ()
@@ -56,6 +58,9 @@ admin appState appConfig req respond = do
["schema_cache"] -> do ["schema_cache"] -> do
sCache <- AppState.getSchemaCache appState sCache <- AppState.getSchemaCache appState
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache) respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
["metrics"] -> do
mets <- metricsToText
respond $ Wai.responseLBS HTTP.status200 [] mets
_ -> _ ->
respond $ Wai.responseLBS HTTP.status404 [] mempty respond $ Wai.responseLBS HTTP.status404 [] mempty
+13 -8
View File
@@ -46,6 +46,7 @@ import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Socket as NS import qualified Network.Socket as NS
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation import PostgREST.Observation
import PostgREST.Version (prettyVersion) import PostgREST.Version (prettyVersion)
import System.TimeIt (timeItT) import System.TimeIt (timeItT)
@@ -111,25 +112,28 @@ data AppState = AppState
, stateSocketREST :: NS.Socket , stateSocketREST :: NS.Socket
-- | Network socket for the admin UI -- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket , stateSocketAdmin :: Maybe NS.Socket
-- | Logger state
, stateLogger :: Logger.LoggerState
-- | Observation handler -- | Observation handler
, stateObserver :: ObservationHandler , stateObserver :: ObservationHandler
, stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState
} }
type AppSockets = (NS.Socket, Maybe NS.Socket) type AppSockets = (NS.Socket, Maybe NS.Socket)
init :: AppConfig -> IO AppState init :: AppConfig -> IO AppState
init conf@AppConfig{configLogLevel} = do init conf@AppConfig{configLogLevel, configDbPoolSize} = do
loggerState <- Logger.init loggerState <- Logger.init
let observer = Logger.observationLogger loggerState configLogLevel metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
pool <- initPool conf observer pool <- initPool conf observer
(sock, adminSock) <- initSockets conf (sock, adminSock) <- initSockets conf
state' <- initWithPool (sock, adminSock) pool conf loggerState observer state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock} pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> ObservationHandler -> IO AppState initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf loggerState observer = do initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
appState <- AppState pool appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -145,8 +149,9 @@ initWithPool (sock, adminSock) pool conf loggerState observer = do
<*> C.newCache Nothing <*> C.newCache Nothing
<*> pure sock <*> pure sock
<*> pure adminSock <*> pure adminSock
<*> pure loggerState
<*> pure observer <*> pure observer
<*> pure loggerState
<*> pure metricsState
debWorker <- debWorker <-
let decisecond = 100000 in let decisecond = 100000 in
+4
View File
@@ -88,6 +88,10 @@ observationLogger loggerState logLevel obs = case obs of
o@(SchemaCacheLoadedObs _) -> do o@(SchemaCacheLoadedObs _) -> do
when (logLevel >= LogDebug) $ do when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
PoolRequest ->
pure ()
PoolRequestFullfilled ->
pure ()
o -> o ->
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
+55
View File
@@ -0,0 +1,55 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
module PostgREST.Metrics
( init
, MetricsState (..)
, observationMetrics
, metricsToText
) where
import qualified Data.ByteString.Lazy as LBS
import qualified Hasql.Pool.Observation as SQL
import qualified Prometheus as Prom
import PostgREST.Observation
import Protolude
data MetricsState = MetricsState
{ poolTimeouts :: Prom.Counter
, poolAvailable :: Prom.Gauge
, poolWaiting :: Prom.Gauge
, poolMaxSize :: Prom.Gauge
}
init :: Int -> IO MetricsState
init poolMaxSize = do
timeouts <- Prom.register $ Prom.counter (Prom.Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")
available <- Prom.register $ Prom.gauge (Prom.Info "pgrst_db_pool_available" "Available connections in the pool")
waiting <- Prom.register $ Prom.gauge (Prom.Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")
maxSize <- Prom.register $ Prom.gauge (Prom.Info "pgrst_db_pool_max" "Max pool connections")
Prom.setGauge maxSize (fromIntegral poolMaxSize)
pure $ MetricsState timeouts available waiting maxSize
observationMetrics :: MetricsState -> ObservationHandler
observationMetrics MetricsState{poolTimeouts, poolAvailable, poolWaiting} obs = case obs of
(PoolAcqTimeoutObs _) -> do
Prom.incCounter poolTimeouts
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
SQL.ReadyForUseConnectionStatus -> do
Prom.incGauge poolAvailable
SQL.InUseConnectionStatus -> do
Prom.decGauge poolAvailable
SQL.TerminatedConnectionStatus _ -> do
Prom.decGauge poolAvailable
SQL.ConnectingConnectionStatus -> pure ()
PoolRequest ->
Prom.incGauge poolWaiting
PoolRequestFullfilled ->
Prom.decGauge poolWaiting
_ ->
pure ()
metricsToText :: IO LBS.ByteString
metricsToText = Prom.exportMetricsAsText
+3
View File
@@ -52,6 +52,8 @@ data Observation
| QueryErrorCodeHighObs SQL.UsageError | QueryErrorCodeHighObs SQL.UsageError
| PoolAcqTimeoutObs SQL.UsageError | PoolAcqTimeoutObs SQL.UsageError
| HasqlPoolObs SQL.Observation | HasqlPoolObs SQL.Observation
| PoolRequest
| PoolRequestFullfilled
type ObservationHandler = Observation -> IO () type ObservationHandler = Observation -> IO ()
@@ -125,6 +127,7 @@ observationMessage = \case
SQL.ReleaseConnectionTerminationReason -> "release" SQL.ReleaseConnectionTerminationReason -> "release"
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message. SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
) )
_ -> mempty
where where
showMillis :: Double -> Text showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) (x * 1000) "" showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""
+7
View File
@@ -43,6 +43,7 @@ import PostgREST.Config (AppConfig (..),
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.Observation (Observation (..))
import PostgREST.Plan (ActionPlan (..), import PostgREST.Plan (ActionPlan (..),
CallReadPlan (..), CallReadPlan (..),
CrudPlan (..), CrudPlan (..),
@@ -77,10 +78,16 @@ data QueryResult
runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult
runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x
runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do
let observer = AppState.getObserver appState
lift $ observer PoolRequest
dbResp <- lift $ do dbResp <- lift $ do
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler) AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler)
lift $ observer PoolRequestFullfilled
resp <- resp <-
liftEither . mapLeft Error.PgErr $ liftEither . mapLeft Error.PgErr $
mapLeft (Error.PgError authenticated) dbResp mapLeft (Error.PgError authenticated) dbResp
+12
View File
@@ -1427,3 +1427,15 @@ def test_multiple_func_settings(defaultenv):
response = postgrest.session.post("/rpc/multiple_func_settings_test") response = postgrest.session.post("/rpc/multiple_func_settings_test")
assert response.text == '[{"work_mem":"5000kB","statement_timeout":"10s"}]' assert response.text == '[{"work_mem":"5000kB","statement_timeout":"10s"}]'
def test_admin_metrics(defaultenv):
"Should get metrics from the admin endpoint"
with run(env=defaultenv, port=freeport()) as postgrest:
response = postgrest.admin.get("/metrics")
assert response.status_code == 200
assert "pgrst_db_pool_max" in response.text
assert "pgrst_db_pool_waiting" in response.text
assert "pgrst_db_pool_available" in response.text
assert "pgrst_db_pool_timeouts_total" in response.text
+10 -9
View File
@@ -17,6 +17,7 @@ import SpecHelper
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import qualified Feature.Auth.AsymmetricJwtSpec import qualified Feature.Auth.AsymmetricJwtSpec
import qualified Feature.Auth.AudienceJwtSecretSpec import qualified Feature.Auth.AudienceJwtSecretSpec
@@ -70,7 +71,6 @@ import qualified Feature.RpcPreRequestGucsSpec
main :: IO () main :: IO ()
main = do main = do
let observer = const $ pure ()
pool <- P.acquire $ P.settings pool <- P.acquire $ P.settings
[ P.size 3 [ P.size 3
, P.acquisitionTimeout 10 , P.acquisitionTimeout 10
@@ -85,22 +85,22 @@ main = do
baseSchemaCache <- loadSCache pool testCfg baseSchemaCache <- loadSCache pool testCfg
sockets <- AppState.initSockets testCfg sockets <- AppState.initSockets testCfg
loggerState <- Logger.init loggerState <- Logger.init
metricsState <- Metrics.init (configDbPoolSize testCfg)
let let
-- For tests that run with the same refSchemaCache initApp sCache config = do
app config = do appState <- AppState.initWithPool sockets pool config loggerState metricsState (const $ pure ())
appState <- AppState.initWithPool sockets pool config loggerState observer
AppState.putPgVersion appState actualPgVersion AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just baseSchemaCache) AppState.putSchemaCache appState (Just sCache)
return ((), postgrest (configLogLevel config) appState (pure ())) return ((), postgrest (configLogLevel config) appState (pure ()))
-- For tests that run with the same schema cache
app = initApp baseSchemaCache
-- For tests that run with a different SchemaCache (depends on configSchemas) -- For tests that run with a different SchemaCache (depends on configSchemas)
appDbs config = do appDbs config = do
customSchemaCache <- loadSCache pool config customSchemaCache <- loadSCache pool config
appState <- AppState.initWithPool sockets pool config loggerState observer initApp customSchemaCache config
AppState.putPgVersion appState actualPgVersion
AppState.putSchemaCache appState (Just customSchemaCache)
return ((), postgrest (configLogLevel config) appState (pure ()))
let withApp = app testCfg let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg maxRowsApp = app testMaxRowsCfg
@@ -280,3 +280,4 @@ main = do
where where
loadSCache pool conf = loadSCache pool conf =
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf) either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)