# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Logging configuration, with the noisy libraries kept out of DEBUG. ``LOG_LEVEL=DEBUG`` exists to answer questions about the bot -- which frame was ignored and why, what shape an envelope actually has. Setting the root logger to DEBUG also turns on httpx, httpcore and asyncio, which between them emit several lines per HTTP request. The bot polls a group listing every 30 seconds, so the one line worth reading arrives buried in hundreds that are not, and journald rate-limiting then starts dropping messages. So the libraries are held at WARNING whatever the bot is set to. A warning from httpx is worth seeing; its per-request bookkeeping is not, at any level -- at INFO it was already emitting a "HTTP Request: GET ... 200 OK" line every thirty seconds, forever. """ from __future__ import annotations import logging from typing import Final FORMAT: Final = "%(asctime)s %(levelname)s %(name)s: %(message)s" # Every third-party logger that is chatty at DEBUG. httpcore is the worst # offender by an order of magnitude: it logs both ends of every connection. NOISY: Final = ("httpx", "httpcore", "asyncio", "websockets", "urllib3") NOISY_FLOOR: Final = logging.WARNING def configure(level: str) -> None: """Send the bot's logs to stderr at ``level``, muting the noisy libraries. Args: level: A logging level name, already validated by config_env. """ logging.basicConfig(level=level, format=FORMAT) # Explicitly too: basicConfig is a documented no-op once the root logger # already has a handler, which is the case under pytest and under some # uvicorn arrangements. Without this the level silently does not apply. logging.getLogger().setLevel(level) for name in NOISY: logging.getLogger(name).setLevel(NOISY_FLOOR)