Problem
The relay has basic pool utilization gauges (buzz_db_pool_size/idle/active/max) and replica fence metrics, but several critical observability gaps make it difficult to diagnose performance issues:
Missing metrics
-
No query-level latency histograms: #[datastore_span] creates tracing spans but does not emit Prometheus histograms. Span-based analysis requires a tracing backend (Jaeger/OTEL); the relay's Prometheus metrics endpoint has no per-operation latency data. You cannot answer "what is the p99 latency of query_events?" from Prometheus alone.
-
No pool acquire wait time: the time between requesting a pool connection and receiving one is invisible. When the pool is saturated, all query latencies spike equally — but the root cause (pool wait vs. query time) is indistinguishable.
-
No advisory lock wait time: advisory lock calls block inside a transaction, but the lock wait is lumped into the enclosing span's duration. There is no way to tell whether a 5-second replace_addressable_event was slow due to query execution or lock contention.
-
No transaction duration metric: long-running transactions (replaceable events with large insert + soft-delete + mention insertion) hold connections and potentially block other advisory lock holders. No metric tracks transaction duration.
-
No connection churn metric: pool_size and pool_idle are gauges sampled every 10s. A connection that is acquired and returned 100 times between samples is invisible. A connection-checkout counter would reveal churn rate.
-
No per-kind event write latency: all event writes go through insert_event or replace_*_event, but there is no metric broken down by event kind. A single problematic kind (e.g., large NIP-33 parametrized replaceable events) can dominate write latency without being identifiable.
-
No pg_stat_statements integration: the relay does not query pg_stat_statements (which is typically enabled on managed Postgres like Aurora). This extension provides per-query mean/max latency, call count, and rows returned — exactly the data needed for slow-query diagnosis. A periodic scrape emitted as Prometheus metrics would close the observability gap.
Missing diagnostic endpoints
-
No /debug/db endpoint: there is no admin endpoint that returns current pool state, active queries, advisory lock holders, or connection ages. Diagnosis requires direct Postgres access (pg_stat_activity), which may not be available to all operators.
-
No /debug/locks endpoint: advisory lock contention is the primary deploy-time risk, but there is no way to inspect it without pg_locks access.
Proposed changes
- Add
buzz_db_operation_duration_seconds histogram with an operation label, emitted from #[datastore_span].
- Add
buzz_db_pool_acquire_wait_seconds histogram (writer and reader pools separately).
- Add
buzz_db_advisory_lock_wait_seconds histogram with a lock_type label.
- Add
buzz_db_transaction_duration_seconds histogram.
- Add
buzz_db_pool_checkouts_total counter.
- Add
buzz_db_event_write_duration_seconds histogram with a kind label for the hot-path event write operations.
- Consider periodic
pg_stat_statements scraping if the extension is available.
- Add an admin-only
/debug/db endpoint behind authentication.
Priority
High — without these metrics, diagnosing deploy-time performance issues requires direct Postgres access and real-time correlation of logs, which is impractical during an incident.
🤖 AI review update (2026-08-23)
Merge the core instrumentation work with #26/#27/#28 under a cardinality budget. Operation, pool_role, and fixed lock_type labels are safe; raw event kind/query ID labels need bounding or coarse families. Prefer external Postgres monitoring or bounded top-N diagnostics for pg_stat_statements, whose identifiers are not stable indefinitely and whose query text can be sensitive. Keep diagnostics off the public relay surface; use a separately authenticated/admin-only mechanism with redaction and query/time limits.
Problem
The relay has basic pool utilization gauges (
buzz_db_pool_size/idle/active/max) and replica fence metrics, but several critical observability gaps make it difficult to diagnose performance issues:Missing metrics
No query-level latency histograms:
#[datastore_span]creates tracing spans but does not emit Prometheus histograms. Span-based analysis requires a tracing backend (Jaeger/OTEL); the relay's Prometheus metrics endpoint has no per-operation latency data. You cannot answer "what is the p99 latency ofquery_events?" from Prometheus alone.No pool acquire wait time: the time between requesting a pool connection and receiving one is invisible. When the pool is saturated, all query latencies spike equally — but the root cause (pool wait vs. query time) is indistinguishable.
No advisory lock wait time: advisory lock calls block inside a transaction, but the lock wait is lumped into the enclosing span's duration. There is no way to tell whether a 5-second
replace_addressable_eventwas slow due to query execution or lock contention.No transaction duration metric: long-running transactions (replaceable events with large insert + soft-delete + mention insertion) hold connections and potentially block other advisory lock holders. No metric tracks transaction duration.
No connection churn metric:
pool_sizeandpool_idleare gauges sampled every 10s. A connection that is acquired and returned 100 times between samples is invisible. A connection-checkout counter would reveal churn rate.No per-kind event write latency: all event writes go through
insert_eventorreplace_*_event, but there is no metric broken down by event kind. A single problematic kind (e.g., large NIP-33 parametrized replaceable events) can dominate write latency without being identifiable.No
pg_stat_statementsintegration: the relay does not querypg_stat_statements(which is typically enabled on managed Postgres like Aurora). This extension provides per-query mean/max latency, call count, and rows returned — exactly the data needed for slow-query diagnosis. A periodic scrape emitted as Prometheus metrics would close the observability gap.Missing diagnostic endpoints
No
/debug/dbendpoint: there is no admin endpoint that returns current pool state, active queries, advisory lock holders, or connection ages. Diagnosis requires direct Postgres access (pg_stat_activity), which may not be available to all operators.No
/debug/locksendpoint: advisory lock contention is the primary deploy-time risk, but there is no way to inspect it withoutpg_locksaccess.Proposed changes
buzz_db_operation_duration_secondshistogram with anoperationlabel, emitted from#[datastore_span].buzz_db_pool_acquire_wait_secondshistogram (writer and reader pools separately).buzz_db_advisory_lock_wait_secondshistogram with alock_typelabel.buzz_db_transaction_duration_secondshistogram.buzz_db_pool_checkouts_totalcounter.buzz_db_event_write_duration_secondshistogram with akindlabel for the hot-path event write operations.pg_stat_statementsscraping if the extension is available./debug/dbendpoint behind authentication.Priority
High — without these metrics, diagnosing deploy-time performance issues requires direct Postgres access and real-time correlation of logs, which is impractical during an incident.
🤖 AI review update (2026-08-23)
Merge the core instrumentation work with #26/#27/#28 under a cardinality budget. Operation, pool_role, and fixed lock_type labels are safe; raw event kind/query ID labels need bounding or coarse families. Prefer external Postgres monitoring or bounded top-N diagnostics for pg_stat_statements, whose identifiers are not stable indefinitely and whose query text can be sensitive. Keep diagnostics off the public relay surface; use a separately authenticated/admin-only mechanism with redaction and query/time limits.