mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Add structured logging with per-request context (#604)
## Summary
New `enable_logging=True` parameter on `responder.API()` that provides
structured, request-scoped logging using stdlib `logging` and
`contextvars`.
### What it does
- **`api.log`** — always available on every API instance. Works as a
plain logger by default; gains per-request context enrichment with
`enable_logging=True`
- **Per-request context** — every log message automatically includes
request ID, HTTP method, path, and client IP
- **Access logging** — logs every request with timing: `GET /path → 200
(1.2ms)`
- **Request ID** — generates or forwards `X-Request-ID` headers
(supersedes `request_id=True` when both are set)
- **stdlib logging** — works with any existing handler, formatter, or
log aggregator
### Usage
```python
# api.log always works — no setup required
api = responder.API()
api.log.info("starting up") # plain logger, no context
# With enable_logging=True, log messages get request context automatically
api = responder.API(enable_logging=True)
@api.route("/")
def index(req, resp):
api.log.info("handling request")
# => 2026-03-24 12:00:00 [INFO] responder.app — handling request [GET /] [req:abc123] [client:127.0.0.1]
```
For additional loggers in helper modules:
```python
from responder.ext.logging import get_logger
logger = get_logger("myapp.db")
```
### Architecture
- `responder/ext/logging.py` — self-contained module with:
- `LoggingMiddleware` — pure ASGI middleware that sets contextvars and
logs access
- `RequestContextFilter` — logging filter that injects context into
records
- `RequestContext` — read-only access to current request metadata
- `get_logger()` / `setup_logging()` — convenience functions
- `api.log` — always a valid logger; context-aware when
`enable_logging=True`, plain stdlib logger otherwise
- Wired into `API.__init__` via the `enable_logging` parameter
### Files
- `responder/ext/logging.py` — new module
- `responder/api.py` — added `enable_logging` parameter and `api.log`
- `tests/test_logging.py` — 9 tests
- `docs/source/tour.rst` — new Structured Logging section
- `docs/source/index.rst` — added to feature list
## Test plan
- [x] 9 logging tests pass
- [x] Full suite: 208 passed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,7 @@ One ``pip install``, batteries included:
|
||||
- Sync and async views — ``async`` is always optional.
|
||||
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
|
||||
- Built-in rate limiting with ``X-RateLimit`` headers.
|
||||
- Structured logging with per-request context.
|
||||
- Content negotiation: JSON, YAML, and MessagePack.
|
||||
- A pleasant API with a single import statement.
|
||||
- OpenAPI schema generation with Swagger UI.
|
||||
|
||||
@@ -596,6 +596,60 @@ can pace themselves.
|
||||
The rate limiter is per-client, keyed by IP address.
|
||||
|
||||
|
||||
Structured Logging
|
||||
------------------
|
||||
|
||||
Production applications need structured, searchable logs. Responder
|
||||
includes built-in logging that automatically attaches request context
|
||||
— request ID, HTTP method, path, and client IP — to every log message
|
||||
emitted during request handling::
|
||||
|
||||
api = responder.API(enable_logging=True)
|
||||
|
||||
This gives you:
|
||||
|
||||
- **Access logging** with timing for every request::
|
||||
|
||||
2026-03-24 12:00:00 [INFO] responder.access — GET /users → 200 (1.2ms)
|
||||
|
||||
- **A logger on the API instance** — use ``api.log`` anywhere in
|
||||
your routes. Request context (ID, method, path, client IP) is
|
||||
attached automatically::
|
||||
|
||||
@api.route("/users/{user_id:int}")
|
||||
def get_user(req, resp, *, user_id):
|
||||
api.log.info("fetching user %d", user_id)
|
||||
# => [INFO] responder.app -- fetching user 42 [GET /users/42] [req:a1b2c3] [client:10.0.0.1]
|
||||
resp.media = {"id": user_id}
|
||||
|
||||
- **Request IDs** generated automatically (or forwarded from the
|
||||
``X-Request-ID`` header) and included in responses.
|
||||
|
||||
The logging uses Python's standard ``logging`` module, so it works with
|
||||
any handler — files, syslog, JSON formatters, Datadog, Sentry, whatever
|
||||
you already use.
|
||||
|
||||
For additional loggers (e.g. in helper modules), use ``get_logger``::
|
||||
|
||||
from responder.ext.logging import get_logger
|
||||
logger = get_logger("myapp.db")
|
||||
|
||||
You can also access the current request context directly::
|
||||
|
||||
from responder.ext.logging import RequestContext
|
||||
|
||||
@api.route("/debug")
|
||||
def debug(req, resp):
|
||||
resp.media = {
|
||||
"request_id": RequestContext.get_request_id(),
|
||||
"client_ip": RequestContext.get_client_ip(),
|
||||
}
|
||||
|
||||
When ``enable_logging=True`` is set, it supersedes ``request_id=True``
|
||||
— the logging middleware handles request IDs itself, so you don't get
|
||||
duplicate headers.
|
||||
|
||||
|
||||
Pydantic Validation
|
||||
-------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user