Files
chicken_babies_site/app/logging_config.py
Phillip Tarrant 78dd1ac243 feat: phase 0 foundation — FastAPI scaffold + /healthz
Scaffold app/ package, pinned requirements.txt, multi-stage Dockerfile,
docker-compose.yml, and .env.example. Add typed pydantic-settings loader
with full env contract and a production validator that refuses the
dev-sentinel SECRET_KEY. Wire structlog with an APP_ENV-driven renderer
(console in dev, JSON in prod). Ship a minimal unauthenticated /healthz
returning {status, version, commit_sha} with commit SHA fed through a
GIT_COMMIT_SHA build arg.

Also mark Phase 0 complete in docs/ROADMAP.md.
2026-04-21 14:44:41 -05:00

73 lines
2.6 KiB
Python

"""Structlog initialization.
Single entry point :func:`configure_logging` sets up structlog with an
``APP_ENV``-driven renderer: a pretty console renderer during development
and JSON-lines output in production so logs plug straight into any log
aggregator. Must be called exactly once at app startup, before any
module obtains a logger via ``structlog.get_logger()``.
"""
from __future__ import annotations
import logging
import sys
from typing import Any
import structlog
def configure_logging(app_env: str) -> None:
"""Configure structlog + stdlib logging for the application.
Parameters
----------
app_env:
The resolved ``APP_ENV`` value. ``"development"`` selects a
colorized console renderer; anything else (including
``"production"``) selects the JSON renderer so logs are
machine-parseable.
Notes
-----
This function is idempotent within a process: structlog accepts
repeated calls to :func:`structlog.configure`. It deliberately keeps
the setup small for Phase 0 — a proper ``ProcessorFormatter`` bridge
between stdlib and structlog can be layered in later without touching
call sites.
"""
# Route stdlib logging through a root handler writing to stdout. Our
# own loggers go through structlog's pipeline; third-party libraries
# that use the stdlib logger will at least surface at INFO.
logging.basicConfig(
level=logging.INFO,
stream=sys.stdout,
format="%(message)s",
)
# Processors shared across environments. Order matters: contextvars
# first so bound context is present for every later step; exception
# info extraction before rendering so tracebacks serialize cleanly.
shared_processors: list[Any] = [
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
]
# Environment-specific final renderer. Dev gets human-friendly
# colorized output; everywhere else emits JSON lines.
if app_env == "development":
final_renderer: Any = structlog.dev.ConsoleRenderer(colors=True)
else:
final_renderer = structlog.processors.JSONRenderer()
structlog.configure(
processors=[*shared_processors, final_renderer],
# PrintLoggerFactory writes to stdout without requiring a stdlib
# logger bridge; sufficient for Phase 0.
logger_factory=structlog.PrintLoggerFactory(),
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
cache_logger_on_first_use=True,
)