refactor(test): provide means to validate metrics and observations
Some helpers are provided for introspecting metrics already (used in JWT cache tests). This change provides facilities to additionally validate emited Observation events. A new Spec module is also implemented, adding basic tests of schema cache reloading - their main goal is to excercise the new infrastructure.
This commit is contained in:
committed by
Steve Chavez
parent
8262faa235
commit
afb95a5268
@@ -303,6 +303,7 @@ test-suite observability
|
|||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
other-modules: ObsHelper
|
other-modules: ObsHelper
|
||||||
Observation.JwtCache
|
Observation.JwtCache
|
||||||
|
Observation.MetricsSpec
|
||||||
build-depends: base >= 4.9 && < 4.20
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
@@ -317,6 +318,7 @@ test-suite observability
|
|||||||
, postgrest
|
, postgrest
|
||||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
|
, text >= 1.2.2 && < 2.2
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module PostgREST.AppState
|
|||||||
, getJwtCacheState
|
, getJwtCacheState
|
||||||
, init
|
, init
|
||||||
, initWithPool
|
, initWithPool
|
||||||
|
, putConfig -- For tests TODO refactoring
|
||||||
, putNextListenerDelay
|
, putNextListenerDelay
|
||||||
, putSchemaCache
|
, putSchemaCache
|
||||||
, putPgVersion
|
, putPgVersion
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Observation
|
Module : PostgREST.Observation
|
||||||
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
||||||
@@ -65,6 +66,7 @@ data Observation
|
|||||||
| JwtCacheEviction
|
| JwtCacheEviction
|
||||||
| TerminationUnixSignalObs Text
|
| TerminationUnixSignalObs Text
|
||||||
| WarpErrorObs Text
|
| WarpErrorObs Text
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,31 @@ import qualified PostgREST.Metrics as Metrics
|
|||||||
import PostgREST.SchemaCache (querySchemaCache)
|
import PostgREST.SchemaCache (querySchemaCache)
|
||||||
|
|
||||||
import qualified Observation.JwtCache
|
import qualified Observation.JwtCache
|
||||||
|
import qualified Observation.MetricsSpec
|
||||||
|
|
||||||
import ObsHelper
|
import ObsHelper
|
||||||
import Protolude hiding (toList, toS)
|
import PostgREST.Observation (Observation (HasqlPoolObs))
|
||||||
|
import Protolude hiding (toList, toS)
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
|
poolChan <- newChan
|
||||||
|
-- make sure poolChan is not growing indefinitely
|
||||||
|
-- start a thread that drains the channel
|
||||||
|
-- this is necessary because test cases operate on
|
||||||
|
-- copies so poolChan is never read from
|
||||||
|
-- this means we have another thread running for the entire duration of the spec but this shouldn't be a problem since Haskell green threads are lightweight
|
||||||
|
void $ forkIO $ forever $ readChan poolChan
|
||||||
|
metricsState <- Metrics.init (configDbPoolSize testCfg)
|
||||||
pool <- P.acquire $ P.settings
|
pool <- P.acquire $ P.settings
|
||||||
[ P.size 3
|
[ P.size 3
|
||||||
, P.acquisitionTimeout 10
|
, P.acquisitionTimeout 10
|
||||||
, P.agingTimeout 60
|
, P.agingTimeout 60
|
||||||
, P.idlenessTimeout 60
|
, P.idlenessTimeout 60
|
||||||
, P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)
|
, P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)
|
||||||
|
-- make sure metrics are updated and pool observations published to poolChan
|
||||||
|
, P.observationHandler $ (writeChan poolChan <> Metrics.observationMetrics metricsState) . HasqlPoolObs
|
||||||
]
|
]
|
||||||
|
|
||||||
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
|
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
|
||||||
@@ -35,19 +47,23 @@ main = do
|
|||||||
-- cached schema cache so most tests run fast
|
-- cached schema cache so most tests run fast
|
||||||
baseSchemaCache <- loadSCache pool testCfg
|
baseSchemaCache <- loadSCache pool testCfg
|
||||||
loggerState <- Logger.init
|
loggerState <- Logger.init
|
||||||
metricsState <- Metrics.init (configDbPoolSize testCfg)
|
|
||||||
|
|
||||||
let
|
let
|
||||||
initApp sCache st config = do
|
initApp sCache config = do
|
||||||
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState)
|
-- duplicate poolChan as a starting point
|
||||||
|
obsChan <- dupChan poolChan
|
||||||
|
stateObsChan <- newObsChan obsChan
|
||||||
|
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan)
|
||||||
AppState.putPgVersion appState actualPgVersion
|
AppState.putPgVersion appState actualPgVersion
|
||||||
AppState.putSchemaCache appState (Just sCache)
|
AppState.putSchemaCache appState (Just sCache)
|
||||||
return (st, postgrest (configLogLevel config) appState (pure ()))
|
return (SpecState appState metricsState stateObsChan, postgrest (configLogLevel config) appState (pure ()))
|
||||||
|
|
||||||
-- Run all test modules
|
-- Run all test modules
|
||||||
hspec $ do
|
hspec $ do
|
||||||
before (initApp baseSchemaCache metricsState testCfgJwtCache) $
|
before (initApp baseSchemaCache testCfgJwtCache) $
|
||||||
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
||||||
|
before (initApp baseSchemaCache testCfg) $
|
||||||
|
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
loadSCache pool conf =
|
loadSCache pool conf =
|
||||||
|
|||||||
@@ -1,31 +1,69 @@
|
|||||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
{-# LANGUAGE ExistentialQuantification #-}
|
{-# LANGUAGE ExistentialQuantification #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE RankNTypes #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
module ObsHelper where
|
module ObsHelper where
|
||||||
|
|
||||||
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Jose.Jwa as JWT
|
import qualified Data.List as DL
|
||||||
import qualified Jose.Jws as JWT
|
import Data.List.NonEmpty (fromList)
|
||||||
import qualified Jose.Jwt as JWT
|
import Data.String (String)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Jose.Jwa as JWT
|
||||||
|
import qualified Jose.Jws as JWT
|
||||||
|
import qualified Jose.Jwt as JWT
|
||||||
|
import Network.HTTP.Types
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
|
import PostgREST.Config (AppConfig (..),
|
||||||
|
JSPathExp (..),
|
||||||
|
LogLevel (..),
|
||||||
|
OpenAPIMode (..),
|
||||||
|
parseSecret)
|
||||||
|
import qualified PostgREST.Metrics as Metrics
|
||||||
|
import PostgREST.Observation (Observation (..))
|
||||||
|
import Prometheus (Counter, getCounter)
|
||||||
|
import Protolude hiding (get, toS)
|
||||||
|
import System.Timeout (timeout)
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Expectations.Contrib (annotate)
|
||||||
|
-- helpers used to produce observation diagnostics in waitForObs
|
||||||
|
-- Implementing the Show instance for Observation is hard due to having many different parameters so instead we use generic programming (`conName`) to obtain the constructor name as `Text`
|
||||||
|
class HasConstructor f where
|
||||||
|
genericConstrName :: f x -> Text
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..), JSPathExp (..),
|
instance HasConstructor f => HasConstructor (D1 c f) where
|
||||||
LogLevel (..), OpenAPIMode (..), parseSecret)
|
genericConstrName (M1 x) = genericConstrName x
|
||||||
|
|
||||||
import Data.List.NonEmpty (fromList)
|
instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where
|
||||||
import Data.String (String)
|
genericConstrName (L1 l) = genericConstrName l
|
||||||
import Prometheus (Counter, getCounter)
|
genericConstrName (R1 r) = genericConstrName r
|
||||||
import Test.Hspec.Expectations.Contrib (annotate)
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
instance Constructor c => HasConstructor (C1 c f) where
|
||||||
import Protolude
|
genericConstrName = T.pack . conName
|
||||||
import Test.Hspec
|
|
||||||
import Test.Hspec.Wai
|
|
||||||
|
|
||||||
|
data SpecState = SpecState {
|
||||||
|
specAppState :: AppState.AppState,
|
||||||
|
specMetrics :: Metrics.MetricsState,
|
||||||
|
specObsChan :: ObsChan
|
||||||
|
}
|
||||||
|
|
||||||
|
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
|
||||||
|
|
||||||
|
data TimeoutException = TimeoutException deriving (Show, Exception)
|
||||||
|
|
||||||
|
data ObsChan = ObsChan (Chan Observation) (Chan Observation)
|
||||||
|
|
||||||
|
constrName :: (HasConstructor (Rep a), Generic a)=> a -> Text
|
||||||
|
constrName = genericConstrName . from
|
||||||
|
|
||||||
baseCfg :: AppConfig
|
baseCfg :: AppConfig
|
||||||
baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||||
@@ -107,18 +145,12 @@ generateJWT claims =
|
|||||||
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
|
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
|
||||||
|
|
||||||
-- state check helpers
|
-- state check helpers
|
||||||
|
|
||||||
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
|
|
||||||
|
|
||||||
stateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m
|
stateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m
|
||||||
stateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)
|
stateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)
|
||||||
|
|
||||||
expectField :: forall s st a c m. (KnownSymbol s, Show a, Eq a, HasField s st c) => (c -> m a) -> (a -> a) -> StateCheck st m
|
expectField :: forall s st a c m. (KnownSymbol s, Show a, Eq a, HasField s st c) => (c -> m a) -> (a -> a) -> StateCheck st m
|
||||||
expectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)
|
expectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)
|
||||||
|
|
||||||
checkState :: (Traversable t) => t (StateCheck st (WaiSession st)) -> WaiSession st b -> WaiSession st ()
|
|
||||||
checkState checks act = getState >>= flip (`checkState'` checks) act
|
|
||||||
|
|
||||||
checkState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()
|
checkState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()
|
||||||
checkState' initialState checks act = do
|
checkState' initialState checks act = do
|
||||||
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks
|
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks
|
||||||
@@ -131,3 +163,48 @@ expectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m
|
|||||||
expectCounter = expectField @s intCounter
|
expectCounter = expectField @s intCounter
|
||||||
where
|
where
|
||||||
intCounter = ((round @Double @Int) <$>) . getCounter
|
intCounter = ((round @Double @Int) <$>) . getCounter
|
||||||
|
|
||||||
|
accumulateUntilTimeout :: Int -> (s -> a -> s) -> s -> IO a -> IO s
|
||||||
|
accumulateUntilTimeout t f start act = do
|
||||||
|
tid <- myThreadId
|
||||||
|
-- mask to make sure TimeoutException is not thrown before starting the loop
|
||||||
|
mask $ \unmask -> do
|
||||||
|
-- start timeout thread unmasking exceptions
|
||||||
|
ttid <- forkIOWithUnmask ($ (threadDelay t *> throwTo tid TimeoutException))
|
||||||
|
-- unmask effect
|
||||||
|
unmask (fix (\loop accum -> (act >>= loop . f accum) `onTimeout` pure accum) start)
|
||||||
|
-- make sure we catch timeout if happens before entering the loop
|
||||||
|
`onTimeout` pure start
|
||||||
|
-- make sure timer thread is killed on other exceptions
|
||||||
|
-- so that it won't throw TimeoutException later
|
||||||
|
`onException` killThread ttid
|
||||||
|
where
|
||||||
|
onTimeout m a = m `catch` \TimeoutException -> a
|
||||||
|
|
||||||
|
newObsChan :: Chan Observation -> IO ObsChan
|
||||||
|
newObsChan = fmap <$> ObsChan <*> dupChan
|
||||||
|
|
||||||
|
-- read messages from copy chan and once condition is met drain original to the same point
|
||||||
|
-- upon timeout report error and messages remaining in the original chan
|
||||||
|
-- that way we report messages since last successful read
|
||||||
|
waitForObs :: HasCallStack => ObsChan -> Int -> Text -> (Observation -> Maybe a) -> IO ()
|
||||||
|
waitForObs (ObsChan orig copy) t msg f =
|
||||||
|
timeout t (readUntil copy *> readUntil orig) >>= maybe failTimeout mempty
|
||||||
|
where
|
||||||
|
failTimeout = takeUntilTimeout decisecond (readChan orig)
|
||||||
|
>>= expectationFailure . DL.unlines . fmap show . (failureMessageHeader :) . fmap obsDiagMessage
|
||||||
|
failureMessageHeader = "Timeout waiting for " <> msg <> " at " <> loc <> ". Remaining observations:"
|
||||||
|
readUntil = void . untilM (pure . not . null . f) . readChan
|
||||||
|
loc = fromMaybe "(unknown)" . head $ (T.pack . prettySrcLoc . snd <$> getCallStack callStack)
|
||||||
|
-- execute effectful computation until result meets provided condition
|
||||||
|
untilM cond m = fix $ \loop -> m >>= \a -> ifM (cond a) (pure a) loop
|
||||||
|
-- duplicate the provided channel and construct wairFor function binding both channels
|
||||||
|
-- accumulate effecful computation results into a list for specified time
|
||||||
|
takeUntilTimeout t' = fmap reverse . accumulateUntilTimeout t' (flip (:)) []
|
||||||
|
decisecond = 100000
|
||||||
|
|
||||||
|
obsDiagMessage :: Observation -> Text
|
||||||
|
obsDiagMessage = \case
|
||||||
|
(HasqlPoolObs o) -> show o
|
||||||
|
o@(DBListenStart channel) -> constrName o <> show channel
|
||||||
|
o -> constrName o
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
module Observation.JwtCache where
|
module Observation.JwtCache where
|
||||||
|
|
||||||
@@ -13,9 +14,11 @@ import PostgREST.Metrics (MetricsState (..))
|
|||||||
import Protolude
|
import Protolude
|
||||||
import Test.Hspec.Wai.JSON (json)
|
import Test.Hspec.Wai.JSON (json)
|
||||||
|
|
||||||
spec :: SpecWith (MetricsState, Application)
|
spec :: SpecWith (SpecState, Application)
|
||||||
spec = describe "Server started with JWT and metrics enabled" $ do
|
spec = describe "Server started with JWT and metrics enabled" $ do
|
||||||
it "Should not have JWT in cache" $ do
|
it "Should not have JWT in cache" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe1"}|]
|
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe1"}|]
|
||||||
|
|
||||||
expectCounters
|
expectCounters
|
||||||
@@ -27,6 +30,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
||||||
|
|
||||||
it "Should have JWT in cache" $ do
|
it "Should have JWT in cache" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
||||||
|
|
||||||
expectCounters
|
expectCounters
|
||||||
@@ -39,6 +44,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
||||||
|
|
||||||
it "Should not cache invalid JWTs" $ do
|
it "Should not cache invalid JWTs" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let auth = authHeaderJWT "some random bytes"
|
let auth = authHeaderJWT "some random bytes"
|
||||||
|
|
||||||
expectCounters
|
expectCounters
|
||||||
@@ -51,6 +58,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||||
|
|
||||||
it "Should cache expired JWTs" $ do
|
it "Should cache expired JWTs" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let auth = genToken [json|{"exp": 1, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
let auth = genToken [json|{"exp": 1, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
||||||
|
|
||||||
expectCounters
|
expectCounters
|
||||||
@@ -63,6 +72,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||||
|
|
||||||
it "Should evict entries from the JWT cache (jwt cache max is 2)" $ do
|
it "Should evict entries from the JWT cache (jwt cache max is 2)" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe3"}|]
|
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe3"}|]
|
||||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe4"}|]
|
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe4"}|]
|
||||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe5"}|]
|
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe5"}|]
|
||||||
@@ -82,6 +93,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
*> request methodGet "/authors_only" [jwt3] ""
|
*> request methodGet "/authors_only" [jwt3] ""
|
||||||
|
|
||||||
it "Should not evict entries from the JWT cache in FIFO order" $ do
|
it "Should not evict entries from the JWT cache in FIFO order" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe6"}|]
|
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe6"}|]
|
||||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe7"}|]
|
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe7"}|]
|
||||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe8"}|]
|
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe8"}|]
|
||||||
@@ -108,6 +121,8 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
-- The test case was added based on coverage report
|
-- The test case was added based on coverage report
|
||||||
-- showing this scenario was not covered by previous tests
|
-- showing this scenario was not covered by previous tests
|
||||||
it "Should evict entries even though all were hit" $ do
|
it "Should evict entries even though all were hit" $ do
|
||||||
|
expectCounters <- checkState' . specMetrics <$> getState
|
||||||
|
|
||||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe9"}|]
|
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe9"}|]
|
||||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe10"}|]
|
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe10"}|]
|
||||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe11"}|]
|
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe11"}|]
|
||||||
@@ -135,4 +150,3 @@ spec = describe "Server started with JWT and metrics enabled" $ do
|
|||||||
requests = expectCounter @"jwtCacheRequests"
|
requests = expectCounter @"jwtCacheRequests"
|
||||||
hits = expectCounter @"jwtCacheHits"
|
hits = expectCounter @"jwtCacheHits"
|
||||||
evictions = expectCounter @"jwtCacheEvictions"
|
evictions = expectCounter @"jwtCacheEvictions"
|
||||||
expectCounters = checkState
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE MonadComprehensions #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
|
module Observation.MetricsSpec where
|
||||||
|
|
||||||
|
import Data.List (lookup)
|
||||||
|
import Network.Wai (Application)
|
||||||
|
import ObsHelper
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
|
import PostgREST.Config (AppConfig (configDbSchemas))
|
||||||
|
import qualified PostgREST.Metrics as Metrics
|
||||||
|
import PostgREST.Observation
|
||||||
|
import Prometheus (getCounter, getVectorWith)
|
||||||
|
import Protolude
|
||||||
|
import Test.Hspec (SpecWith, describe, it)
|
||||||
|
import Test.Hspec.Wai (getState)
|
||||||
|
|
||||||
|
spec :: SpecWith (SpecState, Application)
|
||||||
|
spec = describe "Server started with metrics enabled" $ do
|
||||||
|
it "Should update pgrst_schema_cache_loads_total[SUCCESS]" $ do
|
||||||
|
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ checkState' metrics [
|
||||||
|
schemaCacheLoads "SUCCESS" (+1)
|
||||||
|
] $ do
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
|
||||||
|
|
||||||
|
it "Should update pgrst_schema_cache_loads_total[ERROR]" $ do
|
||||||
|
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ checkState' metrics [
|
||||||
|
schemaCacheLoads "FAIL" (+1),
|
||||||
|
schemaCacheLoads "SUCCESS" (+1)
|
||||||
|
] $ do
|
||||||
|
AppState.getConfig appState >>= \prev -> do
|
||||||
|
AppState.putConfig appState $ prev { configDbSchemas = pure "bad_schema" }
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
waitFor (1 * sec) "SchemaCacheErrorObs" $ \x -> [ o | o@(SchemaCacheErrorObs{}) <- pure x]
|
||||||
|
AppState.putConfig appState prev
|
||||||
|
|
||||||
|
-- wait up to 2 secs so that retry can happen
|
||||||
|
waitFor (2 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
|
||||||
|
|
||||||
|
where
|
||||||
|
-- prometheus-client api to handle vectors is convoluted
|
||||||
|
schemaCacheLoads label = expectField @"schemaCacheLoads" $
|
||||||
|
fmap (maybe (0::Int) round . lookup label) . (`getVectorWith` getCounter)
|
||||||
|
sec = 1000000
|
||||||
Reference in New Issue
Block a user