Operations¶
Reference
Every option on this page, with types and defaults: throttle storage on the
tenancy, caching and throttling reference, the request log on the
middleware reference, Sensitive on the
permissions reference, and Redis throttle storage on the
contrib reference.
Running an API in production: exact rate limits under load, one log line per request for your aggregator, and a way to stop secrets from leaking into logs, error bodies and exports.
Atomic throttle storage¶
Throttles count hits through a ThrottleStorage, not the cache directly:
from ninja_devx.http.throttling import ScopedRateThrottle
from ninja_devx.contrib.redis_throttle import RedisThrottleStorage
redis_throttle_storage = RedisThrottleStorage(url="redis://cache:6379/1")
NINJA_DEVX = {"THROTTLE_STORAGE": "myapp.throttling.redis_throttle_storage"}
class UploadController(Controller):
@post("/", throttle=[ScopedRateThrottle("uploads", storage=redis_throttle_storage)])
def upload(self, request, file: UploadedFile) -> Upload: ...
- The default,
CacheThrottleStorage, counts withcache.add+cache.incr, which is atomic on Redis and Memcached but not everywhere (a crash between the two calls loses one hit). ninja_devx.contrib.redis_throttle.RedisThrottleStorage(pip install ninja-devx[redis]) counts withINCRand a conditionalEXPIRE(NX, so only the window's first hit sets the TTL) in oneMULTI/EXECtransaction: an exact fixed window and aRetry-Afterthat cannot drift from a race between the two calls.- Pick a storage per throttle with
storage=, or project-wide withNINJA_DEVX["THROTTLE_STORAGE"](an instance, or its import path); an explicitstorage=always wins. - Write your own by implementing
hit(key, window_seconds) -> (count, retry_after).
Request log¶
RequestLogPlugin logs one structured record per request and never the body:
from ninja_devx.plugins import install
from ninja_devx.http.requestlog import RequestLogPlugin
install(api, [RequestLogPlugin(naming="otel")])
- Fields:
request_id,method,path,status,duration_ms,user_id,tenant(the resolved tenant, orrequest.tenant),query_count(queries on the default database connection),operation("Controller.method"), andapi_key_prefixwhenninja_devx.contrib.apikeysauthenticated the request. naming="otel"(the default) spellsmethod,path,statusanduser_idwith an OpenTelemetry semantic convention name (http.request.method,url.path,http.response.status_code,user.id);naming="flat"keeps the plain names.- The record goes through stdlib
loggingwith the fields inextra; configureninja_devx.http.requestlog.JSONFormatterinLOGGINGto emit it as one JSON object. - When
structlogis installed (pip install ninja-devx[structlog]), the identity fields are also bound withstructlog.contextvars.bind_contextvarsas the request starts and cleared once the record is logged, so the application's ownstructlogcalls carry them too. RequestLogMiddlewareis the middleware alone, foruse_middleware()orControllerOptions(middleware=[...])without the bundledRequestIDMiddleware.
Sensitive fields¶
Sensitive marks a schema field that must never be echoed back unmasked:
from typing import Annotated
from ninja_devx.serialization.privacy import Sensitive
class UserOut(Schema):
id: int
email: str
api_key: Annotated[str, Sensitive()]
mask(value)returns"***"(NonestaysNone; a list is masked element-wise).redact_payload(schema, data)returns a copy ofdatawith everySensitivefield ofschemamasked, matched by field name or alias.- The CSV/JSONL export (
ExportMixin) masksSensitiveoutput fields unless the controller setsexport_sensitive = True. ninja_devx.http.errors.mask_validation_input(errors, schema)masks theinputa validation error body would otherwise echo back; Ninja's own request validation already drops it, so this matters for apydantic.ValidationErroryou map and render yourself.ninja_devx.contrib.audit.privacy.AuditPrivacy(schema=...)treats a schema'sSensitivefields the same as its ownredactnames.