Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 4 additions & 0 deletions docs/STARTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 6 additions & 11 deletions gfmodules/logging/config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
}
Expand Down
16 changes: 16 additions & 0 deletions tests/test_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
Expand Down Expand Up @@ -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."""
Expand Down
36 changes: 36 additions & 0 deletions tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading