Compare commits
@@ -112,7 +112,7 @@ runs:
|
|||||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
||||||
- name: Save artifact to GitHub Actions
|
- name: Save artifact to GitHub Actions
|
||||||
if: steps.find-task.outputs.task_found
|
if: steps.find-task.outputs.task_found
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.upload }}
|
name: ${{ inputs.upload }}
|
||||||
path: ${{ steps.download.outputs.artifacts }}
|
path: ${{ steps.download.outputs.artifacts }}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ inputs.prefix }}-
|
${{ runner.os }}-${{ inputs.prefix }}-
|
||||||
- uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
# This actions creates the github token using the postgrest app secrets
|
# This actions creates the github token using the postgrest app secrets
|
||||||
- name: Create Github App Token
|
- name: Create Github App Token
|
||||||
id: app-token
|
id: app-token
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Build static executable
|
- name: Build static executable
|
||||||
run: nix-build -A postgrestStatic -A postgrestStatic.tests
|
run: nix-build -A postgrestStatic -A postgrestStatic.tests
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-linux-static-x86-64
|
name: postgrest-linux-static-x86-64
|
||||||
path: result/bin/postgrest
|
path: result/bin/postgrest
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
- name: Save built Docker image as artifact
|
- name: Save built Docker image as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x86-64
|
name: postgrest-docker-x86-64
|
||||||
path: postgrest-docker.tar.gz
|
path: postgrest-docker.tar.gz
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: haskell-actions/setup@f9150cb1d140e9a9271700670baa38991e6fa25c # v2.10.3
|
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.6.7
|
ghc-version: 9.6.7
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
- name: Strip Executable
|
- name: Strip Executable
|
||||||
run: strip result/postgrest*
|
run: strip result/postgrest*
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact }}
|
name: ${{ matrix.artifact }}
|
||||||
path: |
|
path: |
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: haskell-actions/setup@f9150cb1d140e9a9271700670baa38991e6fa25c # v2.10.3
|
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
- name: Cache .cabal
|
- name: Cache .cabal
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
echo "Relevant extract from CHANGELOG.md:"
|
echo "Relevant extract from CHANGELOG.md:"
|
||||||
cat CHANGES.md
|
cat CHANGES.md
|
||||||
- name: Save CHANGES.md as artifact
|
- name: Save CHANGES.md as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-changes
|
name: release-changes
|
||||||
path: CHANGES.md
|
path: CHANGES.md
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||||
|
|
||||||
- name: Save release bundle
|
- name: Save release bundle
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: release-bundle
|
path: release-bundle
|
||||||
|
|||||||
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [14.10] - 2026-04-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
|
||||||
|
|
||||||
|
## [14.9] - 2026-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Remove red herring warp logs on default log-level, only emit them on `log-level=debug` by @steve-chavez in #4799
|
||||||
|
|
||||||
## [14.8] - 2026-04-03
|
## [14.8] - 2026-04-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||||
# nix/tools/docker/README.md
|
# nix/tools/docker/README.md
|
||||||
|
|
||||||
FROM ubuntu:noble@sha256:186072bba1b2f436cbb91ef2567abca677337cfc786c86e107d25b7072feef0c AS postgrest
|
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS postgrest
|
||||||
|
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ Community Tutorials
|
|||||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||||
|
|
||||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||||
|
|
||||||
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
||||||
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Custom Queries
|
|||||||
|
|
||||||
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
|
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
|
||||||
|
|
||||||
* Table unions
|
* SET operators like `UNION, INTERSECT and EXCEPT <https://www.postgresql.org/docs/current/queries-union.html>`_.
|
||||||
* More complicated joins than those provided by :ref:`resource_embedding`.
|
* More complicated joins than those provided by :ref:`resource_embedding`.
|
||||||
* Geo-spatial queries that require an argument, like "points near (lat,lon)"
|
* Geo-spatial queries that require an argument, like "points near (lat,lon)"
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
- Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
||||||
|
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
|
||||||
|
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
|
||||||
|
|
||||||
.. _listener_automatic_recovery:
|
.. _listener_automatic_recovery:
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 14.8
|
version: 14.10
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -304,6 +304,7 @@ test-suite observability
|
|||||||
other-modules: ObsHelper
|
other-modules: ObsHelper
|
||||||
Observation.JwtCache
|
Observation.JwtCache
|
||||||
Observation.MetricsSpec
|
Observation.MetricsSpec
|
||||||
|
Observation.SchemaCacheSpec
|
||||||
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
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ run appState = do
|
|||||||
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
|
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
|
||||||
onWarpException _ ex =
|
onWarpException _ ex =
|
||||||
when (shouldDisplayException ex) $
|
when (shouldDisplayException ex) $
|
||||||
observer $ WarpErrorObs $ show ex
|
observer $ WarpServerObs $ show ex
|
||||||
|
|
||||||
-- Similar to wai defaultShouldDisplayException in
|
-- Similar to wai defaultShouldDisplayException in
|
||||||
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
|
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
|
||||||
|
|||||||
@@ -219,10 +219,14 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
|
|||||||
|
|
||||||
-- | Flush the connection pool so that any future use of the pool will
|
-- | Flush the connection pool so that any future use of the pool will
|
||||||
-- use connections freshly established after this call.
|
-- use connections freshly established after this call.
|
||||||
|
-- | Emits PoolFlushed observation
|
||||||
flushPool :: AppState -> IO ()
|
flushPool :: AppState -> IO ()
|
||||||
flushPool AppState{..} = SQL.release statePool
|
flushPool AppState{..} = do
|
||||||
|
SQL.release statePool
|
||||||
|
stateObserver PoolFlushed
|
||||||
|
|
||||||
-- | Destroy the pool on shutdown.
|
-- | Destroy the pool on shutdown.
|
||||||
|
-- | Differs from flushPool in not emiting PoolFlushed observation.
|
||||||
destroyPool :: AppState -> IO ()
|
destroyPool :: AppState -> IO ()
|
||||||
destroyPool AppState{..} = SQL.release statePool
|
destroyPool AppState{..} = SQL.release statePool
|
||||||
|
|
||||||
@@ -309,8 +313,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
observer $ ConnectionRetryObs delay
|
observer $ ConnectionRetryObs delay
|
||||||
putNextListenerDelay appState delay
|
putNextListenerDelay appState delay
|
||||||
|
|
||||||
flushPool appState
|
|
||||||
|
|
||||||
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@@ -359,6 +361,10 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||||
putSCacheStatus appState SCPending
|
putSCacheStatus appState SCPending
|
||||||
putSchemaCache appState $ Just sCache
|
putSchemaCache appState $ Just sCache
|
||||||
|
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
||||||
|
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
||||||
|
-- and after marking sCacheStatus as pending,
|
||||||
|
flushPool appState
|
||||||
observer $ SchemaCacheQueriedObs resultTime
|
observer $ SchemaCacheQueriedObs resultTime
|
||||||
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||||
observer $ SchemaCacheLoadedObs t
|
observer $ SchemaCacheLoadedObs t
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ import PostgREST.Version (prettyVersion)
|
|||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
import qualified PostgREST.Config as Config
|
import qualified PostgREST.Config as Config
|
||||||
|
|
||||||
import Data.Either.Combinators (whenRight)
|
import Control.Arrow ((&&&))
|
||||||
import Protolude
|
import Data.Bitraversable (bisequence)
|
||||||
|
import Data.Either.Combinators (whenRight)
|
||||||
|
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||||
|
import qualified Hasql.Session as SQL
|
||||||
|
import PostgREST.Config.Database (queryPgVersion)
|
||||||
|
import PostgREST.Config.PgVersion (pgvFullName)
|
||||||
|
import Protolude
|
||||||
|
|
||||||
-- | Starts the Listener in a thread
|
-- | Starts the Listener in a thread
|
||||||
runListener :: AppState -> IO ()
|
runListener :: AppState -> IO ()
|
||||||
@@ -63,6 +69,9 @@ retryingListen appState = do
|
|||||||
\case
|
\case
|
||||||
Right db -> do
|
Right db -> do
|
||||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
|
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
||||||
|
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
|
||||||
|
|
||||||
AppState.putIsListenerOn appState True
|
AppState.putIsListenerOn appState True
|
||||||
|
|
||||||
delay <- AppState.getNextListenerDelay appState
|
delay <- AppState.getNextListenerDelay appState
|
||||||
@@ -72,7 +81,7 @@ retryingListen appState = do
|
|||||||
-- reset the delay
|
-- reset the delay
|
||||||
AppState.putNextListenerDelay appState 1
|
AppState.putNextListenerDelay appState 1
|
||||||
|
|
||||||
observer $ DBListenStart dbChannel
|
observer $ DBListenStart pqHost pqPort pgFullName dbChannel
|
||||||
|
|
||||||
-- wait for notifications
|
-- wait for notifications
|
||||||
-- this will never return, in case of an error it will throw and be caught by onError
|
-- this will never return, in case of an error it will throw and be caught by onError
|
||||||
|
|||||||
@@ -110,12 +110,18 @@ observationLogger loggerState logLevel obs = case obs of
|
|||||||
o@PoolRequestFullfilled ->
|
o@PoolRequestFullfilled ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
o@PoolFlushed ->
|
||||||
|
when (logLevel >= LogDebug) $ do
|
||||||
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@JwtCacheEviction ->
|
o@JwtCacheEviction ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(JwtCacheLookup _) ->
|
o@(JwtCacheLookup _) ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
o@(WarpServerObs _) ->
|
||||||
|
when (logLevel >= LogDebug) $ do
|
||||||
|
logWithZTime loggerState $ observationMessage o
|
||||||
o ->
|
o ->
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ data Observation
|
|||||||
| SchemaCacheSummaryObs Text
|
| SchemaCacheSummaryObs Text
|
||||||
| SchemaCacheLoadedObs Double
|
| SchemaCacheLoadedObs Double
|
||||||
| ConnectionRetryObs Int
|
| ConnectionRetryObs Int
|
||||||
| DBListenStart Text
|
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
||||||
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
||||||
@@ -64,10 +64,11 @@ data Observation
|
|||||||
| HasqlPoolObs SQL.Observation
|
| HasqlPoolObs SQL.Observation
|
||||||
| PoolRequest
|
| PoolRequest
|
||||||
| PoolRequestFullfilled
|
| PoolRequestFullfilled
|
||||||
|
| PoolFlushed
|
||||||
| JwtCacheLookup Bool
|
| JwtCacheLookup Bool
|
||||||
| JwtCacheEviction
|
| JwtCacheEviction
|
||||||
| TerminationUnixSignalObs Text
|
| TerminationUnixSignalObs Text
|
||||||
| WarpErrorObs Text
|
| WarpServerObs Text
|
||||||
deriving (Generic)
|
deriving (Generic)
|
||||||
|
|
||||||
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
||||||
@@ -114,8 +115,8 @@ observationMessage = \case
|
|||||||
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||||
QueryPgVersionError usageErr ->
|
QueryPgVersionError usageErr ->
|
||||||
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||||
DBListenStart channel -> do
|
DBListenStart host port fullName channel -> do
|
||||||
"Listening for database notifications on the " <> show channel <> " channel"
|
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||||
either showListenerConnError showListenerException listenErr
|
either showListenerConnError showListenerException listenErr
|
||||||
@@ -161,14 +162,16 @@ observationMessage = \case
|
|||||||
"Trying to borrow a connection from pool"
|
"Trying to borrow a connection from pool"
|
||||||
PoolRequestFullfilled ->
|
PoolRequestFullfilled ->
|
||||||
"Borrowed a connection from the pool"
|
"Borrowed a connection from the pool"
|
||||||
|
PoolFlushed ->
|
||||||
|
"Database connection pool flushed"
|
||||||
JwtCacheLookup _ ->
|
JwtCacheLookup _ ->
|
||||||
"Looked up a JWT in JWT cache"
|
"Looked up a JWT in JWT cache"
|
||||||
JwtCacheEviction ->
|
JwtCacheEviction ->
|
||||||
"Evicted entry from JWT cache"
|
"Evicted entry from JWT cache"
|
||||||
TerminationUnixSignalObs signal ->
|
TerminationUnixSignalObs signal ->
|
||||||
"Received termination unix signal " <> signal
|
"Received termination unix signal " <> signal
|
||||||
WarpErrorObs txt ->
|
WarpServerObs txt ->
|
||||||
"Warp server error: " <> txt
|
"Warp server: " <> txt
|
||||||
where
|
where
|
||||||
showMillis :: Double -> Text
|
showMillis :: Double -> Text
|
||||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||||
|
|||||||
@@ -55,13 +55,15 @@ def test_openapi_in_big_schema(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_stackoverflow_is_logged(defaultenv):
|
@pytest.mark.parametrize("level", ["crit", "error", "warn", "info", "debug"])
|
||||||
"Stack overflow errors should be logged with the Warp error message"
|
def test_stackoverflow_is_logged(level, defaultenv):
|
||||||
|
"Stack overflow should be logged with the Warp message only on log-level=debug"
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
**defaultenv,
|
**defaultenv,
|
||||||
"PGRST_DB_SCHEMAS": "apflora",
|
"PGRST_DB_SCHEMAS": "apflora",
|
||||||
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
|
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
|
||||||
|
"PGRST_LOG_LEVEL": level,
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:
|
with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:
|
||||||
@@ -69,9 +71,15 @@ def test_stackoverflow_is_logged(defaultenv):
|
|||||||
postgrest.session.get("/")
|
postgrest.session.get("/")
|
||||||
|
|
||||||
output = postgrest.read_stdout(nlines=10)
|
output = postgrest.read_stdout(nlines=10)
|
||||||
output.extend(postgrest.read_stdout(nlines=10))
|
for _ in range(3):
|
||||||
|
output.extend(postgrest.read_stdout(nlines=10))
|
||||||
|
|
||||||
assert any("Warp server error: stack overflow" in line for line in output)
|
found = any("Warp server: stack overflow" in line for line in output)
|
||||||
|
|
||||||
|
if level == "debug":
|
||||||
|
assert found
|
||||||
|
else:
|
||||||
|
assert not found
|
||||||
|
|
||||||
|
|
||||||
# See: https://github.com/PostgREST/postgrest/issues/3329
|
# See: https://github.com/PostgREST/postgrest/issues/3329
|
||||||
|
|||||||
+72
-46
@@ -21,6 +21,19 @@ from postgrest import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def match_log(output, matchers):
|
||||||
|
ito = iter(output)
|
||||||
|
itm = iter(matchers)
|
||||||
|
nextMatcher = next(itm, None)
|
||||||
|
while nextMatcher is not None and (line := next(ito, None)) is not None:
|
||||||
|
if re.match(nextMatcher, line) is not None:
|
||||||
|
nextMatcher = next(itm, None)
|
||||||
|
if nextMatcher is not None:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Expected log line matching {nextMatcher} not found in output"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_connect_with_dburi(dburi, defaultenv):
|
def test_connect_with_dburi(dburi, defaultenv):
|
||||||
"Connecting with db-uri instead of LIPQ* environment variables should work."
|
"Connecting with db-uri instead of LIPQ* environment variables should work."
|
||||||
defaultenv_without_libpq = {
|
defaultenv_without_libpq = {
|
||||||
@@ -679,55 +692,45 @@ def test_log_level(level, defaultenv):
|
|||||||
response = postgrest.session.get("/")
|
response = postgrest.session.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
output = sorted(postgrest.read_stdout(nlines=7))
|
output = postgrest.read_stdout(nlines=9)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
elif level == "error":
|
elif level == "error":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"'],
|
||||||
)
|
)
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
elif level == "warn":
|
elif level == "warn":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
)
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
assert re.match(
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
],
|
||||||
output[1],
|
|
||||||
)
|
)
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
elif level == "info":
|
elif level == "info":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
)
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
assert re.match(
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
output[1],
|
],
|
||||||
)
|
|
||||||
assert re.match(
|
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
|
||||||
output[2],
|
|
||||||
)
|
)
|
||||||
assert len(output) == 3
|
assert len(output) == 3
|
||||||
elif level == "debug":
|
elif level == "debug":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert len(output) == 9
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
|
||||||
output[1],
|
|
||||||
)
|
|
||||||
assert re.match(
|
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
|
||||||
output[2],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(output) == 7
|
|
||||||
assert any("Connection" and "is available" in line for line in output)
|
assert any("Connection" and "is available" in line for line in output)
|
||||||
assert any("Connection" and "is used" in line for line in output)
|
assert any("Connection" and "is used" in line for line in output)
|
||||||
|
|
||||||
@@ -1364,16 +1367,21 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):
|
|||||||
assert response.status_code == 500
|
assert response.status_code == 500
|
||||||
|
|
||||||
# ensure the message appears on the logs
|
# ensure the message appears on the logs
|
||||||
output = sorted(postgrest.read_stdout(nlines=6))
|
output = postgrest.read_stdout(nlines=8)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
elif level == "debug":
|
elif level == "debug":
|
||||||
assert " 500 " in output[0]
|
match_log(
|
||||||
assert "canceling statement due to statement timeout" in output[5]
|
output,
|
||||||
|
[
|
||||||
|
r".*canceling statement due to statement timeout.*",
|
||||||
|
r".*500.*",
|
||||||
|
],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
assert " 500 " in output[0]
|
assert " 500 " in output[1]
|
||||||
assert "canceling statement due to statement timeout" in output[1]
|
assert "canceling statement due to statement timeout" in output[0]
|
||||||
|
|
||||||
reset_statement_timeout(metapostgrest, role)
|
reset_statement_timeout(metapostgrest, role)
|
||||||
|
|
||||||
@@ -1567,18 +1575,17 @@ def test_log_pool_req_observation(level, defaultenv):
|
|||||||
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
|
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
|
||||||
|
|
||||||
pool_req = "Trying to borrow a connection from pool"
|
pool_req = r".*Trying to borrow a connection from pool.*"
|
||||||
pool_req_fullfill = "Borrowed a connection from the pool"
|
pool_req_fullfill = r".*Borrowed a connection from the pool.*"
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
|
|
||||||
postgrest.session.get("/authors_only", headers=headers)
|
postgrest.session.get("/authors_only", headers=headers)
|
||||||
|
|
||||||
if level == "debug":
|
if level == "debug":
|
||||||
output = postgrest.read_stdout(nlines=5)
|
output = postgrest.read_stdout(nlines=7)
|
||||||
assert pool_req in output[1]
|
assert len(output) == 7
|
||||||
assert pool_req_fullfill in output[4]
|
match_log(output, [pool_req, pool_req_fullfill])
|
||||||
assert len(output) == 5
|
|
||||||
elif level == "info":
|
elif level == "info":
|
||||||
output = postgrest.read_stdout(nlines=4)
|
output = postgrest.read_stdout(nlines=4)
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
@@ -1658,6 +1665,25 @@ def test_log_listener_connection_errors(defaultenv):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_listener_connection_start(defaultenv):
|
||||||
|
"The logs should show the listener connection start message in a single line"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env, no_startup_stdout=False, wait_for_readiness=True) as postgrest:
|
||||||
|
output = postgrest.read_stdout(nlines=10)
|
||||||
|
# Check for the listener start message containing host and port
|
||||||
|
# Do not check if pg version is displayed properly as it is tricky to test it
|
||||||
|
assert any(
|
||||||
|
f'"{defaultenv["PGHOST"]}:5432" and listening for database notifications on the "pgrst" channel'
|
||||||
|
in line
|
||||||
|
for line in output
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_db_pre_config_with_pg_reserved_words(defaultenv):
|
def test_db_pre_config_with_pg_reserved_words(defaultenv):
|
||||||
"The db-pre-config should not fail unexpectedly when function name is a postgres reserved word"
|
"The db-pre-config should not fail unexpectedly when function name is a postgres reserved word"
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import PostgREST.SchemaCache (querySchemaCache)
|
|||||||
import qualified Observation.JwtCache
|
import qualified Observation.JwtCache
|
||||||
import qualified Observation.MetricsSpec
|
import qualified Observation.MetricsSpec
|
||||||
|
|
||||||
import ObsHelper
|
import qualified Observation.SchemaCacheSpec
|
||||||
import PostgREST.Observation (Observation (HasqlPoolObs))
|
import ObsHelper
|
||||||
import Protolude hiding (toList, toS)
|
import PostgREST.Observation (Observation (HasqlPoolObs))
|
||||||
import Test.Hspec
|
import Protolude hiding (toList, toS)
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
@@ -64,6 +65,8 @@ main = do
|
|||||||
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
||||||
before (initApp baseSchemaCache testCfg) $
|
before (initApp baseSchemaCache testCfg) $
|
||||||
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
|
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
|
||||||
|
before (initApp baseSchemaCache testCfg) $
|
||||||
|
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
loadSCache pool conf =
|
loadSCache pool conf =
|
||||||
|
|||||||
@@ -206,5 +206,5 @@ waitForObs (ObsChan orig copy) t msg f =
|
|||||||
obsDiagMessage :: Observation -> Text
|
obsDiagMessage :: Observation -> Text
|
||||||
obsDiagMessage = \case
|
obsDiagMessage = \case
|
||||||
(HasqlPoolObs o) -> show o
|
(HasqlPoolObs o) -> show o
|
||||||
o@(DBListenStart channel) -> constrName o <> show channel
|
o@(DBListenStart host port name channel) -> constrName o <> show (host, port, name, channel)
|
||||||
o -> constrName o
|
o -> constrName o
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE MonadComprehensions #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Observation.SchemaCacheSpec where
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
import ObsHelper
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
|
import PostgREST.Config (configDbSchemas)
|
||||||
|
import PostgREST.Observation
|
||||||
|
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 emit PoolFlushed, SchemaCacheQueriedObs and SchemaCacheLoadedObs when schema cache is reloaded" $ do
|
||||||
|
SpecState{specAppState = appState, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
|
waitFor (1 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
|
||||||
|
|
||||||
|
|
||||||
|
it "Should flush pool once when schema reloading retries" $ do
|
||||||
|
SpecState{specAppState = appState, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
AppState.getConfig appState >>= \cfg -> do
|
||||||
|
AppState.putConfig appState $ cfg { configDbSchemas = pure "bad_schema" }
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
|
waitFor (1 * sec) "SchemaCacheErrorObs" $ \x -> [ o | o@SchemaCacheErrorObs{} <- pure x ]
|
||||||
|
|
||||||
|
-- Restore configuration
|
||||||
|
AppState.putConfig appState cfg
|
||||||
|
|
||||||
|
-- Wait for 2 seconds so that retry can happen
|
||||||
|
waitFor (2 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
|
||||||
|
where
|
||||||
|
sec = 1000000
|
||||||
Reference in New Issue
Block a user