Group by tenant

Multi-tenant app, shared codebase. An alert fires. Whose customer just tripped it?

If your tenants are services in the same product, forum mode is now the recommended path: one bot, one supergroup, one topic per tenant. See the Forum mode guide. The recipe below remains valid when topics aren’t an option.

The problem

You can bolt a tenant_id argument onto every notify() and every log.warning() call site. You’ll miss some. The ones you miss are always the ones that fire in production. The alternative is one middleware that scopes the request, and every notify, @watch_slow, and crash report in that scope carries the tenant automatically.

The recipe

# middleware.py — FastAPI, but the pattern works anywhere
import snitchbot
from fastapi import FastAPI, Request

snitchbot.init("billing")
app = FastAPI()

@app.middleware("http")
async def scope_tenant(request: Request, call_next):
    with snitchbot.request_context(
        trace_id=request.headers.get("x-request-id"),
        tenant_id=request.state.tenant.id,
        region=request.state.tenant.region,
    ):
        return await call_next(request)

What you see

🟠 notify · billing · 156afe
Invoice generation failed
Details
  time     17:02:11 UTC
  pid      42
  caller   invoices.py:88 in generate()
Context
  trace_id   req-abc-123
  tenant_id  acme-corp
  region     eu-west-1

Notes

  • request_context rides on contextvars, so it crosses await and asyncio.create_task — but not threading.Thread. For thread pools, wrap with contextvars.copy_context().run(...).
  • Nested contexts merge. Inner extras override outer keys; trace_id inherits from the enclosing block if omitted.
  • Works identically for Flask (@app.before_request) and Litestar (ASGI middleware). See request_context() for the full signature.