Skip to content

Commit 11727ab

Browse files
committed
feat: add metric pgrst_jwt_cache_size in admin server
1 parent 3e1a904 commit 11727ab

File tree

8 files changed

+155
-15
lines changed

8 files changed

+155
-15
lines changed

docs/references/observability.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ pgrst_db_pool_max
169169

170170
Max pool connections.
171171

172+
JWT Cache Metric
173+
----------------
174+
175+
Related to the :ref:`jwt_caching`.
176+
177+
pgrst_jwt_cache_size
178+
~~~~~~~~~~~~~~~~~~~~
179+
180+
======== =======
181+
**Type** Gauge
182+
======== =======
183+
184+
Approximate JWT cache size in bytes.
185+
172186
Traces
173187
======
174188

postgrest.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ library
8888
PostgREST.Response.OpenAPI
8989
PostgREST.Response.GucHeader
9090
PostgREST.Response.Performance
91+
PostgREST.Utils
9192
PostgREST.Version
9293
other-modules: Paths_postgrest
9394
build-depends: base >= 4.9 && < 4.20
@@ -108,6 +109,7 @@ library
108109
, either >= 4.4.1 && < 5.1
109110
, extra >= 1.7.0 && < 2.0
110111
, fuzzyset >= 0.2.4 && < 0.3
112+
, ghc-heap >= 9.4 && < 9.9
111113
, gitrev >= 1.2 && < 1.4
112114
, hasql >= 1.6.1.1 && < 1.7
113115
, hasql-dynamic-statements >= 0.3.1 && < 0.4

src/PostgREST/App.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module PostgREST.App
1515
, run
1616
) where
1717

18-
18+
import Control.Monad
1919
import Control.Monad.Except (liftEither)
2020
import Data.Either.Combinators (mapLeft)
2121
import Data.Maybe (fromJust)
@@ -41,8 +41,7 @@ import qualified PostgREST.Response as Response
4141
import qualified PostgREST.Unix as Unix (installSignalHandlers)
4242

4343
import PostgREST.ApiRequest (ApiRequest (..))
44-
import PostgREST.AppState (AppState)
45-
import PostgREST.Auth (AuthResult (..))
44+
import PostgREST.AppState (AppState, AuthResult (..))
4645
import PostgREST.Config (AppConfig (..), LogLevel (..))
4746
import PostgREST.Config.PgVersion (PgVersion (..))
4847
import PostgREST.Error (Error)

src/PostgREST/Auth.hs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module PostgREST.Auth
1717
, getJwtDur
1818
, getRole
1919
, middleware
20+
, calcApproxCacheSizeInBytes
2021
) where
2122

2223
import qualified Data.Aeson as JSON
@@ -45,11 +46,13 @@ import System.Clock (TimeSpec (..))
4546
import System.IO.Unsafe (unsafePerformIO)
4647
import System.TimeIt (timeItT)
4748

48-
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
49-
getJwtCache, getTime)
50-
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
51-
JSPathExp (..))
52-
import PostgREST.Error (Error (..))
49+
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
50+
getJwtCache, getObserver, getTime)
51+
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
52+
JSPathExp (..))
53+
import PostgREST.Error (Error (..))
54+
import PostgREST.Observation (Observation (..))
55+
import PostgREST.Utils (recursiveSizeNF)
5356

5457
import Protolude
5558

@@ -177,14 +180,22 @@ middleware appState app req respond = do
177180
-- | Used to retrieve and insert JWT to JWT Cache
178181
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
179182
getJWTFromCache appState token maxLifetime parseJwt utc = do
180-
checkCache <- C.lookup (getJwtCache appState) token
183+
184+
checkCache <- C.lookup jwtCache token
181185
authResult <- maybe parseJwt (pure . Right) checkCache
182186

187+
183188
case (authResult,checkCache) of
184-
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
189+
(Right res, Nothing) -> C.insert' jwtCache (getTimeSpec res maxLifetime utc) token res
185190
_ -> pure ()
186191

192+
jwtCacheSize <- calcApproxCacheSizeInBytes jwtCache
193+
observer $ JWTCache jwtCacheSize
194+
187195
return authResult
196+
where
197+
observer = getObserver appState
198+
jwtCache = getJwtCache appState
188199

189200
-- Used to extract JWT exp claim and add to JWT Cache
190201
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
@@ -196,6 +207,25 @@ getTimeSpec res maxLifetime utc = do
196207
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
197208
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0
198209

210+
calcApproxCacheSizeInBytes :: C.Cache ByteString AuthResult -> IO Int
211+
calcApproxCacheSizeInBytes cache = do
212+
cacheItemsList <- C.toList cache
213+
return $ fromIntegral $ accumSize cacheItemsList
214+
where
215+
accumSize :: [(ByteString, AuthResult, Maybe TimeSpec)] -> Word
216+
accumSize lst = sum [ getSize (k,v) | (k,v,_) <- lst]
217+
218+
getSize :: (ByteString, AuthResult) -> Word
219+
getSize = unsafePerformIO . getSize'
220+
221+
getSize' :: (ByteString, AuthResult) -> IO Word
222+
getSize' (bs, ar) = do
223+
keySize <- recursiveSizeNF bs
224+
arClaimsSize <- recursiveSizeNF $ authClaims ar
225+
arRoleSize <- recursiveSizeNF $ authRole ar
226+
227+
return (keySize + arClaimsSize + arRoleSize)
228+
199229
authResultKey :: Vault.Key (Either Error AuthResult)
200230
authResultKey = unsafePerformIO Vault.newKey
201231
{-# NOINLINE authResultKey #-}

src/PostgREST/Metrics.hs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{-|
2-
Module : PostgREST.Logger
2+
Module : PostgREST.Metrics
33
Description : Metrics based on the Observation module. See Observation.hs.
44
-}
55
module PostgREST.Metrics
@@ -19,7 +19,7 @@ import PostgREST.Observation
1919
import Protolude
2020

2121
data MetricsState =
22-
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge
22+
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge Gauge
2323

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

3536
-- Only some observations are used as metrics
3637
observationMetrics :: MetricsState -> ObservationHandler
37-
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
38+
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime jwtCacheSize) obs = case obs of
3839
(PoolAcqTimeoutObs _) -> do
3940
incCounter poolTimeouts
4041
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
@@ -54,6 +55,8 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
5455
setGauge schemaCacheQueryTime resTime
5556
SchemaCacheErrorObs _ -> do
5657
withLabel schemaCacheLoads "FAIL" incCounter
58+
JWTCache cacheSize -> do
59+
setGauge jwtCacheSize $ fromIntegral cacheSize
5760
_ ->
5861
pure ()
5962

src/PostgREST/Observation.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ data Observation
5757
| HasqlPoolObs SQL.Observation
5858
| PoolRequest
5959
| PoolRequestFullfilled
60+
| JWTCache Int
6061

61-
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
62+
data ObsFatalError
63+
= ServerAuthError
64+
| ServerPgrstBug
65+
| ServerError42P05
66+
| ServerError08P01
6267

6368
type ObservationHandler = Observation -> IO ()
6469

src/PostgREST/Utils.hs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE MagicHash #-}
3+
{-# LANGUAGE UnboxedTuples #-}
4+
5+
module PostgREST.Utils (
6+
recursiveSizeNF
7+
) where
8+
9+
import Control.Monad
10+
11+
import GHC.Exts
12+
import GHC.Exts.Heap hiding (size)
13+
import GHC.Exts.Heap.Constants (wORD_SIZE)
14+
15+
import System.Mem
16+
17+
import Protolude
18+
19+
-- Code in this module is taken from:
20+
-- https://hackage.haskell.org/package/ghc-datasize-0.2.7
21+
22+
-- | Calculate size of GHC objects in Bytes. Note that an object may not be
23+
-- evaluated yet and only the size of the initial closure is returned.
24+
closureSize :: a -> IO Word
25+
closureSize x = do
26+
rawWds <- getClosureRawWords x
27+
return . fromIntegral $ length rawWds * wORD_SIZE
28+
29+
-- | Calculate the recursive size of GHC objects in Bytes. Note that the actual
30+
-- size in memory is calculated, so shared values are only counted once.
31+
--
32+
-- Call with
33+
-- @
34+
-- recursiveSize $! 2
35+
-- @
36+
-- to force evaluation to WHNF before calculating the size.
37+
--
38+
-- Call with
39+
-- @
40+
-- recursiveSize $!! \"foobar\"
41+
-- @
42+
-- ($!! from Control.DeepSeq) to force full evaluation before calculating the
43+
-- size.
44+
--
45+
-- A garbage collection is performed before the size is calculated, because
46+
-- the garbage collector would make heap walks difficult.
47+
--
48+
-- This function works very quickly on small data structures, but can be slow
49+
-- on large and complex ones. If speed is an issue it's probably possible to
50+
-- get the exact size of a small portion of the data structure and then
51+
-- estimate the total size from that.
52+
53+
recursiveSize :: a -> IO Word
54+
recursiveSize x = do
55+
performGC
56+
fmap snd $ go ([], 0) $ asBox x
57+
where go (!vs, !acc) b@(Box y) = do
58+
isElem <- or <$> mapM (areBoxesEqual b) vs
59+
if isElem
60+
then return (vs, acc)
61+
else do
62+
size <- closureSize y
63+
closure <- getClosureData y
64+
foldM go (b : vs, acc + size) $ allClosures closure
65+
66+
-- | Calculate the recursive size of GHC objects in Bytes after calling
67+
-- Control.DeepSeq.force on the data structure to force it into Normal Form.
68+
-- Using this function requires that the data structure has an `NFData`
69+
-- typeclass instance.
70+
71+
recursiveSizeNF :: NFData a => a -> IO Word
72+
recursiveSizeNF x = recursiveSize $!! x
73+
74+
-- | Adapted from 'GHC.Exts.Heap.getClosureRaw' which isn't exported.
75+
--
76+
-- This returns the raw words of the closure on the heap. Once back in the
77+
-- Haskell world, the raw words that hold pointers may be outdated after a
78+
-- garbage collector run.
79+
getClosureRawWords :: a -> IO [Word]
80+
getClosureRawWords x = do
81+
case unpackClosure# x of
82+
(# _iptr, dat, _pointers #) -> do
83+
let nelems = I# (sizeofByteArray# dat) `div` wORD_SIZE
84+
end = nelems - 1
85+
pure [W# (indexWordArray# dat i) | I# i <- [0.. end] ]

test/io/test_io.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,8 @@ def test_admin_metrics(defaultenv):
16321632
assert "pgrst_db_pool_available" in response.text
16331633
assert "pgrst_db_pool_timeouts_total" in response.text
16341634

1635+
assert "pgrst_jwt_cache_size" in response.text
1636+
16351637

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

0 commit comments

Comments
 (0)