Skip to content

Commit

Permalink
feat: add metric pgrst_jwt_cache_size in admin server
Browse files Browse the repository at this point in the history
  • Loading branch information
taimoorzaeem committed Nov 28, 2024
1 parent d78877c commit 9a23b43
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 14 deletions.
14 changes: 14 additions & 0 deletions docs/references/observability.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ pgrst_db_pool_max

Max pool connections.

JWT Cache Metric
----------------

Related to the :ref:`jwt_caching`.

pgrst_jwt_cache_size
~~~~~~~~~~~~~~~~~~~~

======== =======
**Type** Gauge
======== =======

Approximate JWT cache size in bytes.

Traces
======

Expand Down
3 changes: 3 additions & 0 deletions postgrest.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ library
PostgREST.Response.OpenAPI
PostgREST.Response.GucHeader
PostgREST.Response.Performance
PostgREST.Utils
PostgREST.Version
other-modules: Paths_postgrest
build-depends: base >= 4.9 && < 4.20
Expand All @@ -105,9 +106,11 @@ library
, containers >= 0.5.7 && < 0.7
, cookie >= 0.4.2 && < 0.5
, directory >= 1.2.6 && < 1.4
, deepseq >= 1.4 && < 1.5
, either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0
, fuzzyset >= 0.2.4 && < 0.3
, ghc-heap >= 9.4 && < 9.5
, gitrev >= 1.2 && < 1.4
, hasql >= 1.6.1.1 && < 1.7
, hasql-dynamic-statements >= 0.3.1 && < 0.4
Expand Down
5 changes: 2 additions & 3 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module PostgREST.App
, run
) where


import Control.Monad
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.Maybe (fromJust)
Expand All @@ -41,8 +41,7 @@ import qualified PostgREST.Response as Response
import qualified PostgREST.Unix as Unix (installSignalHandlers)

import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState)
import PostgREST.Auth (AuthResult (..))
import PostgREST.AppState (AppState, AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error)
Expand Down
42 changes: 36 additions & 6 deletions src/PostgREST/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module PostgREST.Auth
, getJwtDur
, getRole
, middleware
, calcApproxCacheSizeInBytes
) where

import qualified Data.Aeson as JSON
Expand Down Expand Up @@ -44,10 +45,12 @@ import System.Clock (TimeSpec (..))
import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT)

import PostgREST.AppState (AppState, AuthResult (..), getConfig,
getJwtCache, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Error (Error (..))
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
getJwtCache, getObserver, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Error (Error (..))
import PostgREST.Observation (Observation (..))
import PostgREST.Utils (recursiveSizeNF)

import Protolude

Expand Down Expand Up @@ -163,14 +166,22 @@ middleware appState app req respond = do
-- | Used to retrieve and insert JWT to JWT Cache
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
getJWTFromCache appState token maxLifetime parseJwt utc = do
checkCache <- C.lookup (getJwtCache appState) token

checkCache <- C.lookup jwtCache token
authResult <- maybe parseJwt (pure . Right) checkCache


case (authResult,checkCache) of
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
(Right res, Nothing) -> C.insert' jwtCache (getTimeSpec res maxLifetime utc) token res
_ -> pure ()

jwtCacheSize <- calcApproxCacheSizeInBytes jwtCache
observer $ JWTCache jwtCacheSize

return authResult
where
observer = getObserver appState
jwtCache = getJwtCache appState

-- Used to extract JWT exp claim and add to JWT Cache
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
Expand All @@ -182,6 +193,25 @@ getTimeSpec res maxLifetime utc = do
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0

calcApproxCacheSizeInBytes :: C.Cache ByteString AuthResult -> IO Int
calcApproxCacheSizeInBytes cache = do
cacheItemsList <- C.toList cache
return $ fromIntegral $ accumSize cacheItemsList
where
accumSize :: [(ByteString, AuthResult, Maybe TimeSpec)] -> Word
accumSize lst = sum [ getSize (k,v) | (k,v,_) <- lst]

getSize :: (ByteString, AuthResult) -> Word
getSize = unsafePerformIO . getSize'

getSize' :: (ByteString, AuthResult) -> IO Word
getSize' (bs, ar) = do
keySize <- recursiveSizeNF bs
arClaimsSize <- recursiveSizeNF $ authClaims ar
arRoleSize <- recursiveSizeNF $ authRole ar

return (keySize + arClaimsSize + arRoleSize)

authResultKey :: Vault.Key (Either Error AuthResult)
authResultKey = unsafePerformIO Vault.newKey
{-# NOINLINE authResultKey #-}
Expand Down
11 changes: 7 additions & 4 deletions src/PostgREST/Metrics.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-|
Module : PostgREST.Logger
Module : PostgREST.Metrics
Description : Metrics based on the Observation module. See Observation.hs.
-}
module PostgREST.Metrics
Expand All @@ -19,7 +19,7 @@ import PostgREST.Observation
import Protolude

data MetricsState =
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge Gauge

init :: Int -> IO MetricsState
init configDbPoolSize = do
Expand All @@ -29,12 +29,13 @@ init configDbPoolSize = do
poolMaxSize <- register $ gauge (Info "pgrst_db_pool_max" "Max pool connections")
schemaCacheLoads <- register $ vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")
schemaCacheQueryTime <- register $ gauge (Info "pgrst_schema_cache_query_time_seconds" "The query time in seconds of the last schema cache load")
jwtCacheSize <- register $ gauge (Info "pgrst_jwt_cache_size" "The number of cached JWTs")
setGauge poolMaxSize (fromIntegral configDbPoolSize)
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime jwtCacheSize

-- Only some observations are used as metrics
observationMetrics :: MetricsState -> ObservationHandler
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime jwtCacheSize) obs = case obs of
(PoolAcqTimeoutObs _) -> do
incCounter poolTimeouts
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
Expand All @@ -54,6 +55,8 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
setGauge schemaCacheQueryTime resTime
SchemaCacheErrorObs _ -> do
withLabel schemaCacheLoads "FAIL" incCounter
JWTCache cacheSize -> do
setGauge jwtCacheSize $ fromIntegral cacheSize
_ ->
pure ()

Expand Down
7 changes: 6 additions & 1 deletion src/PostgREST/Observation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ data Observation
| HasqlPoolObs SQL.Observation
| PoolRequest
| PoolRequestFullfilled
| JWTCache Int

data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
data ObsFatalError
= ServerAuthError
| ServerPgrstBug
| ServerError42P05
| ServerError08P01

type ObservationHandler = Observation -> IO ()

Expand Down
88 changes: 88 additions & 0 deletions src/PostgREST/Utils.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

module PostgREST.Utils (
closureSize,
recursiveSize,
recursiveSizeNF
) where

import Control.DeepSeq (NFData, ($!!))
import Control.Monad

import GHC.Exts
import GHC.Exts.Heap hiding (size)
import GHC.Exts.Heap.Constants (wORD_SIZE)

import System.Mem

import Prelude

-- Code in this module is taken from:
-- https://hackage.haskell.org/package/ghc-datasize-0.2.7

-- | Calculate size of GHC objects in Bytes. Note that an object may not be
-- evaluated yet and only the size of the initial closure is returned.
closureSize :: a -> IO Word
closureSize x = do
rawWds <- getClosureRawWords x
return . fromIntegral $ length rawWds * wORD_SIZE

-- | Calculate the recursive size of GHC objects in Bytes. Note that the actual
-- size in memory is calculated, so shared values are only counted once.
--
-- Call with
-- @
-- recursiveSize $! 2
-- @
-- to force evaluation to WHNF before calculating the size.
--
-- Call with
-- @
-- recursiveSize $!! \"foobar\"
-- @
-- ($!! from Control.DeepSeq) to force full evaluation before calculating the
-- size.
--
-- A garbage collection is performed before the size is calculated, because
-- the garbage collector would make heap walks difficult.
--
-- This function works very quickly on small data structures, but can be slow
-- on large and complex ones. If speed is an issue it's probably possible to
-- get the exact size of a small portion of the data structure and then
-- estimate the total size from that.

recursiveSize :: a -> IO Word
recursiveSize x = do
performGC
fmap snd $ go ([], 0) $ asBox x
where go (!vs, !acc) b@(Box y) = do
isElem <- or <$> mapM (areBoxesEqual b) vs
if isElem
then return (vs, acc)
else do
size <- closureSize y
closure <- getClosureData y
foldM go (b : vs, acc + size) $ allClosures closure

-- | Calculate the recursive size of GHC objects in Bytes after calling
-- Control.DeepSeq.force on the data structure to force it into Normal Form.
-- Using this function requires that the data structure has an `NFData`
-- typeclass instance.

recursiveSizeNF :: NFData a => a -> IO Word
recursiveSizeNF x = recursiveSize $!! x

-- | Adapted from 'GHC.Exts.Heap.getClosureRaw' which isn't exported.
--
-- This returns the raw words of the closure on the heap. Once back in the
-- Haskell world, the raw words that hold pointers may be outdated after a
-- garbage collector run.
getClosureRawWords :: a -> IO [Word]
getClosureRawWords x = do
case unpackClosure# x of
(# _iptr, dat, _pointers #) -> do
let nelems = I# (sizeofByteArray# dat) `div` wORD_SIZE
end = nelems - 1
pure [W# (indexWordArray# dat i) | I# i <- [0.. end] ]
2 changes: 2 additions & 0 deletions test/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,8 @@ def test_admin_metrics(defaultenv):
assert "pgrst_db_pool_available" in response.text
assert "pgrst_db_pool_timeouts_total" in response.text

assert "pgrst_jwt_cache_size" in response.text


def test_schema_cache_startup_load_with_in_db_config(defaultenv, metapostgrest):
"verify that the Schema Cache loads correctly at startup, using the in-db `pgrst.db_schemas` config"
Expand Down

0 comments on commit 9a23b43

Please sign in to comment.