Repository navigation
feat: logging configuration, X-Request-ID, rate-limit headers, SecretStr - #16
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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(defaultINFO) andLOG_FORMAT(jsonlines in production by default,textelsewhere;config.log_format()is tested likedocs_enabled()).dictConfig: aQueueHandleron the root logger and aQueueListenerthread 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.preparedropsexc_infofor pickling, which a same-process listener does not need) and stamps the request id on the task that logged.X-Request-ID (
request_id.py, new)expose_headerslets a browser client read it.[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 /tokenandPOST /api/users/answer withX-RateLimit-Limit,-Remainingand-Reset(Unix time). slowapi writes them onto aresponse: Responseparameter when the route returns a model, hence the parameter on both routes (verified in slowapi 0.1.10). The custom 429 handler and itsRetry-Afterare unchanged.SecretStr
API_KEYandJWT_SECRET_KEYare pydanticSecretStrvalues; repr is**********. Three use sites callget_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
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 intests/test_rate_limit.py.black --check,flake8,mypyzero 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.🤖 Generated with Claude Code