"""structlog setup for agent runs. Separate from the service-level logging in marketing_intelligence/config.py which uses audience_common.logger. This module is only called by the agent entrypoints (CLI / api campaign handler) where run_id context is needed. LOG_FORMAT=console → human-readable (local dev) LOG_FORMAT=json → JSON to stdout (CloudWatch / ECS) """ from __future__ import annotations import logging import sys from typing import Any import structlog from marketing_intelligence.core.config import settings def setup_logging(run_id: str | None = None) -> None: log_level = getattr(logging, settings.log_level.upper(), logging.INFO) shared_processors: list[Any] = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt="iso", utc=True), ] renderer: structlog.types.Processor if settings.log_format == "json": renderer = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer(colors=sys.stdout.isatty()) structlog.configure( processors=shared_processors + [ structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) formatter = structlog.stdlib.ProcessorFormatter( processor=renderer, foreign_pre_chain=shared_processors, ) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) root.setLevel(log_level) for noisy in ( "httpx", "httpcore", "asyncio", "uvicorn.access", "camoufox", "fastmcp", "kafka", "mcp.server", ): logging.getLogger(noisy).setLevel(logging.WARNING) if run_id: structlog.contextvars.bind_contextvars(run_id=run_id)