Operations and metrics
The admin listener should remain on loopback or behind authenticated network policy. It intentionally exposes no credentials, but backend names, shard names, health, and capacity are operationally sensitive.
Sensitive-data contract
DBProxy metrics never use SQL text, prepared parameters, Redis keys or values, shard-key values, usernames, passwords, authentication payloads, client addresses, or operation IDs as labels. Labels are limited to configured backend and shard names, roles, TLS modes, query/operation classes, outcomes, and capability names. This keeps cardinality bounded and avoids placing application data in Prometheus.
Structured logs similarly omit SQL text, prepared values, Redis command
arguments and payloads, usernames, passwords, tokens, and shard-key values.
Redis command names can appear in validation errors, but their keys and values
do not. Logs can contain random operation IDs, client peer addresses, listener
endpoints, configured backend/shard/table names, configuration revisions, and
sanitized error descriptions. Treat those fields as operationally sensitive:
restrict log access and retention, and keep debug disabled unless it is
needed for a bounded investigation. Secret-bearing configuration structures
deliberately do not implement Debug, invalid TOML diagnostics do not echo
source lines or values, and credential-bearing URLs are rejected or sanitized
before errors are logged.
Backend and operating-system errors may still reveal infrastructure details such as hostnames, ports, certificate paths, or database object names. The admin endpoints expose more topology detail than metrics, including backend addresses. Neither logs nor observability endpoints should be published as a public service.
Use proxy.tls.mode = "required" for client traffic and
backends.tls.mode = "verify_identity" for database traffic in production.
Mount private keys as read-only secrets and restrict them to the DBProxy process
identity. The required backend mode provides encryption but deliberately skips
certificate authentication, so it should be treated as a migration mode.
Readiness
/healthz is process liveness. /readyz requires every configured shard primary
to have passed a health check and to remain below the configured consecutive
failure threshold. Replicas do not gate readiness because reads can fall back to
their shard primary.
When atomic MySQL fanout is enabled, readiness also requires
dbproxy_xa_in_doubt_transactions == 0. A post-decision participant failure
withdraws readiness, and restart recovery must resolve the durable decision
before the listener becomes available.
At startup, readiness is false until the first successful primary probes.
When PostgreSQL is enabled, readiness also requires every configured shard
primary to pass its configured probe. The default is TCP transport. A
per-backend readiness_probe authenticates with a dedicated role and executes
SELECT 1; its password can be supplied through environment expansion.
/postgres/backends reports the probe mode, effective readiness, TCP transport,
optional SQL readiness, and connection counters. SQL probes currently use
plaintext PostgreSQL unless readiness_probe.tls.enabled = true. TLS probes
verify both the trust chain and hostname and can use an explicit CA and client
identity.
When Redis is enabled, readiness also requires TCP transport health to every
configured shard primary. Sentinel mode also requires the most recent refresh
to reach its configured address quorum. Cluster mode instead requires a complete
current 16,384-slot snapshot. /redis/backends reports dynamic
Sentinel primary addresses plus transport and connection counters. Static
transport probes do not authenticate, issue PING, validate replication role,
or measure replica lag.
Prometheus groups
| Area | Metrics |
|---|---|
| Clients | dbproxy_active_clients, dbproxy_accepted_clients_total, dbproxy_rejected_clients_total, dbproxy_client_protocol_errors_total, dbproxy_client_io_timeouts_total, dbproxy_client_session_duration_seconds_* |
| TLS | dbproxy_frontend_tls_upgrade_attempts_total, dbproxy_frontend_plaintext_rejections_total, dbproxy_backend_info{tls_mode} |
| Queries | dbproxy_queries_total{kind}, dbproxy_queries_primary_total, dbproxy_queries_replica_total, dbproxy_query_successes_total, dbproxy_query_errors_total, dbproxy_query_timeouts_total, dbproxy_query_duration_seconds_*, dbproxy_sql_analysis_duration_seconds_*, dbproxy_sql_parse_fallbacks_total |
| Results | dbproxy_rows_returned_total, dbproxy_rows_affected_total, dbproxy_result_bytes_total, dbproxy_streaming_result_sets_total |
| Cache | dbproxy_cache_hits_total, dbproxy_cache_misses_total, dbproxy_cache_bypasses_total, dbproxy_cache_errors_total, fill/rejection/invalidation/local and distributed singleflight/refresh totals, and dbproxy_cache_bytes_served_total |
| Query registry | dbproxy_query_registry_entries, dbproxy_query_registry_evictions_total, and retained dbproxy_query_registry_calls{cache_status=...}; query IDs are never metric labels |
| Runtime config | dbproxy_config_epoch, dbproxy_config_reload_successes_total, dbproxy_config_reload_failures_total, dbproxy_config_poll_failures_total |
| Routing | dbproxy_replica_fallbacks_total, dbproxy_shard_routing_failures_total |
| Fanout | dbproxy_write_fanout_attempts_total, dbproxy_write_fanout_successes_total, dbproxy_write_fanout_failures_total, dbproxy_write_fanout_timeouts_total |
| MySQL XA | dbproxy_xa_transactions_total, dbproxy_xa_prepares_total, dbproxy_xa_commit_decisions_total, dbproxy_xa_commits_total, dbproxy_xa_rollbacks_total, dbproxy_xa_recovery_attempts_total, dbproxy_xa_recovery_failures_total, dbproxy_xa_in_doubt_transactions, dbproxy_xa_oldest_in_doubt_age_seconds |
| Prepared | dbproxy_prepared_statements_created_total, dbproxy_prepared_statement_executions_total, dbproxy_prepared_statements_closed_total, and parsed-routing-template hit/miss totals in dbproxy_prepared_routing_cache_{hits,misses}_total |
| Backend | dbproxy_backend_healthy, dbproxy_backend_consecutive_failures, dbproxy_backend_health_checks_total{outcome}, dbproxy_backend_last_health_check_timestamp_seconds, dbproxy_backend_last_success_timestamp_seconds, backend connection/query totals and duration sums |
| Pools | shared pool connection/idle/in-use/waiter/pressure metrics, dbproxy_backend_reserved_pool_enabled, reserved connection/idle/in-use/waiter metrics, and shared pool failure/reset/discard counters |
| SQL scatter/gather | dbproxy_scatter_plans_total, participating dbproxy_scatter_shards_total, failures, and merged row/byte totals for MySQL and PostgreSQL/TimescaleDB |
| Admin/shutdown | dbproxy_admin_active_connections, dbproxy_admin_requests_total, dbproxy_admin_rejected_connections_total, dbproxy_forced_shutdowns_total |
| Capabilities | dbproxy_capability_info{capability,status}; the same contracts are available from /capabilities |
| PostgreSQL clients | active/accepted/rejected/error totals, transparent wire bytes, terminated queries, primary/replica routes, transaction/session pins, read-only rejections, cancellations, and statement/shard-routing failures |
| PostgreSQL backends | health and SQL-probe metrics, connection totals/failures, and transaction-pool total/idle/in-use/waiter/pressure/checkouts/acquire-timeout/reset-failure metrics |
| Redis/Valkey clients | dbproxy_redis_active_clients, accepted/rejected/error totals, directional forwarded bytes, connection failures, replica fallbacks, shard routes, command totals, read-only rejections, cross-shard commands, routing failures, generated fanout subcommands, partial-write failures |
| Redis backends/topology | transport connection/health metrics; topology epoch; Sentinel refresh/failure/failover totals; Cluster refresh/failure, MOVED, ASK, and redirect-failure totals |
Per-backend metrics use bounded backend, shard, and role labels sourced
only from static configuration. SQL text, usernames, client addresses, and
database values are never used as labels.
Initial alerts
dbproxy_backend_healthy{role="primary"} == 0for longer than two health intervals: shard unavailable or not yet proven ready.increase(dbproxy_query_timeouts_total[5m]) > 0: backend saturation, lock contention, or network loss.dbproxy_backend_pool_waiters > 0sustained for several minutes: pool pressure; correlate with in-use connections and query latency.dbproxy_backend_reserved_pool_waiters > 0: transaction or pinned-session concurrency exceeded the isolated primary reserve; increase it only after checking the database's total connection budget.- Any increase in
dbproxy_write_fanout_failures_totalordbproxy_write_fanout_timeouts_total: possible cross-database divergence. - Any increase in
dbproxy_scatter_failures_total: a participating shard failed, exceeded the shared result budget, or returned incompatible metadata. dbproxy_xa_in_doubt_transactions > 0: a durable commit or abort decision is not resolved everywhere. Do not replay the application write; preserve the journal volume and restore participant connectivity.- A rising ratio of
dbproxy_replica_fallbacks_totaltodbproxy_queries_replica_total: unhealthy or undersized replicas. - Any increase in
dbproxy_forced_shutdowns_total: drain deadline too short or sessions stuck in long operations. - Sustained client I/O timeouts: slow or abandoned clients, packet loss, or an undersized downstream consumer.
dbproxy_postgres_backend_transport_healthy{role="primary"} == 0: the PostgreSQL/TimescaleDB primary cannot accept TCP connections.- Sustained
dbproxy_postgres_pool_waiters > 0or increasingdbproxy_postgres_pool_acquire_timeouts_total: a transaction pool is saturated or its backend is slow to connect. - Any increase in
dbproxy_postgres_pool_reset_failures_total: an unsafe connection was discarded because its state could not be reset. dbproxy_postgres_backend_healthy{role="primary"} == 0while transport is healthy: authenticated SQL readiness, credentials, or database selection is failing.- Rising
dbproxy_postgres_replica_fallbacks_total: unavailable replicas or an incorrect read-only topology. dbproxy_redis_backend_transport_healthy{role="primary"} == 0: the Redis primary cannot accept TCP connections.- Rising
dbproxy_redis_replica_fallbacks_total: unavailable Redis replicas or an incorrect read-only topology. - Any
dbproxy_redis_sentinel_refresh_failures_totalincrease: Sentinel endpoints did not agree at quorum; readiness is withdrawn. - Any
dbproxy_redis_cluster_refresh_failures_totalordbproxy_redis_cluster_redirect_failures_totalincrease: the native slot map or a redirection could not be proven safe. When MOVED responses rise without a successful refresh, check for planned resharding and unreachable announced nodes. - Any sustained increase in
dbproxy_postgres_shard_routing_failures_totalordbproxy_redis_shard_routing_failures_total: missing client initialization, invalid range values, or a configuration/client shard-map mismatch. - Any increase in
dbproxy_redis_partial_write_failures_total: a non-atomic cross-shard write may have changed only a subset of target shards; reconcile from the application's source of truth. - Rising
dbproxy_redis_command_routing_failures_total: unsupported command shapes, cross-shard operations disabled by policy, or incorrect key counts. - Rising
dbproxy_redis_read_only_rejections_total: clients are sending writes or non-whitelisted commands to a command-mode read-only listener. - Any increase in
dbproxy_cache_invalidation_failures_total: Redis/Valkey did not advance a committed table/shard generation; stale entries remain bounded by TTL, but investigate cache availability immediately.
Tune thresholds against normal traffic rather than alerting on raw cumulative counters.
Performance tuning
- Run
cargo bench --bench routingafter parser, classifier, or sharding changes. - Keep
write_fanout.max_concurrencybelow the connection capacity available on every target shard. It bounds concurrency per client operation, not globally. postgres.relay_buffer_bytesandredis.relay_buffer_bytesallocate two buffers per active transparent connection. The accepted range is 4 KiB through 1 MiB and the default is 32 KiB.- Use streaming MySQL results where bounded memory is more important than returning a clean error before any rows have been emitted.
- Treat a sustained increase in SQL-analysis duration as a signal to inspect statement complexity, deny-rule count, and CPU saturation.