Skip to content

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.