Skip to content

Repository files navigation

pg_otel_tracer

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 │ │
│ │ ────────┘ │

What It Does

  1. Your app injects traceparent into SQL comments (sqlcommenter)
  2. The extension intercepts query hooks (planner, ExecutorStart, ExecutorRun, ExecutorEnd)
  3. Spans are batched in shared memory and exported asynchronously by a background worker
  4. You see the full trace in Jaeger/Tempo — from HTTP handler down to Postgres internals

Quick Start (Docker)

git clone https://github.com/mstrYoda/pg_otel_tracer.git
cd pg_otel_tracer
docker compose up --build

This 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:16686

First build compiles the Rust extension inside the Postgres image (5–15 min).

Manual Installation

Prerequisites

  • Rust 1.70+
  • cargo install cargo-pgrx --version 0.11.2 --locked
  • cargo pgrx init
  • PostgreSQL 13–16 dev headers

Build & Install

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)/"

Configure PostgreSQL

Add to postgresql.conf:

shared_preload_libraries = 'pg_otel_tracer'

Restart Postgres, then:

CREATE EXTENSION pg_otel_tracer;

Configuration

SettingHow to SetDefaultDescription
Enable/disableSELECT pg_otel_tracer_set_enabled(false)trueEmergency off switch — no restart needed
Sampling rateSELECT pg_otel_tracer_set_sample_rate(0.1)1.0Fraction of queries to trace (0.0–1.0)
OTLP endpointOTEL_EXPORTER_OTLP_ENDPOINT env varhttp://localhost:4318/v1/tracesWhere spans are sent
Boot-time enablePG_OTEL_TRACER_ENABLED env vartrueInitial state on server startup
Boot-time sample ratePG_OTEL_TRACER_SAMPLE_RATE env var1.0Initial sampling rate

Production Tuning Example

-- Start with 1% sampling to measure overheadSELECT pg_otel_tracer_set_sample_rate(0.01);
-- Monitor queue healthSELECT*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);

Architecture

The extension uses four Postgres hooks to trace query lifecycle:

HookSpanWhat It Measures
planner_hookplannerQuery optimization
ExecutorStart_hookquery executionExecution start
ExecutorRun_hookexecutor runData retrieval + wait events
ExecutorEnd_hookFinalizes 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

How Your App Injects Traceparent

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.

Project Structure

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

Cross-Compilation

# 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

Testing

# Unit tests
cargo pgrx test pg16
# Local Postgres instance
cargo pgrx run pg16

Stopping

docker compose down # stop
docker compose down -v # stop + wipe data

Troubleshooting

SymptomCauseFix
shared_preload_libraries errorExtension not preloadedAdd to postgresql.conf, restart
No spans in JaegerCollector unreachableCheck OTEL_EXPORTER_OTLP_ENDPOINT, verify collector health
queue_dropped increasingCollector can't keep upReduce sample_rate or scale collector
High CPUBGW restart loopCheck collector endpoint, verify network

Contributing

Areas for contribution:

  • GUC variables (pg_otel_tracer.enabled as native GUC)
  • gRPC OTLP exporter
  • pg_stat_statements integration
  • Support for Postgres 13–15

License

Apache-2.0

About

Rust based Postgres extension that adds query lifecycle events to OTEL traces.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages