feat: allow logging the SQL query to stderr

- Logs the main SQL query when `log-query=main-query`.
- Only logs at the current `log-level`.
This commit is contained in:
Laurence Isla
2025-02-18 19:17:26 -05:00
committed by GitHub
parent 66e966d864
commit 9c880c082a
32 changed files with 267 additions and 48 deletions
+1
View File
@@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem - #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem - #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla - #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
### Fixed ### Fixed
+24
View File
@@ -708,6 +708,30 @@ log-level
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput. Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
.. _log-query:
log-query
---------
=============== =================================
**Type** String
**Default** "disabled"
**Reloadable** Y
**Environment** PGRST_LOG_QUERY
**In-Database** `n/a`
=============== =================================
Logs the SQL query for the corresponding request at the current :ref:`log-level`.
See :ref:``sql_query_logs``.
.. code:: bash
# Logs the main SQL query
log-query = "main-query"
# Disables logging the SQL query
log-query = "disabled"
.. _openapi-mode: .. _openapi-mode:
openapi-mode openapi-mode
+29 -3
View File
@@ -41,12 +41,38 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel 06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
06/May/2024:14:11:27 -0500: Config reloaded 06/May/2024:14:11:27 -0500: Config reloaded
.. _sql_query_logs:
SQL Query Logs
--------------
To log the :ref:`main SQL query <main_query>` executed for a request, set the :ref:`log-query` to ``main-query``.
It will be logged based on the current :ref:`log-level` setting.
For example, with this configuration:
.. code-block:: bash
log-level = "warn"
log-query = "main-query"
The SQL queries will only be logged on ``400`` HTTP errors and up.
So, if the user requests a resource without sufficient privileges:
.. code-block:: bash
curl "localhost:3000/protected_table"
This will be logged by PostgREST:
.. code::
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 - "" "curl/8.7.1"
Database Logs Database Logs
------------- -------------
Currently PostgREST doesn't log the SQL commands executed against the underlying database. Additionally, to find all the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file. Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
+11 -5
View File
@@ -17,7 +17,7 @@ module PostgREST.App
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft, whenLeft)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort, import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
@@ -43,7 +43,8 @@ import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..)) import PostgREST.Config (AppConfig (..), LogLevel (..),
LogQuery (..))
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.Network (resolveHost) import PostgREST.Network (resolveHost)
@@ -148,16 +149,21 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache (planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let query = Query.query conf authResult apiReq plan sCache pgVer let query = Query.query conf authResult apiReq plan sCache pgVer
logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query)
(queryTime, queryResult) <- withTiming $ do (queryTime, queryResult) <- withTiming $ do
case query of case query of
Query.NoDbQuery r -> pure r Query.NoDbQuery r -> pure r
Query.DbQuery{..} -> do Query.DbQuery{..} -> do
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler) dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
err <- liftEither . mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes
liftEither err when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
liftEither eitherResp >>= liftEither
(respTime, resp) <- withTiming $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile (respTime, resp) <- withTiming $ do
let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response
liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
+3
View File
@@ -209,6 +209,9 @@ exampleConfigFile =
|## Logging level, the admitted values are: crit, error, warn, info and debug. |## Logging level, the admitted values are: crit, error, warn, info and debug.
|log-level = "error" |log-level = "error"
| |
|## Log the requested SQL query at the current log-level.
|log-query = "disabled"
|
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely. |## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|## Admitted values: follow-privileges, ignore-privileges, disabled |## Admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-privileges" |openapi-mode = "follow-privileges"
+20
View File
@@ -17,6 +17,7 @@ module PostgREST.Config
, JSPathExp(..) , JSPathExp(..)
, FilterExp(..) , FilterExp(..)
, LogLevel(..) , LogLevel(..)
, LogQuery(..)
, OpenAPIMode(..) , OpenAPIMode(..)
, Proxy(..) , Proxy(..)
, toText , toText
@@ -98,6 +99,7 @@ data AppConfig = AppConfig
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxLifetime :: Int , configJwtCacheMaxLifetime :: Int
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
, configLogQuery :: LogQuery
, configOpenApiMode :: OpenAPIMode , configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool , configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
@@ -126,6 +128,14 @@ dumpLogLevel = \case
LogInfo -> "info" LogInfo -> "info"
LogDebug -> "debug" LogDebug -> "debug"
data LogQuery = LogQueryMain | LogQueryDisabled
deriving (Eq)
dumpLogQuery :: LogQuery -> Text
dumpLogQuery = \case
LogQueryMain -> "main-query"
LogQueryDisabled -> "disabled"
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
deriving Eq deriving Eq
@@ -169,6 +179,7 @@ toText conf =
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64) ,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime) ,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
,("log-level", q . dumpLogLevel . configLogLevel) ,("log-level", q . dumpLogLevel . configLogLevel)
,("log-query", q . dumpLogQuery . configLogQuery)
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode) ,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive) ,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
@@ -278,6 +289,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime") <*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> parseLogQuery "log-query"
<*> parseOpenAPIMode "openapi-mode" <*> parseOpenAPIMode "openapi-mode"
<*> (fromMaybe False <$> optBool "openapi-security-active") <*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
@@ -353,6 +365,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Just "debug" -> pure LogDebug Just "debug" -> pure LogDebug
Just _ -> fail "Invalid logging level. Check your configuration." Just _ -> fail "Invalid logging level. Check your configuration."
parseLogQuery :: C.Key -> C.Parser C.Config LogQuery
parseLogQuery k =
optString k >>= \case
Nothing -> pure LogQueryDisabled
Just "disabled" -> pure LogQueryDisabled
Just "main-query" -> pure LogQueryMain
Just _ -> fail "Invalid SQL logging value. Check your configuration."
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
parseTxEnd k f = parseTxEnd k f =
optString k >>= \case optString k >>= \case
+4
View File
@@ -90,6 +90,10 @@ observationLogger loggerState logLevel obs = case obs of
o@(HasqlPoolObs _) -> do o@(HasqlPoolObs _) -> do
when (logLevel >= LogDebug) $ do when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
o@(DBQuery sql status) -> do
-- Does not log SQL when it's empty (for OPTIONS requests or for the default OpenAPI output)
when (sql /= mempty && shouldLogResponse logLevel status) $ do
logWithZTime loggerState $ observationMessage o
PoolRequest -> PoolRequest ->
pure () pure ()
PoolRequestFullfilled -> PoolRequestFullfilled ->
+4
View File
@@ -19,6 +19,7 @@ import qualified Data.Text.Encoding as T
import qualified Hasql.Connection as SQL import qualified Hasql.Connection as SQL
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Observation as SQL import qualified Hasql.Pool.Observation as SQL
import Network.HTTP.Types.Status (Status)
import qualified Network.Socket as NS import qualified Network.Socket as NS
import Numeric (showFFloat) import Numeric (showFFloat)
import PostgREST.Config.PgVersion import PostgREST.Config.PgVersion
@@ -46,6 +47,7 @@ data Observation
| DBListenRetry Int | DBListenRetry Int
| DBListenerGotSCacheMsg ByteString | DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| DBQuery ByteString Status
| ConfigReadErrorObs SQL.UsageError | ConfigReadErrorObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
| ConfigSucceededObs | ConfigSucceededObs
@@ -112,6 +114,8 @@ observationMessage = \case
"Received a schema cache reload message on the " <> show channel <> " channel" "Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel -> DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel" "Received a config reload message on the " <> show channel <> " channel"
DBQuery sql _ ->
T.decodeUtf8 sql
ConfigReadErrorObs usageErr -> ConfigReadErrorObs usageErr ->
"Failed to query database settings for the config parameters." <> jsonMessage usageErr "Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr -> QueryRoleSettingsErrorObs usageErr ->
+34 -23
View File
@@ -5,6 +5,7 @@ module PostgREST.Query
( Query (..) ( Query (..)
, QueryResult (..) , QueryResult (..)
, query , query
, getSQLQuery
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -69,6 +70,7 @@ data Query
, dqTxMode :: SQL.Mode , dqTxMode :: SQL.Mode
, dqDbHandler :: DbHandler QueryResult , dqDbHandler :: DbHandler QueryResult
, dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error QueryResult) -> SQL.Session (Either Error QueryResult) , dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error QueryResult) -> SQL.Session (Either Error QueryResult)
, dqSQL :: ByteString
} }
| NoDbQuery QueryResult | NoDbQuery QueryResult
@@ -81,16 +83,17 @@ data QueryResult
query :: AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Query query :: AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Query
query _ _ _ (NoDb x) _ _ = NoDbQuery $ NoDbResult x query _ _ _ (NoDb x) _ _ = NoDbQuery $ NoDbResult x
query config AuthResult{..} apiReq (Db plan) sCache pgVer = query config AuthResult{..} apiReq (Db plan) sCache pgVer =
DbQuery isoLvl txMode dbHandler transaction DbQuery isoLvl txMode dbHandler transaction mainSQLQuery
where where
transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
prepared = configDbPreparedStatements config prepared = configDbPreparedStatements config
isoLvl = planIsoLvl config authRole plan isoLvl = planIsoLvl config authRole plan
txMode = planTxMode plan txMode = planTxMode plan
(mainActionQuery, mainSQLQuery) = actionQuery plan config apiReq pgVer sCache
dbHandler = do dbHandler = do
setPgLocals plan config authClaims authRole apiReq setPgLocals plan config authClaims authRole apiReq
runPreReq config runPreReq config
actionQuery plan config apiReq pgVer sCache mainActionQuery
planTxMode :: DbActionPlan -> SQL.Mode planTxMode :: DbActionPlan -> SQL.Mode
planTxMode (DbCrud x) = pTxMode x planTxMode (DbCrud x) = pTxMode x
@@ -104,12 +107,13 @@ planIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of
where where
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl
actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> DbHandler QueryResult -- TODO: Generate the Hasql Statement in a diferent module after the OpenAPI functionality is removed
actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> (DbHandler QueryResult, ByteString)
actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ =
mainActionQuery (mainActionQuery, mainSQLQuery)
where where
countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
result = Statements.prepareRead (result, mainSQLQuery) = Statements.prepareRead
(QueryBuilder.readPlanToQuery wrReadPlan) (QueryBuilder.readPlanToQuery wrReadPlan)
(if preferCount == Just EstimatedCount then (if preferCount == Just EstimatedCount then
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
@@ -128,10 +132,10 @@ actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiReque
DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery
actionQuery (DbCrud plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = actionQuery (DbCrud plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ =
mainActionQuery (mainActionQuery, mainSQLQuery)
where where
(isPut, isInsert, pkCols) = case mrMutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);} (isPut, isInsert, pkCols) = case mrMutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);}
result = Statements.prepareWrite (result, mainSQLQuery) = Statements.prepareWrite
(QueryBuilder.readPlanToQuery mrReadPlan) (QueryBuilder.readPlanToQuery mrReadPlan)
(QueryBuilder.mutatePlanToQuery mrMutatePlan) (QueryBuilder.mutatePlanToQuery mrMutatePlan)
isInsert isInsert
@@ -160,9 +164,9 @@ actionQuery (DbCrud plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiReques
pure $ DbCrudResult plan resultSet pure $ DbCrudResult plan resultSet
actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ = actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ =
mainActionQuery (mainActionQuery, mainSQLQuery)
where where
result = Statements.prepareCall (result, mainSQLQuery) = Statements.prepareCall
crProc crProc
(QueryBuilder.callPlanToQuery crCallPlan pgVer) (QueryBuilder.callPlanToQuery crCallPlan pgVer)
(QueryBuilder.readPlanToQuery crReadPlan) (QueryBuilder.readPlanToQuery crReadPlan)
@@ -179,20 +183,23 @@ actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{
pure $ DbCallResult plan resultSet pure $ DbCallResult plan resultSet
actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ _ sCache = actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ _ sCache =
lift $ case configOpenApiMode of (mainActionQuery, mempty)
OAFollowPriv -> do where
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements) mainActionQuery = lift $
MaybeDbResult plan . Just <$> ((,,) case configOpenApiMode of
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache) OAFollowPriv -> do
<$> SQL.statement ([tSchema], configDbHoistedTxSettings) (SchemaCache.accessibleFuncs configDbPreparedStatements) tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements)
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements)) MaybeDbResult plan . Just <$> ((,,)
OAIgnorePriv -> (HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
MaybeDbResult plan . Just <$> ((,,) <$> SQL.statement ([tSchema], configDbHoistedTxSettings) (SchemaCache.accessibleFuncs configDbPreparedStatements)
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache) <*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache) OAIgnorePriv ->
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements)) MaybeDbResult plan . Just <$> ((,,)
OADisabled -> (HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
pure $ MaybeDbResult plan Nothing (HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
OADisabled ->
pure $ MaybeDbResult plan Nothing
-- Makes sure the querystring pk matches the payload pk -- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
@@ -291,3 +298,7 @@ runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequ
("select " <> fromQi req <> "()") ("select " <> fromQi req <> "()")
HD.noResult HD.noResult
(configDbPreparedStatements conf) (configDbPreparedStatements conf)
getSQLQuery :: Query -> ByteString
getSQLQuery DbQuery{dqSQL} = dqSQL
getSQLQuery _ = mempty
+8 -8
View File
@@ -46,14 +46,14 @@ import Protolude
readPlanToQuery :: ReadPlanTree -> SQL.Snippet readPlanToQuery :: ReadPlanTree -> SQL.Snippet
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect} forest) = readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect} forest) =
"SELECT " <> "SELECT " <>
intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ joinsSelects) <> " " <> intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ joinsSelects) <>
fromFrag <> " " <> fromFrag <>
intercalateSnippet " " joins <> " " <> intercalateSnippet " " joins <>
(if null logicForest && null relJoinConds (if null logicForest && null relJoinConds
then mempty then mempty
else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <> else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
groupF qi select relSelect <> " " <> groupF qi select relSelect <>
orderF qi order <> " " <> orderF qi order <>
limitOffsetF readRange limitOffsetF readRange
where where
fromFrag = fromF relToParent mainQi fromAlias fromFrag = fromF relToParent mainQi fromAlias
@@ -94,7 +94,7 @@ getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet
getJoin fld node@(Node ReadPlan{relJoinType} _) = getJoin fld node@(Node ReadPlan{relJoinType} _) =
let let
correlatedSubquery sub al cond = correlatedSubquery sub al cond =
(if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond " " <> (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
subquery = readPlanToQuery node subquery = readPlanToQuery node
aggAlias = pgFmtIdent $ rsAggAlias fld aggAlias = pgFmtIdent $ rsAggAlias fld
in in
@@ -258,7 +258,7 @@ getQualifiedIdentifier rel mainQi tblAlias = case rel of
-- FROM clause plus implicit joins -- FROM clause plus implicit joins
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
fromF rel mainQi tblAlias = "FROM " <> fromF rel mainQi tblAlias = " FROM " <>
(case rel of (case rel of
-- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading. -- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.
-- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386 -- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386
+12 -9
View File
@@ -56,10 +56,11 @@ data ResultSet
prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler -> prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler ->
Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> (SQL.Statement () ResultSet, ByteString)
prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys = prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys prepared =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt (result, sql)
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''" checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int" pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int"
snippet = snippet =
@@ -93,10 +94,11 @@ prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pK
MTVndPlan{} -> planRow MTVndPlan{} -> planRow
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False) _ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> (SQL.Statement () ResultSet, ByteString)
prepareRead selectQuery countQuery countTotal mt handler = prepareRead selectQuery countQuery countTotal mt handler prepared =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt (result, sql)
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
snippet = snippet =
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <> "WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
countCTEF <> " " <> countCTEF <> " " <>
@@ -118,10 +120,11 @@ prepareRead selectQuery countQuery countTotal mt handler =
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
MediaType -> MediaHandler -> Bool -> MediaType -> MediaHandler -> Bool ->
SQL.Statement () ResultSet (SQL.Statement () ResultSet, ByteString)
prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler = prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler prepared =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt (result, sql)
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
snippet = snippet =
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <> "WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
countCTEF <> countCTEF <>
@@ -308,6 +308,23 @@
pdSchema: public pdSchema: public
pdVolatility: Volatile pdVolatility: Volatile
- - qiName: root
qiSchema: public
- - pdDescription: null
pdFuncSettings: []
pdHasVariadic: false
pdName: root
pdParams: []
pdReturnType:
contents:
contents:
qiName: json
qiSchema: pg_catalog
tag: Scalar
tag: Single
pdSchema: public
pdVolatility: Volatile
- - qiName: uses_prepared_statements - - qiName: uses_prepared_statements
qiSchema: public qiSchema: public
- - pdDescription: null - - pdDescription: null
@@ -10,6 +10,18 @@
tableSchema: public tableSchema: public
tableUpdatable: true tableUpdatable: true
- - qiName: infinite_recursion
qiSchema: public
- tableColumns: {}
tableDeletable: false
tableDescription: null
tableInsertable: false
tableIsView: true
tableName: infinite_recursion
tablePKCols: []
tableSchema: public
tableUpdatable: false
- - qiName: cats - - qiName: cats
qiSchema: public qiSchema: public
- tableColumns: - tableColumns:
+1
View File
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
+1
View File
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
@@ -25,6 +25,7 @@ jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 7200 jwt-cache-max-lifetime = 7200
log-level = "info" log-level = "info"
log-query = "main-query"
openapi-mode = "disabled" openapi-mode = "disabled"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "https://otherexample.org/api" openapi-server-proxy-uri = "https://otherexample.org/api"
@@ -25,6 +25,7 @@ jwt-secret = "OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE"
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 3600 jwt-cache-max-lifetime = 3600
log-level = "info" log-level = "info"
log-query = "main-query"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://example.org/api" openapi-server-proxy-uri = "https://example.org/api"
@@ -25,6 +25,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ="
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
jwt-cache-max-lifetime = 86400 jwt-cache-max-lifetime = 86400
log-level = "info" log-level = "info"
log-query = "main-query"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
+1
View File
@@ -25,6 +25,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
jwt-cache-max-lifetime = 0 jwt-cache-max-lifetime = 0
log-level = "error" log-level = "error"
log-query = "disabled"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
+1
View File
@@ -28,6 +28,7 @@ PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ=
PGRST_JWT_SECRET_IS_BASE64: true PGRST_JWT_SECRET_IS_BASE64: true
PGRST_JWT_CACHE_MAX_LIFETIME: 86400 PGRST_JWT_CACHE_MAX_LIFETIME: 86400
PGRST_LOG_LEVEL: info PGRST_LOG_LEVEL: info
PGRST_LOG_QUERY: 'main-query'
PGRST_OPENAPI_MODE: 'ignore-privileges' PGRST_OPENAPI_MODE: 'ignore-privileges'
PGRST_OPENAPI_SECURITY_ACTIVE: true PGRST_OPENAPI_SECURITY_ACTIVE: true
PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org' PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
+1
View File
@@ -25,6 +25,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ="
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
jwt-cache-max-lifetime = 86400 jwt-cache-max-lifetime = 86400
log-level = "info" log-level = "info"
log-query = "main-query"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
+1
View File
@@ -47,6 +47,7 @@ ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_idletime = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_lifetime = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_lifetime = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_uri = 'postgresql://ignored'; ALTER ROLE db_config_authenticator SET pgrst.db_uri = 'postgresql://ignored';
ALTER ROLE db_config_authenticator SET pgrst.log_level = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.log_level = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.log_query = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.server_host = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.server_host = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.server_port = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.server_port = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket = 'ignored';
+10
View File
@@ -243,3 +243,13 @@ end $_$ volatile security definer language plpgsql ;
create function test.get_current_schema() returns text as $$ create function test.get_current_schema() returns text as $$
select current_schema()::text; select current_schema()::text;
$$ language sql; $$ language sql;
create or replace function root() returns json as $_$
select '{"swagger": "2.0"}'::json;
$_$ language sql;
create view infinite_recursion as
select * from projects;
create or replace view infinite_recursion as
select * from infinite_recursion;
+60
View File
@@ -980,6 +980,66 @@ def test_log_level(level, defaultenv):
assert len(output) == 7 assert len(output) == 7
@pytest.mark.parametrize("level", ["crit", "error", "warn", "info", "debug"])
def test_log_query(level, defaultenv):
"log_query=true should log the SQL query according to the log_level"
env = {
**defaultenv,
"PGRST_LOG_LEVEL": level,
"PGRST_LOG_QUERY": "main-query",
# The root path can only log SQL when a function is set in db-root-spec
"PGRST_DB_ROOT_SPEC": "root",
}
with run(env=env) as postgrest:
response = postgrest.session.get("/")
assert response.status_code == 200
response = postgrest.session.get("/projects")
assert response.status_code == 200
response = postgrest.session.get("/unknown")
assert response.status_code == 404
response = postgrest.session.get("/infinite_recursion")
assert response.status_code == 500
root_2xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."root"\(\) pgrst_scalar.+_postgrest_t'
get_2xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."projects"\.\* FROM "public"\."projects".+_postgrest_t'
unknown_4xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."unknown"\.\* FROM "public"\."unknown".+_postgrest_t'
infinite_recursion_5xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."infinite_recursion"\.\* FROM "public"\."infinite_recursion".+_postgrest_t'
if level == "crit":
output = postgrest.read_stdout(nlines=1)
assert len(output) == 0
elif level == "error":
output = postgrest.read_stdout(nlines=4)
assert re.match(infinite_recursion_5xx_regx, output[1])
assert len(output) == 3
elif level == "warn":
output = postgrest.read_stdout(nlines=6)
assert re.match(unknown_4xx_regx, output[0])
assert re.match(infinite_recursion_5xx_regx, output[3])
assert len(output) == 5
elif level == "info":
output = postgrest.read_stdout(nlines=10)
assert re.match(root_2xx_regx, output[0])
assert re.match(get_2xx_regx, output[2])
assert re.match(unknown_4xx_regx, output[4])
assert re.match(infinite_recursion_5xx_regx, output[7])
assert len(output) == 9
elif level == "debug":
output_ok = postgrest.read_stdout(nlines=8)
assert re.match(root_2xx_regx, output_ok[2])
assert re.match(get_2xx_regx, output_ok[6])
assert len(output_ok) == 8
output_err = postgrest.read_stdout(nlines=10)
assert re.match(unknown_4xx_regx, output_err[2])
assert re.match(infinite_recursion_5xx_regx, output_err[7])
assert len(output_err) == 9
def test_no_pool_connection_required_on_bad_http_logic(defaultenv): def test_no_pool_connection_required_on_bad_http_logic(defaultenv):
"no pool connection should be consumed for failing on invalid http logic" "no pool connection should be consumed for failing on invalid http logic"
+2
View File
@@ -30,6 +30,7 @@ import Data.String (String)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
JSPathExp (..), JSPathExp (..),
LogLevel (..), LogLevel (..),
LogQuery (..),
OpenAPIMode (..), OpenAPIMode (..),
parseSecret) parseSecret)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
@@ -138,6 +139,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configJwtCacheMaxLifetime = 0 , configJwtCacheMaxLifetime = 0
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configLogQuery = LogQueryDisabled
, configOpenApiMode = OAFollowPriv , configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False , configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing , configOpenApiServerProxyUri = Nothing