Observability contribs¶
dorm.contrib.querylog, dorm.contrib.querycount and the
assertNumQueries test helpers all listen on the pre_query /
post_query signals via the shared ScopedCollector primitive —
per-task isolation via ContextVar, so concurrent requests don't
bleed into each other's counters.
dorm.contrib.querylog¶
Captures every SQL statement inside a context-manager block. Useful for inspecting the query mix per request, asserting in tests that a code path doesn't regress on count / duration, and surfacing N+1 patterns by grouping captured statements by SQL template.
from dorm.contrib.querylog import QueryLog, QueryLogASGIMiddleware
with QueryLog() as log:
do_work()
for stats in log.summary():
print(stats.template, stats.count, stats.p95_ms)
# ASGI middleware — log lands on scope["dorm_querylog"]
app = QueryLogASGIMiddleware(your_asgi_app)
dorm.contrib.querylog.QueryLog
dataclass
¶
Context manager that captures every query in its scope.
Usage::
with QueryLog() as log:
do_work()
for record in log.records:
print(record.sql, record.elapsed_ms)
print(log.summary())
summary() -> list[TemplateStats]
¶
Group captured records by SQL template; return stats sorted by descending total time. p95 uses nearest-rank — fine for the ~tens of queries a typical request issues.
dorm.contrib.querylog.QueryRecord
dataclass
¶
dorm.contrib.querylog.TemplateStats
dataclass
¶
dorm.contrib.querylog.QueryLogASGIMiddleware
¶
Minimal ASGI middleware that wraps each request in a
:class:QueryLog and stashes the result on
scope["dorm_querylog"] so the downstream handler can read /
log it.
Provider-agnostic — works with FastAPI, Starlette, Quart, Sanic, anything that speaks ASGI 3.
dorm.contrib.querylog.query_log() -> Iterator[QueryLog]
¶
Function-style alias for :class:QueryLog.
dorm.contrib.querycount¶
Lightweight N+1 guard around a code block. Counts queries inside
the block via pre_query and emits a single WARNING if the count
crosses a threshold.
from dorm.contrib.querycount import query_count_guard
with query_count_guard(warn_above=20, label="GET /articles"):
return [article_dict(a) for a in Article.objects.all()]
warn_above falls back to settings.QUERY_COUNT_WARN when not
given; None (the default) leaves the guard inert (counts but
never warns).
dorm.contrib.querycount.query_count_guard(warn_above: int | None = None, *, label: str | None = None) -> Iterator['QueryCount']
¶
Count queries executed inside the block.
warn_above — emit a WARNING on exit if the count exceeds this
threshold. Falls back to settings.QUERY_COUNT_WARN when not
given; None means "count but never warn".
label — included in the warning to identify the call-site.
Yields a :class:QueryCount whose count attribute holds the
final total after the block exits. Tests that need an exact-count
assertion can read it directly without grepping the log.
dorm.contrib.querycount.QueryCount
¶
Live handle returned by :func:query_count_guard. count
is finalised on context exit; reading it inside the with block
returns 0.
dorm.test.assertNumQueries / assertMaxQueries¶
Django-parity test helpers. Context-manager forms assert exact /
≤-N query counts on exit; decorator forms wrap a sync or async
def test function. Nested scopes accumulate — an outer guard
counts queries fired inside any nested inner one (matches Django's
behaviour).
from dorm.test import assertNumQueries, assertMaxQueriesFactory
def test_list_view(transactional_db):
with assertNumQueries(3):
list(Article.objects.select_related("author")[:10])
@assertMaxQueriesFactory(5)
def test_dashboard(transactional_db):
render_dashboard()
dorm.test.assertNumQueries(num: int) -> Iterator[_NumQueriesContext]
¶
Assert that exactly num SQL statements fire inside the block.
Usage::
def test_list_view(transactional_db):
with assertNumQueries(3):
list(Article.objects.select_related("author")[:10])
The assertion runs on context exit; if the block raises, the original exception propagates and the count assertion is skipped (the failure is the more interesting signal).
dorm.test.assertMaxQueries(num: int) -> Iterator[_NumQueriesContext]
¶
Assert that at most num SQL statements fire inside the
block. Fewer is fine — useful when the upper bound is what
matters (defending against an N+1 regression) without pinning the
exact count.
dorm.test.assertNumQueriesFactory(num: int)
¶
Decorator factory equivalent of :func:assertNumQueries —
use as @assertNumQueriesFactory(N) on a test function (sync or
async def).
dorm.test.assertMaxQueriesFactory(num: int)
¶
Decorator factory equivalent of :func:assertMaxQueries.