From da8292909a2aba72924ff2bbdddfe98d36420a86 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 19 Sep 2025 18:32:23 +0300 Subject: [PATCH] allow std loggers for structlog --- .../instruments/logging_instrument.py | 54 +++++++++++++------ .../instruments/sentry_instrument.py | 1 + tests/test_fastapi_bootstrap.py | 20 +++++++ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lite_bootstrap/instruments/logging_instrument.py b/lite_bootstrap/instruments/logging_instrument.py index 32242a0..b81eaf4 100644 --- a/lite_bootstrap/instruments/logging_instrument.py +++ b/lite_bootstrap/instruments/logging_instrument.py @@ -98,6 +98,19 @@ class LoggingInstrument(BaseInstrument): not_ready_message = "service_debug is True" missing_dependency_message = "structlog is not installed" + @property + def structlog_pre_chain_processors(self) -> list[typing.Any]: + return [ + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + tracer_injection, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + ] + def is_ready(self) -> bool: return not self.bootstrap_config.service_debug and import_checker.is_structlog_installed @@ -105,29 +118,15 @@ def is_ready(self) -> bool: def check_dependencies() -> bool: return import_checker.is_structlog_installed - def bootstrap(self) -> None: - # Configure basic logging to allow structlog to catch its events - logging.basicConfig( - format="%(levelname)s [%(asctime)s] %(module)s %(pathname)s - %(message)s", - stream=sys.stdout, - datefmt="%Y-%m-%d %H:%M:%S", - level=self.bootstrap_config.logging_log_level, - ) - + def _unset_handlers(self) -> None: for unset_handlers_logger in self.bootstrap_config.logging_unset_handlers: logging.getLogger(unset_handlers_logger).handlers = [] + def _configure_structlog_loggers(self) -> None: structlog.configure( processors=[ structlog.stdlib.filter_by_level, - structlog.stdlib.add_log_level, - structlog.stdlib.add_logger_name, - tracer_injection, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), + *self.structlog_pre_chain_processors, *self.bootstrap_config.logging_extra_processors, structlog.processors.JSONRenderer(), ], @@ -141,5 +140,26 @@ def bootstrap(self) -> None: cache_logger_on_first_use=True, ) + def _configure_foreign_loggers(self) -> None: + root_logger: typing.Final = logging.getLogger() + stream_handler: typing.Final = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter( + structlog.stdlib.ProcessorFormatter( + foreign_pre_chain=self.structlog_pre_chain_processors, + processors=[ + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.processors.JSONRenderer(), + ], + logger=root_logger, + ) + ) + root_logger.addHandler(stream_handler) + root_logger.setLevel(self.bootstrap_config.logging_log_level) + + def bootstrap(self) -> None: + self._unset_handlers() + self._configure_structlog_loggers() + self._configure_foreign_loggers() + def teardown(self) -> None: structlog.reset_defaults() diff --git a/lite_bootstrap/instruments/sentry_instrument.py b/lite_bootstrap/instruments/sentry_instrument.py index dd59f6b..4cddcb5 100644 --- a/lite_bootstrap/instruments/sentry_instrument.py +++ b/lite_bootstrap/instruments/sentry_instrument.py @@ -24,6 +24,7 @@ class SentryConfig(BaseConfig): sentry_integrations: list["Integration"] = dataclasses.field(default_factory=list) sentry_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) sentry_tags: dict[str, str] | None = None + sentry_default_integrations: bool = True @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) diff --git a/tests/test_fastapi_bootstrap.py b/tests/test_fastapi_bootstrap.py index 9c24335..842e771 100644 --- a/tests/test_fastapi_bootstrap.py +++ b/tests/test_fastapi_bootstrap.py @@ -1,4 +1,5 @@ import dataclasses +import logging import fastapi import pytest @@ -11,6 +12,7 @@ logger = structlog.getLogger(__name__) +std_logger = logging.getLogger() @pytest.fixture @@ -58,6 +60,24 @@ def test_fastapi_bootstrap(fastapi_config: FastAPIConfig) -> None: assert not bootstrapper.is_bootstrapped +def test_fastapi_bootstrap_std_logger(fastapi_config: FastAPIConfig, capsys: pytest.CaptureFixture[str]) -> None: + bootstrapper = FastAPIBootstrapper(bootstrap_config=fastapi_config) + application = bootstrapper.bootstrap() + + @application.get("/") + async def home() -> str: + std_logger.info("std logger") + logger.info("structlog logger") + return "" + + with TestClient(application) as test_client: + test_client.get("/") + + stdout = capsys.readouterr().out + assert '"event": "std logger", "level": "info", "logger": "root"' in stdout + assert stdout.count("std logger") == 1 + + def test_fastapi_bootstrapper_not_ready() -> None: with emulate_package_missing("fastapi"), pytest.raises(RuntimeError, match="fastapi is not installed"): FastAPIBootstrapper(bootstrap_config=FastAPIConfig())