From b94e2eeae7b2f9000eb4942a2cd4fde84a2e9d4b Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 27 Oct 2026 22:19:14 +0300 Subject: [PATCH] fix: keep lite-bootstrap's root log handler on Litestar and write litestar records once --- .../bootstrappers/litestar_bootstrapper.py | 13 +++++++++- tests/test_litestar_bootstrap.py | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index 623f031..ec37235 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -31,6 +31,7 @@ from litestar import status_codes from litestar.config.app import AppConfig from litestar.config.cors import CORSConfig + from litestar.logging.config import LoggingConfig as LitestarStandardLoggingConfig from litestar.logging.config import StructLoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig from litestar.openapi import OpenAPIConfig @@ -187,6 +188,16 @@ def _build_logging_middleware_config(self) -> "LoggingMiddlewareConfig": exclude=excluded_paths or None, ) + def _build_standard_lib_logging_config(self) -> "LitestarStandardLoggingConfig": + """Leave stdlib output to lite-bootstrap: Litestar's default config replaces the root handler.""" + return LitestarStandardLoggingConfig( + configure_root_logger=False, + handlers={"console": {"class": "logging.NullHandler"}, "queue_listener": {"class": "logging.NullHandler"}}, + loggers={ + "litestar": {"level": self.bootstrap_config.logging_log_level, "handlers": [], "propagate": False} + }, + ) + def _configure_structlog_loggers(self) -> None: """Register Litestar's StructlogPlugin instead of calling ``structlog.configure`` directly.""" self.bootstrap_config.application_config.plugins.append( @@ -198,7 +209,7 @@ def _configure_structlog_loggers(self) -> None: wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, pretty_print_tty=False, - standard_lib_logging_config=None, + standard_lib_logging_config=self._build_standard_lib_logging_config(), ), # Litestar defaults this to True, which logs full request/response bodies. enable_middleware_logging=self.bootstrap_config.litestar_logging_middleware_enabled, diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index 6cee32f..f135fe0 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -3,6 +3,7 @@ import inspect import json import logging +import logging.handlers import sys import typing import warnings @@ -309,6 +310,31 @@ async def _login_handler(data: dict[str, str]) -> dict[str, str]: return log_lines +def test_litestar_logging_keeps_lite_bootstrap_root_handler(litestar_config: LitestarConfig) -> None: + application = LitestarBootstrapper(bootstrap_config=litestar_config).bootstrap() + with TestClient(app=application): + root_handlers = logging.getLogger().handlers + assert not any(isinstance(handler, logging.handlers.QueueHandler) for handler in root_handlers) + assert any(isinstance(handler.formatter, structlog.stdlib.ProcessorFormatter) for handler in root_handlers) + + +def test_litestar_access_log_has_one_handler(litestar_config: LitestarConfig) -> None: + @litestar.get("/ping") + async def _ping_handler() -> dict[str, bool]: + return {"ok": True} + + config = dataclasses.replace( + litestar_config, + litestar_logging_middleware_enabled=True, + application_config=AppConfig(route_handlers=[_ping_handler]), + ) + application = LitestarBootstrapper(bootstrap_config=config).bootstrap() + with TestClient(app=application) as client: + assert client.get("/ping").status_code == status_codes.HTTP_200_OK + litestar_handlers = logging.getLogger("litestar").handlers + assert [type(handler) for handler in litestar_handlers] == [logging.handlers.MemoryHandler] + + def test_litestar_access_logging_disabled_by_default(litestar_config: LitestarConfig) -> None: log_lines = _post_password(litestar_config)