Skip to content

Commit d4114c6

Browse files
committed
add: produce OpenTelemetry traces
* Introduces producing OpenTelemetry traces with hs-opentelemetry. * Adds OTel spans over the whole application loop and over each request processing phase * Preliminary OTel tracing support in spec tests * Disables tracing in load and memory tests fix deps
1 parent e1d5476 commit d4114c6

40 files changed

Lines changed: 701 additions & 52 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ All notable changes to this project will be documented in this file. From versio
5353
### Added
5454

5555
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
56+
- Introduced producing OpenTelemetry traces by @develop7 in #3140
57+
+ Requires a new `otel-enabled` config parameter to be enabled first.
5658

5759
### Fixed
5860

cabal.project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
packages: postgrest.cabal
22
tests: true
33
allow-newer:
4-
hasql:postgresql-libpq
4+
hasql:postgresql-libpq

cabal.project.freeze

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
index-state: hackage.haskell.org 2026-04-18T18:42:36Z
1+
index-state: hackage.haskell.org 2026-05-15T10:51:09Z

docs/postgrest.dict

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ Observability
109109
Okta
110110
OpenAPI
111111
openapi
112+
OpenTelemetry
113+
otel
114+
OTLP
112115
ov
113116
parametrized
114117
passphrase

docs/references/configuration.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,21 @@ openapi-server-proxy-uri
840840
]
841841
}
842842
843+
.. _otel-enabled:
844+
845+
otel-enabled
846+
-------------------
847+
848+
=============== =================================
849+
**Type** Boolean
850+
**Default** False
851+
**Reloadable** Y
852+
**Environment** PGRST_OTEL_ENABLED
853+
**In-Database** `pgrst.otel_enabled`
854+
=============== =================================
855+
856+
When this is set to :code:`true`, OpenTelemetry tracing is enabled. See :ref:`opentelemetry` for details and settings.
857+
843858
.. _server_cors_allowed_origins:
844859

845860
server-cors-allowed-origins

docs/references/observability.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,73 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
312312
HTTP/1.1 200 OK
313313
X-Request-Id: 123
314314
315+
.. _opentelemetry:
316+
317+
OpenTelemetry
318+
-------------
319+
320+
PostgREST is able to produce OpenTelemetry traces. OpenTelemetry is configured
321+
using ``OTEL_*`` environment variables, per the `OpenTelemetry specification`_.
322+
323+
The OpenTelemetry support is currently both experimental and in early stages of development, so expect some rough edges
324+
or lack of functionality, such as metrics or logs. Since current OpenTelemetry implementation incurs a small performance
325+
hit, it is gated behind the :ref:`otel-enabled` configuration option, disabled by default.
326+
327+
Example configuration:
328+
329+
.. code-block:: shell
330+
331+
OTEL_EXPORTER_OTLP_ENDPOINT='https://api.honeycomb.io/' \
332+
OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=<honeycomb_api_key>" \
333+
OTEL_SERVICE_NAME='PostgREST'\
334+
OTEL_LOG_LEVEL='debug'\
335+
OTEL_TRACES_SAMPLER='always_on' \
336+
postgrest
337+
338+
Prometheus metrics through the OpenTelemetry Collector
339+
------------------------------------------------------
340+
341+
PostgREST currently only exports traces through OpenTelemetry,-. To get metrics,
342+
it's possible to scrape the :ref:`metrics` endpoint exposed by the
343+
:ref:`admin_server` and relay it through the OpenTelemetry Collector.
344+
345+
Example collector configuration:
346+
347+
.. code-block:: yaml
348+
349+
receivers:
350+
prometheus:
351+
config:
352+
scrape_configs:
353+
- job_name: postgrest
354+
scrape_interval: 15s
355+
metrics_path: /metrics
356+
static_configs:
357+
- targets: ["127.0.0.1:3001"]
358+
359+
processors:
360+
batch:
361+
362+
exporters:
363+
otlp:
364+
endpoint: otel-collector:4317
365+
tls:
366+
insecure: true
367+
368+
service:
369+
pipelines:
370+
metrics:
371+
receivers: [prometheus]
372+
processors: [batch]
373+
exporters: [otlp]
374+
375+
This assumes the PostgREST :ref:`admin_server` is listening on port ``3001`` and that
376+
its ``/metrics`` endpoint is reachable by the collector. Adjust the target address and
377+
OTLP exporter settings for your deployment.
378+
379+
.. _`OpenTelemetry specification`: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
380+
381+
315382
Proxy-Status Header
316383
-------------------
317384

nix/overlays/haskell-packages.nix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ let
7676
}
7777
{ };
7878

79+
# Support GHC 9.12
80+
proto-lens = lib.doJailbreak prev.proto-lens;
81+
proto-lens-runtime = lib.doJailbreak prev.proto-lens-runtime;
82+
7983
warp =
8084
lib.dontCheck (prev.callHackageDirect
8185
{

nix/tools/tests.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
, jq
1111
, lib
1212
, nginx
13+
, opentelemetry-collector
1314
, postgrest
1415
, python3
1516
, runtimeShell
@@ -40,6 +41,7 @@ let
4041
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
4142
workingDir = "/";
4243
withEnv = postgrest.env;
44+
withPath = [ opentelemetry-collector ];
4345
}
4446
''
4547
${withTools.withPg} -f test/observability/fixtures/load.sql \

postgrest.cabal

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ library
8080
PostgREST.Query.QueryBuilder
8181
PostgREST.Query.SqlFragment
8282
PostgREST.Query.Statements
83+
PostgREST.OpenTelemetry
8384
PostgREST.Plan
8485
PostgREST.Plan.CallPlan
8586
PostgREST.Plan.MutatePlan
@@ -116,6 +117,7 @@ library
116117
, crypton < 1.1.0
117118
, directory >= 1.2.6 && < 1.4
118119
, either >= 4.4.1 && < 5.1
120+
, exceptions >= 0.10 && < 0.12
119121
, extra >= 1.7.0 && < 2.0
120122
, fast-logger >= 3.2.0 && < 3.3
121123
, fuzzyset >= 0.2.4 && < 0.3
@@ -126,6 +128,12 @@ library
126128
, hasql-transaction >= 1.0.1 && <= 1.2.1
127129
, http-client >= 0.7.19 && < 0.8
128130
, http-types >= 0.12.2 && < 0.13
131+
, hs-opentelemetry-sdk >= 0.1.0.0 && < 0.2.0.0
132+
, hs-opentelemetry-instrumentation-wai
133+
, hs-opentelemetry-api >= 0.3.1.0
134+
-- ^ this is due to hs-otel-sdk is not reexporting getTracerTracerProvider
135+
-- needed to initialize OpenTelemetry.middleware
136+
, hs-opentelemetry-utils-exceptions
129137
, insert-ordered-containers >= 0.2.2 && < 0.3
130138
, jose-jwt >= 0.9.6 && < 0.11
131139
, lens >= 4.14 && < 5.4
@@ -276,6 +284,8 @@ test-suite spec
276284
, hasql-pool >= 1.0.1 && <= 1.3.0.4
277285
, hasql-transaction >= 1.0.1 && <= 1.2.1
278286
, heredoc >= 0.2 && < 0.3
287+
, hs-opentelemetry-sdk >= 0.1.0.0 && < 0.2.0.0
288+
, hs-opentelemetry-instrumentation-hspec
279289
, hspec >= 2.3 && < 2.12
280290
, hspec-expectations >= 0.8.4 && < 0.9
281291
, hspec-wai >= 0.10 && < 0.12
@@ -311,12 +321,18 @@ test-suite observability
311321
hs-source-dirs: test/observability
312322
main-is: Main.hs
313323
other-modules: ObsHelper
324+
OTelHelper
314325
Observation.JwtCache
315326
Observation.MetricsSpec
327+
Observation.OpenTelemetry
316328
Observation.SchemaCacheSpec
317329
build-depends: base >= 4.9 && < 4.22
330+
, aeson >= 2.0.3 && < 2.3
318331
, base64-bytestring >= 1 && < 1.3
319332
, bytestring >= 0.10.8 && < 0.13
333+
, containers >= 0.5.7 && < 0.8
334+
, directory >= 1.2.6 && < 1.4
335+
, filepath >= 1.4.1 && < 1.6
320336
, hasql-pool >= 1.0.1 && <= 1.3.0.4
321337
, hasql-transaction >= 1.0.1 && <= 1.2.1
322338
, hspec >= 2.3 && < 2.12
@@ -325,10 +341,15 @@ test-suite observability
325341
, hspec-wai-json >= 0.10 && < 0.12
326342
, http-types >= 0.12.3 && < 0.13
327343
, jose-jwt >= 0.9.6 && < 0.11
344+
, network >= 2.6 && < 3.3
328345
, postgrest
329346
, prometheus-client >= 1.1.1 && < 1.2.0
347+
, process >= 1.4.2 && < 1.7
330348
, protolude >= 0.3.1 && < 0.4
331349
, text >= 1.2.2 && < 2.2
350+
, retry >= 0.7.4 && < 0.10
351+
, streaming-commons
352+
, temporary >= 1.3 && < 1.4
332353
, wai >= 3.2.1 && < 3.3
333354
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
334355
-fno-spec-constr -optP-Wno-nonportable-include-path

src/PostgREST/App.hs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,22 @@ import qualified Network.Wai as Wai
3535
import qualified Network.Wai.Handler.Warp as Warp
3636
import qualified Network.Wai.Header as WaiHeader
3737

38-
import qualified PostgREST.Admin as Admin
39-
import qualified PostgREST.ApiRequest as ApiRequest
40-
import qualified PostgREST.AppState as AppState
41-
import qualified PostgREST.Auth as Auth
42-
import qualified PostgREST.Cors as Cors
43-
import qualified PostgREST.Error as Error
44-
import qualified PostgREST.Listener as Listener
45-
import qualified PostgREST.MainTx as MainTx
46-
import qualified PostgREST.Plan as Plan
47-
import qualified PostgREST.Query as Query
48-
import qualified PostgREST.Response as Response
49-
import qualified PostgREST.Unix as Unix (installSignalHandlers)
38+
import qualified PostgREST.Admin as Admin
39+
import qualified PostgREST.ApiRequest as ApiRequest
40+
import qualified PostgREST.AppState as AppState
41+
import qualified PostgREST.Auth as Auth
42+
import qualified PostgREST.Cors as Cors
43+
import qualified PostgREST.Error as Error
44+
import qualified PostgREST.Listener as Listener
45+
import qualified PostgREST.MainTx as MainTx
46+
import qualified PostgREST.OpenTelemetry as OTel
47+
import qualified PostgREST.Plan as Plan
48+
import qualified PostgREST.Query as Query
49+
import qualified PostgREST.Response as Response
50+
import qualified PostgREST.Unix as Unix (installSignalHandlers)
5051

5152
import PostgREST.ApiRequest (ApiRequest (..))
52-
import PostgREST.AppState (AppState)
53+
import PostgREST.AppState (AppState, getOTelMaybeTracer)
5354
import PostgREST.Auth.Types (AuthResult (..))
5455
import PostgREST.Config (AppConfig (..))
5556
import PostgREST.Error (Error)
@@ -71,7 +72,7 @@ import qualified Network.Socket as NS
7172
import PostgREST.Unix (createAndBindDomainSocket)
7273
import Protolude hiding (Handler)
7374

74-
run :: AppState -> IO ()
75+
run :: HasCallStack => AppState -> IO ()
7576
run appState = do
7677
conf <- AppState.getConfig appState
7778

@@ -119,11 +120,12 @@ serverSettings AppConfig{..} =
119120
& setServerName ("postgrest/" <> prettyVersion)
120121

121122
-- | PostgREST application
122-
postgrest :: AppState.AppState -> IO () -> Wai.Application
123+
postgrest :: HasCallStack => AppState.AppState -> IO () -> Wai.Application
123124
postgrest appState connWorker =
125+
(OTel.oTelMiddleware . getOTelMaybeTracer) appState .
124126
traceHeaderMiddleware appState .
125127
Cors.middleware appState $
126-
\req respond -> do
128+
\req respond -> OTel.inSpanMDefault (getOTelMaybeTracer appState) "request" $ do
127129
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
128130
maybeSchemaCache <- AppState.getSchemaCache appState
129131

@@ -164,7 +166,8 @@ postgrest appState connWorker =
164166
ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp)
165167

166168
postgrestResponse
167-
:: AppState.AppState
169+
:: HasCallStack
170+
=> AppState.AppState
168171
-> AppConfig
169172
-> Maybe SchemaCache
170173
-> Maybe Double
@@ -186,14 +189,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
186189

187190
body <- lift $ Wai.strictRequestBody req
188191

189-
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
190-
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
192+
(parseTime, apiReq@ApiRequest{..}) <- withRequestPhase "parse" $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
193+
(planTime, plan) <- withRequestPhase "plan" $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
191194

192195
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
193196
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
194197
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
195198

196-
(txTime, txResult) <- withTiming conf $ do
199+
(txTime, txResult) <- withRequestPhase "query" $ do
197200
case tx of
198201
MainTx.NoDbTx r -> pure r
199202
MainTx.DbTx{..} -> do
@@ -206,7 +209,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
206209
lift $ whenLeft eitherResp $ obsQuery . Error.status
207210
liftEither eitherResp
208211

209-
(respTime, resp) <- withTiming conf $ do
212+
(respTime, resp) <- withRequestPhase "response" $ do
210213
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
211214
status' = either Error.status Response.pgrstStatus response
212215

@@ -230,6 +233,13 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
230233
varyHeaderPresent :: [HTTP.Header] -> Bool
231234
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
232235

236+
withOTel :: HasCallStack => Text -> ExceptT e IO a -> ExceptT e IO a
237+
withOTel label = do
238+
OTel.inSpanMDefault (getOTelMaybeTracer appState) label
239+
240+
withRequestPhase :: HasCallStack => Text -> ExceptT e IO a -> ExceptT e IO (Maybe Double, a)
241+
withRequestPhase label = withOTel label . withTiming conf
242+
233243
withTiming :: AppConfig -> ExceptT e IO a -> ExceptT e IO (Maybe Double, a)
234244
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
235245
then do

0 commit comments

Comments
 (0)