feat(logging): JSON formatter so structured log fields survive
LoggingMiddleware attaches per-request structured fields to every log
record via extra={} — path, status code, latency, and the
authenticated identity (user_sub / user_email / user_provider). None
of it reached the logs: src/main.py configured logging with
basicConfig and a fixed format string ("%(asctime)s - %(name)s -
%(levelname)s - %(message)s"), and the stdlib formatter renders only
the fields named in that string. Every extra was silently dropped, so
CloudWatch / any log sink saw nothing but "Response: 200 (0.041s)" —
no path, no status, no user.
This adds src/utils/logging_config.py with:
- JsonFormatter: emits each record as single-line JSON including any
extra={} fields, exc_info, and stack_info. Uses default=str so a
non-serialisable extra (UUID, datetime) degrades instead of raising
inside the log call.
- configure_logging(level, json_format): installs one root handler
with the chosen formatter.
src/main.py now calls configure_logging with json_format gated on
environment == "production". Production emits JSON (queryable in
CloudWatch Logs Insights, Loki, etc.); local dev keeps the
human-readable text format. The noisy-third-party-logger silencing is
unchanged.
No new dependency — JsonFormatter is ~40 lines of stdlib. The
middleware was already doing the work; this stops the formatter from
throwing it away.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>