Skip to content

feat: logging configuration, X-Request-ID, rate-limit headers, SecretStr - #16

Merged
JsNcAr merged 5 commits into
mainfrom
feat/logging-and-request-id
Oct 9, 2026
Merged

JsNcAr merged 5 commits into
mainfrom
feat/logging-and-request-id

Conversation

@JsNcAr

@JsNcAr JsNcAr commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Summary

Builds on #15 (merge that first; this PR then shows only its own commits). Closes the logging half of the Known Issues entry No logging configuration or observability, which becomes No metrics, tracing or error tracking.

Logging configuration (logging_config.py, new)

  • LOG_LEVEL (default INFO) and LOG_FORMAT (json lines in production by default, text elsewhere; config.log_format() is tested like docs_enabled()).
  • Standard-library pipeline from dictConfig: a QueueHandler on the root logger and a QueueListener thread writing to stderr, so a slow destination never blocks the event loop. Python 3.12+ builds the listener from the config; the lifespan runs it as a context manager (Python 3.14) so it starts before the first line and drains on shutdown.
  • The queue handler subclass keeps the record intact (the base prepare drops exc_info for pickling, which a same-process listener does not need) and stamps the request id on the task that logged.
  • uvicorn's own loggers keep their handlers and do not propagate, so they are untouched.

X-Request-ID (request_id.py, new)

  • Pure ASGI middleware, outermost: every response carries the header, 413s and CORS preflights included; expose_headers lets a browser client read it.
  • A supplied id is kept when it is 1 to 128 characters of [A-Za-z0-9._-], otherwise a UUIDv7 is generated. The id is in a context variable for the duration of the request and appears on every log line of that request.

X-RateLimit headers

  • Limiter(headers_enabled=True): POST /token and POST /api/users/ answer with X-RateLimit-Limit, -Remaining and -Reset (Unix time). slowapi writes them onto a response: Response parameter when the route returns a model, hence the parameter on both routes (verified in slowapi 0.1.10). The custom 429 handler and its Retry-After are unchanged.

SecretStr

  • API_KEY and JWT_SECRET_KEY are pydantic SecretStr values; repr is **********. Three use sites call get_secret_value().

Docs: docs/setup.md (table rows, a Logging section), docs/api.md (request id and rate-limit paragraphs), .env.example, AGENTS.md (Logging rule, structure), CHANGELOG.md.

Verification

  • New pure tests: tests/test_logging_config.py (formatter fields and exception text, queue handler stamping, dictConfig shape, listener start/stop and delivery with the id), tests/test_request_id.py (generated UUIDv7, echo, replacement of bad ids, contextvar reset after a failing route), test_limiter.py (budget headers on a limited route of a throwaway app), test_config.py (log_format, secret reprs). One database test added in tests/test_rate_limit.py.
  • Locally on Python 3.14.7: black --check, flake8, mypy zero errors, 62 pure tests passed with --noconftest; the pipeline smoke-run in both formats shows the id and the traceback in the right field; the header is present on a preflight and on a 413.
  • The last commit fixes two assertions that parsed the reset header as an integer (it is a Unix time with a fraction); the earlier commits were pushed before that was caught.

🤖 Generated with Claude Code

BrahiamSSS and others added 5 commits October 9, 2026 09:16
Replace the 16 Optional[X] annotations with X | None, declare the response
envelope as a PEP 695 generic (class SuccessResponse[T](BaseModel), which
pydantic 2.13 and mypy 2.4 handle), and move the eight default-value
dependency parameters to Annotated through two aliases in dependencies.py:
SessionDep and CurrentUserDep. Body(...) on the registration body is dropped;
a single pydantic-model parameter is the body already.

No behaviour change. The OpenAPI document before and after differs only in
the envelope example's id, which was still an integer from before UUIDv7 ids.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Configure logging once, at startup, from the lifespan: LOG_LEVEL and
LOG_FORMAT (json lines in production by default, text elsewhere) drive a
dictConfig with a QueueHandler on the root logger and a QueueListener thread
writing to stderr, so a slow destination never blocks the event loop. The
listener runs as a context manager (Python 3.14) for the app's lifetime and
drains on shutdown. uvicorn's loggers are untouched.

Every response carries X-Request-ID: a supplied id is kept when it is 1 to
128 characters of [A-Za-z0-9._-], else a UUIDv7 is generated. The id lives in
a context variable set by a pure ASGI middleware (outermost, so 413s and CORS
preflights carry it too) and is stamped on every log record as it enters the
queue, on the task that logged.

Known Issues: the logging entry becomes "No metrics, tracing or error
tracking", keeping only what is still missing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Limiter(headers_enabled=True): POST /token and POST /api/users/ answer with
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so a client
can pace itself before the 429. slowapi writes them onto the route's
`response: Response` parameter when the route returns a model, hence the
parameter on both routes. The custom 429 handler and its Retry-After are
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
API_KEY and JWT_SECRET_KEY are pydantic SecretStr values: their repr is
'**********', so a traceback, a debugger dump or a log line that reaches a
config value never shows them. The three use sites call get_secret_value().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
slowapi reports the reset as a Unix timestamp with a fraction; the two new
assertions parsed it as an integer and failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@JsNcAr
JsNcAr merged commit 2b0af67 into main Oct 9, 2026
2 checks passed
@JsNcAr
JsNcAr deleted the feat/logging-and-request-id branch October 9, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants