From 56ec255e595d108789874dceeeebc2b70a359f22 Mon Sep 17 00:00:00 2001 From: thomas-samoht <54805624+thomas-samoht@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:06:47 +0200 Subject: [PATCH] Apply the log level to every stream --- CHANGELOG.md | 9 ++++++++ docs/STARTING_GUIDE.md | 4 ++++ gfmodules/logging/config_builder.py | 17 +++++--------- tests/test_config_builder.py | 16 +++++++++++++ tests/test_routing.py | 36 +++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d3104..e795107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Fixed + +- `loglevel` now applies to every stream. A logger with a lower level of its + own, such as SQLAlchemy's with `echo=True` or an application module set to + `DEBUG`, got records below `loglevel` onto every stream on the log server + and onto the debug console. Lower `loglevel` to see those records again. + ## 0.5.0 - 2026-09-17 ### Changed (breaking) diff --git a/docs/STARTING_GUIDE.md b/docs/STARTING_GUIDE.md index 2fe41a0..ea1dea6 100644 --- a/docs/STARTING_GUIDE.md +++ b/docs/STARTING_GUIDE.md @@ -109,6 +109,10 @@ gflog.configure( ) ``` +`loglevel` applies to every stream, on the console and on the log server alike. +A logger with a lower level of its own, such as SQLAlchemy's with `echo=True`, +cannot get a record below `loglevel` through. + Compose `ConfigLogging` into the application's own settings model: ```python diff --git a/gfmodules/logging/config_builder.py b/gfmodules/logging/config_builder.py index d78189e..e6557ad 100644 --- a/gfmodules/logging/config_builder.py +++ b/gfmodules/logging/config_builder.py @@ -31,6 +31,7 @@ def _syslog_handler(self, path: str, formatter: str, filters: list[str] | None = cfg: dict[str, Any] = { "class": "logging.handlers.SysLogHandler", "address": (host, int(port_str)), + "level": self.loglevel, "formatter": formatter, } if filters: @@ -148,28 +149,22 @@ def _add_console_streams(self, conf: dict[str, Any]) -> None: uvicorn_error_handlers = conf["loggers"]["uvicorn.error"]["handlers"] root_handlers = conf["root"]["handlers"] - bindings: dict[str, tuple[str, str | None, str, list[list[str]]]] = { - "app": ( - "plain_app", - "app_filter", - self.loglevel, - [app_logger_handlers, uvicorn_handlers, uvicorn_error_handlers], - ), - "siem": ("plain_siem", "siem_filter", self.loglevel, [app_logger_handlers]), + bindings: dict[str, tuple[str, str | None, list[list[str]]]] = { + "app": ("plain_app", "app_filter", [app_logger_handlers, uvicorn_handlers, uvicorn_error_handlers]), + "siem": ("plain_siem", "siem_filter", [app_logger_handlers]), "debug": ( "plain_debug", None, - "DEBUG", [app_logger_handlers, uvicorn_handlers, uvicorn_error_handlers, root_handlers], ), } for stream_name in self._selected_console_streams(): - formatter, filter_name, level, logger_handler_lists = bindings[stream_name] + formatter, filter_name, logger_handler_lists = bindings[stream_name] handler_name = f"console_{stream_name}" handler: dict[str, Any] = { "class": "logging.StreamHandler", - "level": level, + "level": self.loglevel, "formatter": formatter, "stream": "ext://sys.stdout", } diff --git a/tests/test_config_builder.py b/tests/test_config_builder.py index ed135a0..31dd15f 100644 --- a/tests/test_config_builder.py +++ b/tests/test_config_builder.py @@ -86,24 +86,28 @@ "syslog_app": { "class": "logging.handlers.SysLogHandler", "address": ADDRESS, + "level": "INFO", "formatter": "json_app", "filters": ["app_filter"], }, "syslog_siem": { "class": "logging.handlers.SysLogHandler", "address": ADDRESS, + "level": "INFO", "formatter": "json_siem", "filters": ["siem_filter"], }, "syslog_public_inspect": { "class": "logging.handlers.SysLogHandler", "address": ADDRESS, + "level": "INFO", "formatter": "json_public_inspect", "filters": ["public_inspect_filter"], }, "syslog_debug": { "class": "logging.handlers.SysLogHandler", "address": ADDRESS, + "level": "INFO", "formatter": "json_debug", }, }, @@ -201,6 +205,18 @@ def test_honours_the_requested_log_level(self) -> None: assert conf["root"]["level"] == "WARNING" +class TestTheLogLevel: + def test_every_handler_applies_it(self) -> None: + conf = build( + ConfigLogging(syslog_path=SYSLOG, console_streams=["app", "siem", "debug"]), + loglevel="WARNING", + ) + + assert {name: spec.get("level") for name, spec in conf["handlers"].items()} == dict.fromkeys( + conf["handlers"], "WARNING" + ) + + class TestConsoleStreamSelection: def test_an_explicit_selection_replaces_the_default_rather_than_adding_to_it(self) -> None: """Otherwise the app stream prints twice, once per handler.""" diff --git a/tests/test_routing.py b/tests/test_routing.py index 7a089e3..1065b09 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -272,3 +272,39 @@ def test_it_stays_quiet_once_configured(self, delivered: list[dict[str, Any]]) - configure() assert streams_reporting(delivered, "no application_id") == set() + + +class TestALoggerSetBelowTheLogLevel: + @pytest.mark.parametrize("name", ["sqlalchemy.engine.Engine", "app.db"]) + def test_its_records_stay_off_the_log_server(self, delivered: list[dict[str, Any]], name: str) -> None: + configure(loglevel="WARNING") + logging.getLogger(name).setLevel(logging.DEBUG) + + logging.getLogger(name).info("below the log level") + + assert delivered == [] + + def test_its_events_stay_off_the_log_server(self, delivered: list[dict[str, Any]]) -> None: + configure(loglevel="WARNING") + logging.getLogger("app.db").setLevel(logging.DEBUG) + + gflog.emit( + logging.getLogger("app.db"), + CompleteCatalogue.RESOURCE_CREATED, + "resource created", + fields={"resource_id": "r-1", "owner_id": "o-1", "created_by": "alice"}, + ) + + assert delivered == [] + + def test_its_records_stay_off_the_debug_console(self, capsys: pytest.CaptureFixture[str]) -> None: + gflog.configure( + config=gflog.ConfigLogging(console_streams=["debug"]), + loglevel="WARNING", + catalogue=CompleteCatalogue, + ) + logging.getLogger("sqlalchemy.engine.Engine").setLevel(logging.DEBUG) + + logging.getLogger("sqlalchemy.engine.Engine").info("below the log level") + + assert "below the log level" not in capsys.readouterr().out