from __future__ import annotations import contextlib import logging import sys import structlog from findata_core.config import get_settings from findata_core.redact import RedactingFilter def configure_logging(level: str = "INFO") -> None: settings = get_settings() logging.basicConfig(level=level, stream=sys.stdout, format="%(message)s") # Never let httpx/httpcore print request URLs (they contain the vendor user id). for noisy in ("httpx", "httpcore", "urllib3"): logging.getLogger(noisy).setLevel(logging.WARNING) structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso", utc=True), RedactingFilter(settings.secret_values()), structlog.processors.JSONRenderer() if settings.is_production else structlog.dev.ConsoleRenderer(), ], wrapper_class=structlog.make_filtering_bound_logger(logging.getLevelName(level)), logger_factory=_SafePrintLoggerFactory(), cache_logger_on_first_use=False, ) class _SafePrintLogger: """stdout logger that survives a closed pipe/pty (EIO/EPIPE) so a hang-up never crashes a running job.""" def __init__(self): self._file = sys.stdout def msg(self, message: str) -> None: with contextlib.suppress(OSError, ValueError): print(message, file=self._file, flush=True) log = debug = info = warn = warning = error = critical = exception = fatal = msg class _SafePrintLoggerFactory: def __call__(self, *args): return _SafePrintLogger() def get_logger(name: str): # type: ignore[no-untyped-def] return structlog.get_logger(name)