Pool telemetry¶
Telemetry is opt-in and supports both pool engines. Install the optional client:
pip install 'django-pg-real-pool[telemetry]'
# With the SQLAlchemy-based backend:
pip install 'django-pg-real-pool[dj-db-conn-pool,telemetry]'
Then enable collection in Django settings:
Installing the extra alone does not enable telemetry. When disabled, the backend does
not import prometheus_client, register metrics, run callbacks or start a sampler.
When enabled without the extra, creating a database wrapper raises ImproperlyConfigured.
Metrics¶
| Configuration key | Default Prometheus name | Type | Meaning |
|---|---|---|---|
connection_count |
db_client_connection_count |
Gauge | Connections checked out through this backend (state="used") or available in the pool (state="idle") |
connection_size |
db_client_connection_size |
Gauge | Current size reported by the pool |
connection_max |
db_client_connection_max |
Gauge | Maximum capacity, including SQLAlchemy overflow; absent for an unlimited pool |
connection_pending_requests |
db_client_connection_pending_requests |
Gauge | Requests waiting in the pool queue; native backend only |
connection_requests |
db_client_connection_requests_total |
Counter | Completed checkout attempts, including failures |
connection_errors |
db_client_connection_errors_total |
Counter | Failed checkout attempts |
connection_timeouts |
db_client_connection_timeouts_total |
Counter | Checkout attempts that failed specifically with a pool timeout |
Count, maximum, pending requests and timeouts use names based on OpenTelemetry database pool conventions, with Prometheus formatting. Size, requests and errors are library extensions. These defaults are part of this library's API; upstream changes to experimental conventions do not silently rename them.
used counts borrowed connections, including connections held by transactions or cursors.
It does not measure SQL queries actively executing on the server. Accessing the underlying
pool directly bypasses checkout counters and used tracking.
For psycopg, size includes connections being prepared in background workers. Consequently,
used + idle can be smaller than size. SQLAlchemy size is checked-in plus checked-out
connections, not its configured persistent capacity. No SQL queries are issued to collect metrics.
All metrics describe local application pools, not PgBouncer's internal pool or PostgreSQL's
server-wide connection count. Before the first connection attempt there are no pool samples.
Used connections and checkout counters update synchronously on checkout/return using
cached metric handles, without reading pool statistics or invoking the labels provider.
Idle connections, size, capacity and queue depth are sampled approximately every INTERVAL
seconds (default 1.0). Any finite positive interval is supported; scheduling and collection
cost still limit actual precision. Samples are not an atomic snapshot across different metrics.
The daemon sampler sleeps until the next pool's deadline, wakes on registration/retirement,
and exits once no observed pools remain. New registrations restart it lazily.
Labels and configuration¶
Default labels:
pool_name: Django database alias, overridable withPOOL_NAME.server_address: configured endpoints ashost:port, for examplepg1.host:5432,pg2.host:3322. Hosts and ports are paired in their configured order. A single port applies to all hosts; empty ports default to5432. Whitespace is trimmed, and IPv6 addresses are bracketed ([::1]:5432).state:usedoridle, only onconnection_count.
server_address identifies the configured endpoint, not the physical server selected by
failover. For a PgBouncer endpoint it is the PgBouncer address. An empty HOST produces :5432 (or the configured port); connection parameters supplied through service files or environment variables
are not resolved. Passwords and connection strings are never added as labels.
DJANGO_PG_REAL_POOL_TELEMETRY = {
"ENABLED": True,
"INTERVAL": 1.0,
"METRIC_NAMES": {
"connection_count": "myapp_pool_connections",
"connection_max": "myapp_pool_capacity",
},
"LABELS": {"service": "billing"},
"LABEL_NAMES": ["cluster"],
"LABELS_PROVIDER": "myapp.telemetry.pool_labels",
}
# Overrides global options for this alias. Nested dictionaries are replaced.
DATABASES["default"]["TELEMETRY"] = {"POOL_NAME": "billing-pgbouncer"}
DATABASES["replica"]["TELEMETRY"] = {"ENABLED": False}
# myapp/telemetry.py
from django_pg_real_pool.telemetry import LabelContext
def pool_labels(context: LabelContext):
# Read your application's thread-safe, in-memory configuration here.
return {"cluster": "primary" if context.alias == "default" else "replica"}
The provider can be a callable or a dotted import path. LabelContext contains alias,
backend (native or dj_db_conn_pool), metric (the configuration key), server_address. It deliberately contains neither credentials nor database connections.
Declare dynamic label names in LABEL_NAMES; their values can change at runtime.
Missing dynamic values become empty strings. Undeclared names are rejected. Providers run
during initial registration and in the sampler thread: they must be fast, thread-safe, and independent
of request-local state. Do not execute database queries from a provider. Callback exceptions
are logged without failing database operations; subsequent updates retry the callback.
Dynamic value changes take effect on the next successful sampler refresh. The previous gauge series is set to zero and the current pool snapshot is published under the new values. Counters retain history under old values and count future events under the refreshed values. If refreshing event labels fails, existing handles remain usable; an initial callback failure suppresses event samples until a successful refresh (the internal used count is retained). Use bounded values (for example cluster or deployment role), not user IDs or request IDs: historical label combinations remain registered.
Configuration is read when wrappers are constructed. Names, registry, static labels and
enabling/disabling telemetry are startup settings; use the provider for runtime value changes.
Pool names must uniquely identify pools in a registry. Aliases sharing a metric name must
use the same label-name schema. Reserved labels cannot be supplied through LABELS or
LABEL_NAMES. Invalid configuration and metric registration conflicts raise
ImproperlyConfigured rather than silently dropping metrics.
Exposing metrics¶
By default metrics use prometheus_client.REGISTRY. If your application already exposes
that registry, pool metrics appear on its existing endpoint. The library does not start an
HTTP server or add a Django URL automatically.
A minimal single-process Django endpoint:
from django.http import HttpResponse
from django.urls import path
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
def metrics(request):
return HttpResponse(generate_latest(), content_type=CONTENT_TYPE_LATEST)
urlpatterns = [path("metrics", metrics)]
For a custom single-process registry, pass its object as REGISTRY in the telemetry
configuration and use the same registry in generate_latest(registry).
Gunicorn and other multiprocess workers¶
Metrics use ordinary Prometheus gauges and counters, including multiprocess_mode="livesum"
for gauges. Each worker samples its own pool; the multiprocess collector sums worker values.
For example, four workers each configured with capacity 10 report total capacity 40.
Follow the Prometheus Python multiprocess setup:
- Set
PROMETHEUS_MULTIPROC_DIRto an existing writable directory before starting Python. Use a fresh/empty directory for each application run; never clear it while workers are running. - Create the metrics endpoint's registry separately from the registry used for instrumentation:
from django.http import HttpResponse
from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest, multiprocess
def metrics(request):
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
return HttpResponse(generate_latest(registry), content_type=CONTENT_TYPE_LATEST)
- Notify the client when a worker exits. For Gunicorn:
# gunicorn.conf.py
from prometheus_client import multiprocess
def child_exit(server, worker):
multiprocess.mark_process_dead(worker.pid)
Other process managers, including Celery, need equivalent worker-exit cleanup. Without it, terminated workers' gauge values remain in the aggregate. Counters retain completed workers' history. Do not create/open database pools before forking; each worker must create its own pool. Custom registries do not provide isolation in Prometheus multiprocess mode.
Inherited telemetry observers are retired after fork; closing a wrapper inherited from the parent does not acquire its old telemetry locks or update its metrics. This does not make inherited database connections safe to use.