Skip to content

Commit bfd02a2

Browse files
committed
feat: add metrics=timings to prefer header
1 parent 80783a7 commit bfd02a2

File tree

8 files changed

+88
-71
lines changed

8 files changed

+88
-71
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2323
+ Shows the correct JSON format in the `hints` field
2424
- #3340, Log when the LISTEN channel gets a notification - @steve-chavez
2525
- #3184, Log full pg version to stderr on connection - @steve-chavez
26+
- #3410, Add `metrics=timings` to Prefer header - @taimoorzaeem
2627

2728
### Fixed
2829

src/PostgREST/ApiRequest/Preferences.hs

+21-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module PostgREST.ApiRequest.Preferences
1818
, PreferTransaction(..)
1919
, PreferTimezone(..)
2020
, PreferMaxAffected(..)
21+
, PreferMetrics(..)
2122
, fromHeaders
2223
, shouldCount
2324
, prefAppliedHeader
@@ -44,6 +45,7 @@ import Protolude
4445
-- >>> deriving instance Show PreferHandling
4546
-- >>> deriving instance Show PreferTimezone
4647
-- >>> deriving instance Show PreferMaxAffected
48+
-- >>> deriving instance Show PreferMetrics
4749
-- >>> deriving instance Show Preferences
4850

4951
-- | Preferences recognized by the application.
@@ -58,6 +60,7 @@ data Preferences
5860
, preferHandling :: Maybe PreferHandling
5961
, preferTimezone :: Maybe PreferTimezone
6062
, preferMaxAffected :: Maybe PreferMaxAffected
63+
, preferMetrics :: Maybe PreferMetrics
6164
, invalidPrefs :: [ByteString]
6265
}
6366

@@ -67,7 +70,7 @@ data Preferences
6770
-- >>> let sc = S.fromList ["America/Los_Angeles"]
6871
--
6972
-- One header with comma-separated values can be used to set multiple preferences:
70-
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100")]
73+
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles, max-affected=100, metrics=timings")]
7174
-- Preferences
7275
-- { preferResolution = Just IgnoreDuplicates
7376
-- , preferRepresentation = Nothing
@@ -80,6 +83,7 @@ data Preferences
8083
-- ( PreferTimezone "America/Los_Angeles" )
8184
-- , preferMaxAffected = Just
8285
-- ( PreferMaxAffected 100 )
86+
-- , preferMetrics = Just Timings
8387
-- , invalidPrefs = []
8488
-- }
8589
--
@@ -97,6 +101,7 @@ data Preferences
97101
-- , preferTimezone = Nothing
98102
-- , preferMaxAffected = Just
99103
-- ( PreferMaxAffected 5999 )
104+
-- , preferMetrics = Nothing
100105
-- , invalidPrefs = [ "invalid" ]
101106
-- }
102107
--
@@ -129,6 +134,7 @@ data Preferences
129134
-- , preferHandling = Just Strict
130135
-- , preferTimezone = Nothing
131136
-- , preferMaxAffected = Nothing
137+
-- , preferMetrics = Nothing
132138
-- , invalidPrefs = [ "anything" ]
133139
-- }
134140
--
@@ -144,6 +150,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
144150
, preferHandling = parsePrefs [Strict, Lenient]
145151
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
146152
, preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
153+
, preferMetrics = parsePrefs [Timings]
147154
, invalidPrefs = filter isUnacceptable prefs
148155
}
149156
where
@@ -155,7 +162,8 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
155162
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
156163
mapToHeadVal [Commit, Rollback] ++
157164
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
158-
mapToHeadVal [Strict, Lenient]
165+
mapToHeadVal [Strict, Lenient] ++
166+
mapToHeadVal [Timings]
159167

160168
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
161169
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
@@ -179,7 +187,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
179187
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
180188

181189
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
182-
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
190+
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected, preferMetrics } =
183191
if null prefsVals
184192
then Nothing
185193
else Just (HTTP.hPreferenceApplied, combined)
@@ -195,6 +203,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
195203
, toHeaderValue <$> preferHandling
196204
, toHeaderValue <$> preferTimezone
197205
, if preferHandling == Just Strict then toHeaderValue <$> preferMaxAffected else Nothing
206+
, toHeaderValue <$> preferMetrics
198207
]
199208

200209
-- |
@@ -302,3 +311,12 @@ newtype PreferMaxAffected = PreferMaxAffected Int64
302311

303312
instance ToHeaderValue PreferMaxAffected where
304313
toHeaderValue (PreferMaxAffected n) = "max-affected=" <> show n
314+
315+
-- |
316+
-- Show Performance Metrics
317+
data PreferMetrics
318+
= Timings -- show server timings for the request
319+
deriving Eq
320+
321+
instance ToHeaderValue PreferMetrics where
322+
toHeaderValue Timings = "metrics=timings"

src/PostgREST/App.hs

+33-28
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,20 @@ import qualified PostgREST.Query as Query
3939
import qualified PostgREST.Response as Response
4040
import qualified PostgREST.Unix as Unix (installSignalHandlers)
4141

42-
import PostgREST.ApiRequest (ApiRequest (..))
43-
import PostgREST.AppState (AppState)
44-
import PostgREST.Auth (AuthResult (..))
45-
import PostgREST.Config (AppConfig (..), LogLevel (..))
46-
import PostgREST.Config.PgVersion (PgVersion (..))
47-
import PostgREST.Error (Error)
48-
import PostgREST.Observation (Observation (..))
49-
import PostgREST.Response.Performance (ServerTiming (..),
50-
serverTimingHeader)
51-
import PostgREST.SchemaCache (SchemaCache (..))
52-
import PostgREST.Version (docsVersion, prettyVersion)
42+
import PostgREST.ApiRequest (ApiRequest (..))
43+
import PostgREST.ApiRequest.Preferences (PreferMetrics (..),
44+
Preferences (..))
45+
import PostgREST.AppState (AppState)
46+
import PostgREST.Auth (AuthResult (..))
47+
import PostgREST.Config (AppConfig (..),
48+
LogLevel (..))
49+
import PostgREST.Config.PgVersion (PgVersion (..))
50+
import PostgREST.Error (Error)
51+
import PostgREST.Observation (Observation (..))
52+
import PostgREST.Response.Performance (ServerTiming (..),
53+
serverTimingHeader)
54+
import PostgREST.SchemaCache (SchemaCache (..))
55+
import PostgREST.Version (docsVersion, prettyVersion)
5356

5457
import qualified Data.ByteString.Char8 as BS
5558
import qualified Data.List as L
@@ -141,27 +144,29 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
141144

142145
body <- lift $ Wai.strictRequestBody req
143146

144-
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
147+
-- the preference metrics=timings cant be used before it is parsed, hence
148+
-- parseTime will be calculated for all requests
149+
(parseTime, apiReq@ApiRequest{..}) <- withTiming True $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf req body sCache
150+
let timingsPref = preferMetrics iPreferences == Just Timings
151+
(planTime, plan) <- withTiming timingsPref $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
152+
(queryTime, queryResult) <- withTiming timingsPref $ Query.runQuery appState conf authResult apiReq plan sCache pgVer (Just authRole /= configDbAnonRole)
153+
(respTime, resp) <- withTiming timingsPref $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
145154

146-
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf req body sCache
147-
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
148-
(queryTime, queryResult) <- withTiming $ Query.runQuery appState conf authResult apiReq plan sCache pgVer (Just authRole /= configDbAnonRole)
149-
(respTime, resp) <- withTiming $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
155+
let jwtTime = if timingsPref then Auth.getJwtDur req else Nothing
150156

151-
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
157+
return $ toWaiResponse timingsPref (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
152158

153159
where
154-
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
155-
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled])) bod
156-
157-
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
158-
withTiming f = if configServerTimingEnabled
159-
then do
160-
(t, r) <- timeItT f
161-
pure (Just t, r)
162-
else do
163-
r <- f
164-
pure (Nothing, r)
160+
toWaiResponse :: Bool -> ServerTiming -> Response.PgrstResponse -> Wai.Response
161+
toWaiResponse includeTimings timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | includeTimings])) bod
162+
163+
withTiming :: Bool -> Handler IO a -> Handler IO (Maybe Double, a)
164+
withTiming True f = do
165+
(t, r) <- timeItT f
166+
pure (Just t, r)
167+
withTiming False f = do
168+
r <- f
169+
pure (Nothing, r)
165170

166171
traceHeaderMiddleware :: AppState -> Wai.Middleware
167172
traceHeaderMiddleware appState app req respond = do

src/PostgREST/Auth.hs

+4-13
Original file line numberDiff line numberDiff line change
@@ -107,25 +107,16 @@ middleware appState app req respond = do
107107
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
108108
parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
109109

110-
-- If DbPlanEnabled -> calculate JWT validation time
111-
-- If JwtCacheMaxLifetime -> cache JWT validation result
112-
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
113-
(True, 0) -> do
110+
-- If JwtCacheMaxLifetime -> cache JWT validation result
111+
req' <- case configJwtCacheMaxLifetime conf of
112+
0 -> do
114113
(dur, authResult) <- timeItT parseJwt
115114
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
116115

117-
(True, maxLifetime) -> do
116+
maxLifetime -> do
118117
(dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
119118
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
120119

121-
(False, 0) -> do
122-
authResult <- parseJwt
123-
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
124-
125-
(False, maxLifetime) -> do
126-
authResult <- getJWTFromCache appState token maxLifetime parseJwt time
127-
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
128-
129120
app req' respond
130121

131122
-- | Used to retrieve and insert JWT to JWT Cache

src/PostgREST/Response.hs

+6-6
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
6969
RSStandard{..} -> do
7070
let
7171
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
72-
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
72+
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing preferMetrics []
7373
headers =
7474
[ contentRange
7575
, ( "Content-Location"
@@ -99,7 +99,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
9999
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
100100
prefHeader = prefAppliedHeader $
101101
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
102-
preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
102+
preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing preferMetrics []
103103
headers =
104104
catMaybes
105105
[ if null rsLocation then
@@ -139,7 +139,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
139139
contentRangeHeader =
140140
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
141141
if shouldCount preferCount then Just rsQueryTotal else Nothing
142-
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
142+
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected preferMetrics []
143143
headers = catMaybes [contentRangeHeader, prefHeader]
144144

145145
let (status, headers', body) =
@@ -158,7 +158,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
158158
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
159159
RSStandard {..} -> do
160160
let
161-
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
161+
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing preferMetrics []
162162
cTHeader = contentTypeHeaders mrMedia ctxApiRequest
163163

164164
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
@@ -181,7 +181,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
181181
contentRangeHeader =
182182
RangeQuery.contentRangeH 1 0 $
183183
if shouldCount preferCount then Just rsQueryTotal else Nothing
184-
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
184+
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected preferMetrics []
185185
headers = contentRangeHeader : prefHeader
186186

187187
let (status, headers', body) =
@@ -206,7 +206,7 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
206206
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
207207
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
208208
else LBS.fromStrict rsBody
209-
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
209+
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected preferMetrics []
210210
headers = contentRange : prefHeader
211211

212212
let (status', headers', body) =

test/io/test_big_schema.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ def test_requests_wait_for_schema_cache_reload(defaultenv):
1515
"PGRST_DB_SCHEMAS": "apflora",
1616
"PGRST_DB_POOL": "2",
1717
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
18-
"PGRST_SERVER_TIMING_ENABLED": "true",
1918
}
2019

20+
headers = {"Prefer": "metrics=timings"}
21+
2122
with run(env=env, wait_max_seconds=30) as postgrest:
2223
# reload the schema cache
2324
response = postgrest.session.get("/rpc/notify_pgrst")
2425
assert response.status_code == 204
2526

2627
postgrest.wait_until_scache_starts_loading()
2728

28-
response = postgrest.session.get("/tpopmassn?select=*,tpop(*)")
29+
response = postgrest.session.get("/tpopmassn?select=*,tpop(*)", headers=headers)
2930
assert response.status_code == 200
3031

3132
plan_dur = parse_server_timings_header(response.headers["Server-Timing"])[

test/io/test_io.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,6 @@ def test_server_timing_jwt_should_decrease_on_subsequent_requests(defaultenv):
12301230

12311231
env = {
12321232
**defaultenv,
1233-
"PGRST_SERVER_TIMING_ENABLED": "true",
12341233
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
12351234
"PGRST_JWT_SECRET": "@/dev/stdin",
12361235
"PGRST_DB_CONFIG": "false",
@@ -1245,6 +1244,7 @@ def test_server_timing_jwt_should_decrease_on_subsequent_requests(defaultenv):
12451244
},
12461245
SECRET,
12471246
)
1247+
headers["Prefer"] = "metrics=timings"
12481248

12491249
with run(stdin=SECRET.encode(), env=env) as postgrest:
12501250
first_timings = postgrest.session.get("/authors_only", headers=headers).headers[
@@ -1290,13 +1290,13 @@ def test_server_timing_jwt_should_not_decrease_when_caching_disabled(defaultenv)
12901290

12911291
env = {
12921292
**defaultenv,
1293-
"PGRST_SERVER_TIMING_ENABLED": "true",
12941293
"PGRST_JWT_CACHE_MAX_LIFETIME": "0", # cache disabled
12951294
"PGRST_JWT_SECRET": "@/dev/stdin",
12961295
"PGRST_DB_CONFIG": "false",
12971296
}
12981297

12991298
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
1299+
headers["Prefer"] = "metrics=timings"
13001300

13011301
with run(stdin=SECRET.encode(), env=env) as postgrest:
13021302
warmup_req = postgrest.session.get("/authors_only", headers=headers)
@@ -1320,13 +1320,13 @@ def test_jwt_cache_with_no_exp_claim(defaultenv):
13201320

13211321
env = {
13221322
**defaultenv,
1323-
"PGRST_SERVER_TIMING_ENABLED": "true",
13241323
"PGRST_JWT_CACHE_MAX_LIFETIME": "86400",
13251324
"PGRST_JWT_SECRET": "@/dev/stdin",
13261325
"PGRST_DB_CONFIG": "false",
13271326
}
13281327

13291328
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET) # no exp
1329+
headers["Prefer"] = "metrics=timings"
13301330

13311331
with run(stdin=SECRET.encode(), env=env) as postgrest:
13321332
first_timings = postgrest.session.get("/authors_only", headers=headers).headers[

0 commit comments

Comments
 (0)