OpenTelemetry tracing extension for PostgreSQL. Extracts W3C traceparent from SQL comments and exports query lifecycle spans via OTLP/HTTP.
Backend API PostgreSQL OTEL Collector
│ SQL + traceparent │ │
│ ──────────────────────> │ planner span │
│ │ query execution span │
│ │ executor run span │
│ │ ────────┬───────────> │
│ │ Shared │ Memory │
│ │ Queue │ │
│ │ ────────┘ │
- Your app injects
traceparentinto SQL comments (sqlcommenter) - The extension intercepts query hooks (
planner,ExecutorStart,ExecutorRun,ExecutorEnd) - Spans are batched in shared memory and exported asynchronously by a background worker
- You see the full trace in Jaeger/Tempo — from HTTP handler down to Postgres internals
git clone https://github.com/mstrYoda/pg_otel_tracer.git
cd pg_otel_tracer
docker compose up --buildThis starts Postgres 16 + OTEL Collector + Jaeger + Go demo app.
# Send a traced request
curl -X POST http://localhost:8080/users \
-d '{"name":"Alice","email":"alice@example.com"}'
# View traces
open http://localhost:16686First build compiles the Rust extension inside the Postgres image (5–15 min).
- Rust 1.70+
cargo install cargo-pgrx --version 0.11.2 --lockedcargo pgrx init- PostgreSQL 13–16 dev headers
cargo pgrx package --pg-config $(which pg_config)
# Copy artifacts to Postgres dirs
PG_CONFIG=$(which pg_config)
cp target/release/pg_otel_tracer-pg16/usr/share/postgresql/16/extension/* \
"$($PG_CONFIG --sharedir)/extension/"
cp target/release/pg_otel_tracer-pg16/usr/lib/postgresql/16/lib/* \
"$($PG_CONFIG --pkglibdir)/"Add to postgresql.conf:
shared_preload_libraries = 'pg_otel_tracer'
Restart Postgres, then:
CREATE EXTENSION pg_otel_tracer;| Setting | How to Set | Default | Description |
|---|---|---|---|
| Enable/disable | SELECT pg_otel_tracer_set_enabled(false) |
true |
Emergency off switch — no restart needed |
| Sampling rate | SELECT pg_otel_tracer_set_sample_rate(0.1) |
1.0 |
Fraction of queries to trace (0.0–1.0) |
| OTLP endpoint | OTEL_EXPORTER_OTLP_ENDPOINT env var |
http://localhost:4318/v1/traces |
Where spans are sent |
| Boot-time enable | PG_OTEL_TRACER_ENABLED env var |
true |
Initial state on server startup |
| Boot-time sample rate | PG_OTEL_TRACER_SAMPLE_RATE env var |
1.0 |
Initial sampling rate |
-- Start with 1% sampling to measure overhead
SELECT pg_otel_tracer_set_sample_rate(0.01);
-- Monitor queue health
SELECT * FROM pg_otel_tracer_status();
-- metric | value
-- --------------+--------
-- version | 0.1.0
-- enabled | true
-- sample_rate | 0.0100
-- queue_size | 3
-- queue_dropped| 0
-- If queue_dropped > 0, the collector can't keep up — reduce sample_rate
-- or scale the collector. If problems persist, disable immediately:
SELECT pg_otel_tracer_set_enabled(false);The extension uses four Postgres hooks to trace query lifecycle:
| Hook | Span | What It Measures |
|---|---|---|
planner_hook |
planner |
Query optimization |
ExecutorStart_hook |
query execution |
Execution start |
ExecutorRun_hook |
executor run |
Data retrieval + wait events |
ExecutorEnd_hook |
— | Finalizes spans + flushes to queue |
Thread-local buffers store spans per backend. Shared-memory ring buffer (1024 slots, 8KB each) passes data to the background worker. The BGW exports via OTLP/HTTP JSON every 500ms.
Key design decisions:
- Drop on overflow — never block the database for telemetry
- Bounded buffer — max 64 spans per backend before forced flush
- Spinlock + yield — protects the queue without LWLock ABI fragility
- catch_unwind — panics in the BGW are trapped, not propagated
- Exponential backoff retry — 3 attempts on export failure
The extension reads W3C traceparent from SQL comments:
SELECT * FROM users
/*traceparent='00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'*/;The included Go demo does this automatically. For other languages, append the trace context as a trailing comment before sending the query to Postgres.
src/
lib.rs # _PG_init, SQL functions
hooks.rs # Planner + Executor hooks
parser.rs # Traceparent regex extraction
span.rs # RawSpan types + ID generation
shared.rs # Shmem ring buffer + spinlock
bgw.rs # Background worker (drain → export)
exporter.rs # OTLP/HTTP JSON payload
wait_events.rs # MyProc wait event sampling
config.rs # Runtime enable/sampling toggles
demo-go/ # End-to-end Go/GORM demo
# macOS → Linux
brew install FiloSottile/musl-cross/musl-cross
# Edit .cargo/config.toml to set linker
cargo build --release --target x86_64-unknown-linux-gnu# Unit tests
cargo pgrx test pg16
# Local Postgres instance
cargo pgrx run pg16docker compose down # stop
docker compose down -v # stop + wipe data| Symptom | Cause | Fix |
|---|---|---|
shared_preload_libraries error |
Extension not preloaded | Add to postgresql.conf, restart |
| No spans in Jaeger | Collector unreachable | Check OTEL_EXPORTER_OTLP_ENDPOINT, verify collector health |
queue_dropped increasing |
Collector can't keep up | Reduce sample_rate or scale collector |
| High CPU | BGW restart loop | Check collector endpoint, verify network |
Areas for contribution:
- GUC variables (
pg_otel_tracer.enabledas native GUC) - gRPC OTLP exporter
- pg_stat_statements integration
- Support for Postgres 13–15
Apache-2.0