"""Structured logging with correlation ids (structlog → JSON lines or console).""" from __future__ import annotations import contextvars import logging import sys import uuid from typing import Any import structlog correlation_id: contextvars.ContextVar[str] = contextvars.ContextVar("correlation_id", default="-") SECRET_KEYS = {"api_key", "api_secret", "secret", "password", "token", "api-sign", "api-key", "authorization"} def _redact(_, __, event_dict): # noqa: ANN001 for k in list(event_dict): if k.lower() in SECRET_KEYS: event_dict[k] = "***" return event_dict def _add_correlation(_, __, event_dict): # noqa: ANN001 event_dict["cid"] = correlation_id.get() return event_dict def new_correlation_id() -> str: cid = uuid.uuid4().hex[:12] correlation_id.set(cid) return cid def configure_logging(level: str = "INFO", json_output: bool = True) -> None: logging.basicConfig(level=level.upper(), stream=sys.stdout, format="%(message)s") for noisy in ("httpx", "httpcore", "websockets", "uvicorn.access"): logging.getLogger(noisy).setLevel(logging.WARNING) processors: list[Any] = [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso", utc=True), _add_correlation, _redact, structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, ] renderer = structlog.processors.JSONRenderer() if json_output else structlog.dev.ConsoleRenderer() structlog.configure( processors=[*processors, renderer], wrapper_class=structlog.make_filtering_bound_logger(logging.getLevelName(level.upper())), logger_factory=structlog.PrintLoggerFactory(sys.stdout), cache_logger_on_first_use=False, ) def get_logger(name: str) -> structlog.stdlib.BoundLogger: return structlog.get_logger(name)