Skip to content

Commit ada4cf3

Browse files
committed
feat: add metric pgrst_jwt_cache_size_bytes in admin server
1 parent 2b91df8 commit ada4cf3

File tree

13 files changed

+177
-22
lines changed

13 files changed

+177
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1717
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
1818
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
1919
- #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem
20+
- #3802, Add metric `pgrst_jwt_cache_size_bytes` in admin server - @taimoorzaeem
2021

2122
### Fixed
2223

default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ rec {
8585
lib.enableExecutableProfiling
8686
lib.enableLibraryProfiling
8787
lib.dontHaddock
88+
(drv: lib.appendConfigureFlags drv [ "--ghc-option=-DJWT_CACHE_METRIC" ])
89+
(drv: lib.appendConfigureFlags drv [ "--flags=jwt-cache-metric" ])
8890
];
8991

9092
inherit (postgrest) env;

docs/references/observability.rst

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

196196
Max pool connections.
197197

198+
JWT Cache Metric
199+
----------------
200+
201+
Related to the :ref:`jwt_caching`.
202+
203+
pgrst_jwt_cache_size_bytes
204+
~~~~~~~~~~~~~~~~~~~~~~~~~~
205+
206+
======== =======
207+
**Type** Gauge
208+
======== =======
209+
210+
The JWT cache size in bytes.
211+
198212
Traces
199213
======
200214

nix/overlays/haskell-packages.nix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ let
5050
# jailbreak, because hspec limit for tests
5151
fuzzyset = prev.fuzzyset_0_2_4;
5252

53+
# TODO: Remove this once https://github.com/NixOS/nixpkgs/pull/375121
54+
# has made it to us.
55+
ghc-datasize = lib.markUnbroken prev.ghc-datasize;
56+
5357
hasql-pool = lib.dontCheck (prev.callHackageDirect
5458
{
5559
pkg = "hasql-pool";

postgrest.cabal

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ flag hpc
4040
manual: True
4141
description: Enable HPC (dev only)
4242

43+
flag jwt-cache-metric
44+
default: False
45+
manual: True
46+
description: Allow memory tests to run without ghc-datasize
47+
4348
library
4449
default-language: Haskell2010
4550
default-extensions: OverloadedStrings
@@ -110,6 +115,7 @@ library
110115
, either >= 4.4.1 && < 5.1
111116
, extra >= 1.7.0 && < 2.0
112117
, fuzzyset >= 0.2.4 && < 0.3
118+
, ghc-heap >= 9.4 && < 9.9
113119
, hasql >= 1.6.1.1 && < 1.7
114120
, hasql-dynamic-statements >= 0.3.1 && < 0.4
115121
, hasql-notifications >= 0.2.2.2 && < 0.2.3
@@ -168,6 +174,9 @@ library
168174
else
169175
ghc-options: -O2
170176

177+
if !flag(jwt-cache-metric)
178+
build-depends: ghc-datasize >= 0.2.7 && < 0.3
179+
171180
if !os(windows)
172181
build-depends:
173182
unix

src/PostgREST/Auth.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import System.IO.Unsafe (unsafePerformIO)
4444
import System.TimeIt (timeItT)
4545

4646
import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
47-
getTime)
47+
getObserver, getTime)
4848
import PostgREST.Auth.JwtCache (lookupJwtCache)
4949
import PostgREST.Auth.Types (AuthResult (..))
5050
import PostgREST.Config (AppConfig (..), FilterExp (..),
@@ -161,6 +161,7 @@ middleware appState app req respond = do
161161
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
162162
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
163163
jwtCacheState = getJwtCacheState appState
164+
observer = getObserver appState
164165

165166
-- If ServerTimingEnabled -> calculate JWT validation time
166167
-- If JwtCacheMaxLifetime -> cache JWT validation result
@@ -171,7 +172,7 @@ middleware appState app req respond = do
171172

172173
(True, maxLifetime) -> do
173174
(dur, authResult) <- timeItT $ case token of
174-
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
175+
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time observer
175176
Nothing -> parseJwt
176177
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
177178

@@ -181,7 +182,7 @@ middleware appState app req respond = do
181182

182183
(False, maxLifetime) -> do
183184
authResult <- case token of
184-
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
185+
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time observer
185186
Nothing -> parseJwt
186187
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
187188

src/PostgREST/Auth/JwtCache.hs

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Description : PostgREST Jwt Authentication Result Cache.
44
55
This module provides functions to deal with the JWT cache
66
-}
7-
{-# LANGUAGE NamedFieldPuns #-}
7+
{-# LANGUAGE CPP #-}
88
module PostgREST.Auth.JwtCache
99
( init
1010
, JwtCacheState
@@ -18,30 +18,42 @@ import qualified Data.Scientific as Sci
1818

1919
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
2020
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
21-
import System.Clock (TimeSpec (..))
21+
#ifndef JWT_CACHE_METRIC
22+
import GHC.DataSize (recursiveSizeNF)
23+
#endif
24+
import System.Clock (TimeSpec (..))
2225

23-
import PostgREST.Auth.Types (AuthResult (..))
24-
import PostgREST.Error (Error (..))
26+
import PostgREST.Auth.Types (AuthResult (..))
27+
import PostgREST.Error (Error (..))
28+
import PostgREST.Observation (Observation (..), ObservationHandler)
2529

30+
import Control.Debounce
2631
import Protolude
2732

28-
newtype JwtCacheState = JwtCacheState
29-
{ jwtCache :: C.Cache ByteString AuthResult
33+
type SizeInBytes = Int
34+
35+
-- TODO: EXPLAIN HERE WHY WE ARE STORING THE SIZE IN CACHE, Remove the below line
36+
-- https://github.com/PostgREST/postgrest/pull/3802#discussion_r1971074445
37+
data JwtCacheState = JwtCacheState
38+
-- | Jwt Cache
39+
{ jwtCache :: C.Cache ByteString (AuthResult,SizeInBytes)
40+
-- | Calculate cache size with debounce
41+
, cacheSizeCalcDebounceTimeout :: MVar (IO ())
3042
}
3143

3244
-- | Initialize JwtCacheState
3345
init :: IO JwtCacheState
3446
init = do
3547
cache <- C.newCache Nothing -- no default expiration
36-
return $ JwtCacheState cache
48+
JwtCacheState cache <$> newEmptyMVar
3749

3850
-- | Used to retrieve and insert JWT to JWT Cache
39-
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
40-
lookupJwtCache JwtCacheState{jwtCache} token maxLifetime parseJwt utc = do
41-
checkCache <- C.lookup jwtCache token
42-
authResult <- maybe parseJwt (pure . Right) checkCache
51+
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> ObservationHandler -> IO (Either Error AuthResult)
52+
lookupJwtCache jwtCacheState token maxLifetime parseJwt utc observer = do
53+
checkCache <- C.lookup (jwtCache jwtCacheState) token
54+
authResult <- maybe parseJwt (pure . Right . fst) checkCache
4355

44-
case (authResult,checkCache) of
56+
case (authResult, checkCache) of
4557
-- From comment:
4658
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
4759
--
@@ -56,13 +68,20 @@ lookupJwtCache JwtCacheState{jwtCache} token maxLifetime parseJwt utc = do
5668

5769
(Right res, Nothing) -> do -- cache miss
5870

71+
-- get expiration time
5972
let timeSpec = getTimeSpec res maxLifetime utc
6073

6174
-- purge expired cache entries
62-
C.purgeExpired jwtCache
75+
C.purgeExpired (jwtCache jwtCacheState)
76+
77+
-- calculate size of the cache entry to store it with authResult
78+
sz <- calcCacheEntrySizeInBytes (token,res,timeSpec)
6379

64-
-- insert new cache entry
65-
C.insert' jwtCache (Just timeSpec) token res
80+
-- insert new cache entry with byte size
81+
C.insert' (jwtCache jwtCacheState) (Just timeSpec) token (res,sz)
82+
83+
-- calculate complete cache size with debounce and log it
84+
updateCacheSizeWithDebounce jwtCacheState observer
6685

6786
_ -> pure ()
6887

@@ -77,3 +96,61 @@ getTimeSpec res maxLifetime utc = do
7796
case expireJSON of
7897
Just (JSON.Number seconds) -> TimeSpec (sciToInt seconds - utcToSecs utc) 0
7998
_ -> TimeSpec (fromIntegral maxLifetime :: Int64) 0
99+
100+
-- | Update JwtCacheSize Metric
101+
--
102+
-- Runs the cache size calculation with debounce
103+
updateCacheSizeWithDebounce :: JwtCacheState -> ObservationHandler -> IO ()
104+
updateCacheSizeWithDebounce jwtCacheState observer = do
105+
cSizeDebouncer <- tryReadMVar $ cacheSizeCalcDebounceTimeout jwtCacheState
106+
case cSizeDebouncer of
107+
Just d -> d
108+
Nothing -> do
109+
newDebouncer <-
110+
mkDebounce defaultDebounceSettings
111+
-- debounceFreq is set to default 1 second
112+
{ debounceAction = calculateSizeThenLog
113+
, debounceEdge = leadingEdge -- logs at the start and the end
114+
}
115+
putMVar (cacheSizeCalcDebounceTimeout jwtCacheState) newDebouncer
116+
newDebouncer
117+
where
118+
calculateSizeThenLog :: IO ()
119+
calculateSizeThenLog = do
120+
entries <- C.toList $ jwtCache jwtCacheState
121+
-- extract the size from each entry and sum them all
122+
let size = sum [ sz | (_,(_,sz),_) <- entries]
123+
observer $ JwtCache size -- updates and logs the metric
124+
125+
-- | Calculate JWT Cache Size in Bytes
126+
--
127+
-- The cache size is updated by calculating the size of every
128+
-- cache entry and updating the metric.
129+
--
130+
-- The cache entry consists of
131+
-- key :: ByteString
132+
-- value :: AuthReult
133+
-- expire value :: TimeSpec
134+
--
135+
-- We calculate the size of each cache entry component
136+
-- by using recursiveSizeNF function which first evaluates
137+
-- the data structure to Normal Form and then calculate size.
138+
-- The normal form evaluation is necessary for accurate size
139+
-- calculation because haskell is lazy and we dont wanna count
140+
-- the size of large thunks (unevaluated expressions)
141+
calcCacheEntrySizeInBytes :: (ByteString, AuthResult, TimeSpec) -> IO Int
142+
#ifndef JWT_CACHE_METRIC
143+
calcCacheEntrySizeInBytes entry = fromIntegral <$> getSize entry
144+
where
145+
-- We also include the size of SizeInBytes integer which is a constant 8 bytes
146+
getSize :: (ByteString, AuthResult, TimeSpec) -> IO Word
147+
getSize (bs, ar, ts) = do
148+
keySize <- recursiveSizeNF bs
149+
arClaimsSize <- recursiveSizeNF $ authClaims ar
150+
arRoleSize <- recursiveSizeNF $ authRole ar
151+
timeSpecSize <- liftA2 (+) (recursiveSizeNF (sec ts)) (recursiveSizeNF (nsec ts))
152+
let sizeOfSizeEntryItself = 8 -- a constant 8 bytes size of each size entry in the cache
153+
return (keySize + arClaimsSize + arRoleSize + timeSpecSize + sizeOfSizeEntryItself)
154+
#else
155+
calcCacheEntrySizeInBytes _ = return 0
156+
#endif

src/PostgREST/Logger.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ observationLogger loggerState logLevel obs = case obs of
100100
o@PoolRequestFullfilled ->
101101
when (logLevel >= LogDebug) $ do
102102
logWithZTime loggerState $ observationMessage o
103+
o@(JwtCache _) -> do
104+
when (logLevel >= LogInfo) $ do
105+
logWithZTime loggerState $ observationMessage o
103106
o ->
104107
logWithZTime loggerState $ observationMessage o
105108

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_bytes" "The JWT cache size in bytes")
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ data Observation
5959
| HasqlPoolObs SQL.Observation
6060
| PoolRequest
6161
| PoolRequestFullfilled
62+
| JwtCache Int
6263

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

@@ -146,6 +147,8 @@ observationMessage = \case
146147
"Trying to borrow a connection from pool"
147148
PoolRequestFullfilled ->
148149
"Borrowed a connection from the pool"
150+
JwtCache sz ->
151+
"The JWT Cache size updated to " <> show sz <> " bytes"
149152
where
150153
showMillis :: Double -> Text
151154
showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""

0 commit comments

Comments
 (0)