diff --git a/gfmodules/logging/config_builder.py b/gfmodules/logging/config_builder.py index 54e2fe6..186eac0 100644 --- a/gfmodules/logging/config_builder.py +++ b/gfmodules/logging/config_builder.py @@ -1,3 +1,4 @@ +import logging from typing import Any from gfmodules.logging.config import ConfigLogging @@ -7,6 +8,11 @@ from gfmodules.logging.streams import LoggingStreams +def _at_least(loglevel: str, floor: int) -> str: + numeric = logging.getLevelNamesMapping().get(loglevel.upper()) + return loglevel if numeric is not None and numeric >= floor else logging.getLevelName(floor) + + class LogConfigBuilder: """Reads the registered logger root when ``build()`` runs, not before.""" @@ -118,6 +124,12 @@ def build(self) -> dict[str, Any]: "level": "CRITICAL", "propagate": False, }, + # Floored rather than silenced, and left propagating + # Would leak otherwise as inject logs every binding at DEBUG level + "inject": { + "level": _at_least(self.loglevel, logging.INFO), + "propagate": True, + }, }, "root": {"handlers": ["console"], "level": self.loglevel}, } diff --git a/tests/test_config_builder.py b/tests/test_config_builder.py index bb77f9d..c2e6fc9 100644 --- a/tests/test_config_builder.py +++ b/tests/test_config_builder.py @@ -2,6 +2,8 @@ import logging from typing import Any +import pytest + from gfmodules.logging.config import ConfigLogging from gfmodules.logging.config_builder import LogConfigBuilder from gfmodules.logging.filters import AppFilter, PublicInspectFilter, SiemFilter @@ -101,6 +103,7 @@ "propagate": False, }, "uvicorn.access": {"handlers": [], "level": "CRITICAL", "propagate": False}, + "inject": {"level": "INFO", "propagate": True}, }, "root": {"handlers": ["console", "syslog_debug"], "level": "INFO"}, } @@ -208,6 +211,21 @@ def test_existing_loggers_are_left_enabled(self) -> None: assert build(ConfigLogging())["disable_existing_loggers"] is False +class TestInjectIsFloored: + @pytest.mark.parametrize("loglevel", ["DEBUG", "NOTSET", "NONSENSE"]) + def test_it_never_runs_below_info_however_verbose_the_application_is(self, loglevel: str) -> None: + assert build(ConfigLogging(), loglevel=loglevel)["loggers"]["inject"]["level"] == "INFO" + + def test_an_application_quieter_than_the_floor_is_left_alone(self) -> None: + assert build(ConfigLogging(), loglevel="ERROR")["loggers"]["inject"]["level"] == "ERROR" + + def test_what_survives_the_floor_still_reaches_the_debug_stream(self) -> None: + logger = build(ConfigLogging(syslog_path=SYSLOG))["loggers"]["inject"] + + assert logger["propagate"] is True + assert "handlers" not in logger + + class TestApplicationIdStamping: def test_every_json_formatter_is_stamped(self) -> None: formatters = build(ConfigLogging(application_id=APP_ID, syslog_path=SYSLOG))["formatters"]