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.
73 lines
2.6 KiB
Python
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,
|
|
)
|