Skip to content

Commit 65fcbd2

Browse files
authored
Merge pull request #78 from modern-python/53-try-litestar-structlog-plugin
feat: integrate litestar StructlogPlugin for request.logger support
2 parents 53334b9 + b7d7f05 commit 65fcbd2

5 files changed

Lines changed: 88 additions & 11 deletions

File tree

docs/integrations/litestar.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,17 @@ application = bootstrapper.bootstrap()
4747
```
4848

4949
Read more about available configuration options [here](../../../introduction/configuration):
50+
51+
## Logging
52+
53+
Structlog is integrated via Litestar's `StructlogPlugin`, which makes `request.logger` available in route handlers:
54+
55+
```python
56+
from litestar import Request, get
57+
58+
59+
@get("/items")
60+
async def list_items(request: Request) -> list[str]:
61+
request.logger.info("listing items")
62+
return []
63+
```

docs/introduction/configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ Additional parameters:
115115
- `logging_extra_processors`
116116
- `logging_unset_handlers`.
117117

118+
### Structlog Litestar
119+
120+
When using Litestar, the `StructlogPlugin` is automatically registered, which enables `request.logger` inside route handlers:
121+
122+
```python
123+
from litestar import Litestar, Request, get
124+
from lite_bootstrap import LitestarConfig, LitestarBootstrapper
125+
126+
127+
@get("/")
128+
async def handler(request: Request) -> dict[str, str]:
129+
request.logger.info("handling request")
130+
return {"status": "ok"}
131+
```
132+
118133
### Structlog FastStream
119134

120135
When using FastStream, the structlog logger is automatically injected into the broker so that all broker

lite_bootstrap/bootstrappers/litestar_bootstrapper.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
import litestar
2929
from litestar.config.app import AppConfig
3030
from litestar.config.cors import CORSConfig
31+
from litestar.logging.config import StructLoggingConfig
3132
from litestar.openapi import OpenAPIConfig
3233
from litestar.openapi.plugins import SwaggerRenderPlugin
3334
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
35+
from litestar.plugins.structlog import StructlogConfig, StructlogPlugin
3436
from litestar.static_files import create_static_files_router
3537

3638
if import_checker.is_litestar_opentelemetry_installed:
@@ -42,6 +44,9 @@
4244
if import_checker.is_opentelemetry_installed:
4345
from opentelemetry.trace import get_tracer_provider
4446

47+
if import_checker.is_structlog_installed:
48+
import structlog
49+
4550

4651
def build_span_name(method: str, route: str) -> str:
4752
if not route:
@@ -144,6 +149,25 @@ def bootstrap(self) -> None:
144149
class LitestarLoggingInstrument(LoggingInstrument):
145150
bootstrap_config: LitestarConfig
146151

152+
def bootstrap(self) -> None:
153+
self._unset_handlers()
154+
if import_checker.is_structlog_installed and import_checker.is_litestar_installed:
155+
self.bootstrap_config.application_config.plugins.append(
156+
StructlogPlugin(
157+
config=StructlogConfig(
158+
structlog_logging_config=StructLoggingConfig(
159+
processors=self.structlog_processors,
160+
logger_factory=self.memory_logger_factory,
161+
wrapper_class=structlog.stdlib.BoundLogger,
162+
cache_logger_on_first_use=True,
163+
pretty_print_tty=False,
164+
standard_lib_logging_config=None,
165+
),
166+
),
167+
)
168+
)
169+
self._configure_foreign_loggers()
170+
147171

148172
@dataclasses.dataclass(kw_only=True, frozen=True)
149173
class LitestarOpenTelemetryInstrument(OpenTelemetryInstrument):

lite_bootstrap/instruments/logging_instrument.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,28 @@ def _unset_handlers(self) -> None:
127127
for unset_handlers_logger in self.bootstrap_config.logging_unset_handlers:
128128
logging.getLogger(unset_handlers_logger).handlers = []
129129

130+
@property
131+
def structlog_processors(self) -> list[typing.Any]:
132+
return [
133+
structlog.stdlib.filter_by_level,
134+
*self.structlog_pre_chain_processors,
135+
*self.bootstrap_config.logging_extra_processors,
136+
structlog.processors.JSONRenderer(serializer=_serialize_log_with_orjson_to_string),
137+
]
138+
139+
@property
140+
def memory_logger_factory(self) -> "MemoryLoggerFactory":
141+
return MemoryLoggerFactory(
142+
logging_buffer_capacity=self.bootstrap_config.logging_buffer_capacity,
143+
logging_flush_level=self.bootstrap_config.logging_flush_level,
144+
logging_log_level=self.bootstrap_config.logging_log_level,
145+
)
146+
130147
def _configure_structlog_loggers(self) -> None:
131148
structlog.configure(
132-
processors=[
133-
structlog.stdlib.filter_by_level,
134-
*self.structlog_pre_chain_processors,
135-
*self.bootstrap_config.logging_extra_processors,
136-
structlog.processors.JSONRenderer(serializer=_serialize_log_with_orjson_to_string),
137-
],
149+
processors=self.structlog_processors,
138150
context_class=dict,
139-
logger_factory=MemoryLoggerFactory(
140-
logging_buffer_capacity=self.bootstrap_config.logging_buffer_capacity,
141-
logging_flush_level=self.bootstrap_config.logging_flush_level,
142-
logging_log_level=self.bootstrap_config.logging_log_level,
143-
),
151+
logger_factory=self.memory_logger_factory,
144152
wrapper_class=structlog.stdlib.BoundLogger,
145153
cache_logger_on_first_use=True,
146154
)

tests/test_litestar_bootstrap.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ async def get_item(item_id: int) -> dict[str, int]:
112112
assert any("GET /items/{item_id}" in name for name in span_names)
113113

114114

115+
def test_litestar_request_logger(litestar_config: LitestarConfig) -> None:
116+
@litestar.get("/log-test")
117+
async def log_handler(request: litestar.Request) -> dict[str, str]:
118+
request.logger.info("test log from handler", key="value")
119+
return {"status": "ok"}
120+
121+
config = dataclasses.replace(litestar_config, application_config=AppConfig(route_handlers=[log_handler]))
122+
bootstrapper = LitestarBootstrapper(bootstrap_config=config)
123+
application = bootstrapper.bootstrap()
124+
125+
with TestClient(app=application) as client:
126+
response = client.get("/log-test")
127+
assert response.status_code == status_codes.HTTP_200_OK
128+
assert response.json() == {"status": "ok"}
129+
130+
115131
def test_build_span_name_no_route() -> None:
116132
assert build_span_name("GET", "") == "GET"
117133

0 commit comments

Comments
 (0)