From b7d7f05c6635fa495097f791fafae7b1ce83347a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 29 Apr 2026 10:42:59 +0300 Subject: [PATCH] feat: integrate litestar StructlogPlugin for request.logger support - Extract structlog_processors and memory_logger_factory properties on LoggingInstrument to avoid duplication across bootstrappers - Override LitestarLoggingInstrument.bootstrap() to register StructlogPlugin with our processors and MemoryLoggerFactory, enabling request.logger in route handlers - Document Structlog Litestar integration in configuration and litestar integration pages --- docs/integrations/litestar.md | 14 +++++++++ docs/introduction/configuration.md | 15 ++++++++++ .../bootstrappers/litestar_bootstrapper.py | 24 +++++++++++++++ .../instruments/logging_instrument.py | 30 ++++++++++++------- tests/test_litestar_bootstrap.py | 16 ++++++++++ 5 files changed, 88 insertions(+), 11 deletions(-) diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 044e216..e8ca88f 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -47,3 +47,17 @@ application = bootstrapper.bootstrap() ``` Read more about available configuration options [here](../../../introduction/configuration): + +## Logging + +Structlog is integrated via Litestar's `StructlogPlugin`, which makes `request.logger` available in route handlers: + +```python +from litestar import Request, get + + +@get("/items") +async def list_items(request: Request) -> list[str]: + request.logger.info("listing items") + return [] +``` diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index b8fab52..587d77f 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -115,6 +115,21 @@ Additional parameters: - `logging_extra_processors` - `logging_unset_handlers`. +### Structlog Litestar + +When using Litestar, the `StructlogPlugin` is automatically registered, which enables `request.logger` inside route handlers: + +```python +from litestar import Litestar, Request, get +from lite_bootstrap import LitestarConfig, LitestarBootstrapper + + +@get("/") +async def handler(request: Request) -> dict[str, str]: + request.logger.info("handling request") + return {"status": "ok"} +``` + ### Structlog FastStream When using FastStream, the structlog logger is automatically injected into the broker so that all broker diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index 5b9e7b2..18334a0 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -28,9 +28,11 @@ import litestar from litestar.config.app import AppConfig from litestar.config.cors import CORSConfig + from litestar.logging.config import StructLoggingConfig from litestar.openapi import OpenAPIConfig from litestar.openapi.plugins import SwaggerRenderPlugin from litestar.plugins.prometheus import PrometheusConfig, PrometheusController + from litestar.plugins.structlog import StructlogConfig, StructlogPlugin from litestar.static_files import create_static_files_router if import_checker.is_litestar_opentelemetry_installed: @@ -42,6 +44,9 @@ if import_checker.is_opentelemetry_installed: from opentelemetry.trace import get_tracer_provider +if import_checker.is_structlog_installed: + import structlog + def build_span_name(method: str, route: str) -> str: if not route: @@ -144,6 +149,25 @@ def bootstrap(self) -> None: class LitestarLoggingInstrument(LoggingInstrument): bootstrap_config: LitestarConfig + def bootstrap(self) -> None: + self._unset_handlers() + if import_checker.is_structlog_installed and import_checker.is_litestar_installed: + self.bootstrap_config.application_config.plugins.append( + StructlogPlugin( + config=StructlogConfig( + structlog_logging_config=StructLoggingConfig( + processors=self.structlog_processors, + logger_factory=self.memory_logger_factory, + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + pretty_print_tty=False, + standard_lib_logging_config=None, + ), + ), + ) + ) + self._configure_foreign_loggers() + @dataclasses.dataclass(kw_only=True, frozen=True) class LitestarOpenTelemetryInstrument(OpenTelemetryInstrument): diff --git a/lite_bootstrap/instruments/logging_instrument.py b/lite_bootstrap/instruments/logging_instrument.py index 1984334..25844bf 100644 --- a/lite_bootstrap/instruments/logging_instrument.py +++ b/lite_bootstrap/instruments/logging_instrument.py @@ -127,20 +127,28 @@ def _unset_handlers(self) -> None: for unset_handlers_logger in self.bootstrap_config.logging_unset_handlers: logging.getLogger(unset_handlers_logger).handlers = [] + @property + def structlog_processors(self) -> list[typing.Any]: + return [ + structlog.stdlib.filter_by_level, + *self.structlog_pre_chain_processors, + *self.bootstrap_config.logging_extra_processors, + structlog.processors.JSONRenderer(serializer=_serialize_log_with_orjson_to_string), + ] + + @property + def memory_logger_factory(self) -> "MemoryLoggerFactory": + return MemoryLoggerFactory( + logging_buffer_capacity=self.bootstrap_config.logging_buffer_capacity, + logging_flush_level=self.bootstrap_config.logging_flush_level, + logging_log_level=self.bootstrap_config.logging_log_level, + ) + def _configure_structlog_loggers(self) -> None: structlog.configure( - processors=[ - structlog.stdlib.filter_by_level, - *self.structlog_pre_chain_processors, - *self.bootstrap_config.logging_extra_processors, - structlog.processors.JSONRenderer(serializer=_serialize_log_with_orjson_to_string), - ], + processors=self.structlog_processors, context_class=dict, - logger_factory=MemoryLoggerFactory( - logging_buffer_capacity=self.bootstrap_config.logging_buffer_capacity, - logging_flush_level=self.bootstrap_config.logging_flush_level, - logging_log_level=self.bootstrap_config.logging_log_level, - ), + logger_factory=self.memory_logger_factory, wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index be2b2e9..210cfe0 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -112,6 +112,22 @@ async def get_item(item_id: int) -> dict[str, int]: assert any("GET /items/{item_id}" in name for name in span_names) +def test_litestar_request_logger(litestar_config: LitestarConfig) -> None: + @litestar.get("/log-test") + async def log_handler(request: litestar.Request) -> dict[str, str]: + request.logger.info("test log from handler", key="value") + return {"status": "ok"} + + config = dataclasses.replace(litestar_config, application_config=AppConfig(route_handlers=[log_handler])) + bootstrapper = LitestarBootstrapper(bootstrap_config=config) + application = bootstrapper.bootstrap() + + with TestClient(app=application) as client: + response = client.get("/log-test") + assert response.status_code == status_codes.HTTP_200_OK + assert response.json() == {"status": "ok"} + + def test_build_span_name_no_route() -> None: assert build_span_name("GET", "") == "GET"