Skip to content

Commit d4c5e5a

Browse files
authored
Merge pull request #11 from modern-python/4-feature-bootstrap-for-litestar
add litestar bootstrap
2 parents a8f7ee6 + 4215f54 commit d4c5e5a

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import contextlib
2+
import dataclasses
3+
import typing
4+
5+
from lite_bootstrap.bootstraps.base import BaseBootstrap
6+
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksInstrument, HealthCheckTypedDict
7+
from lite_bootstrap.instruments.logging_instrument import LoggingInstrument
8+
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
9+
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
10+
from lite_bootstrap.service_config import ServiceConfig
11+
12+
13+
with contextlib.suppress(ImportError):
14+
import litestar
15+
from litestar.config.app import AppConfig
16+
from litestar.contrib.opentelemetry import OpenTelemetryConfig
17+
from opentelemetry.trace import get_tracer_provider
18+
19+
20+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
21+
class LitestarHealthChecksInstrument(HealthChecksInstrument):
22+
enabled: bool = True
23+
path: str = "/health/"
24+
include_in_schema: bool = False
25+
26+
def build_litestar_health_check_router(self, service_config: ServiceConfig) -> litestar.Router:
27+
@litestar.get(media_type=litestar.MediaType.JSON)
28+
async def health_check_handler() -> HealthCheckTypedDict:
29+
return self.render_health_check_data(service_config)
30+
31+
return litestar.Router(
32+
path=self.path,
33+
route_handlers=[health_check_handler],
34+
tags=["probes"],
35+
include_in_schema=self.include_in_schema,
36+
)
37+
38+
def bootstrap(self, service_config: ServiceConfig, app_config: AppConfig | None = None) -> None:
39+
if app_config:
40+
app_config.route_handlers.append(self.build_litestar_health_check_router(service_config))
41+
42+
43+
@dataclasses.dataclass(kw_only=True, frozen=True)
44+
class LitestarLoggingInstrument(LoggingInstrument): ...
45+
46+
47+
@dataclasses.dataclass(kw_only=True, frozen=True)
48+
class LitestarOpenTelemetryInstrument(OpenTelemetryInstrument):
49+
excluded_urls: list[str] = dataclasses.field(default_factory=list)
50+
51+
def bootstrap(self, service_config: ServiceConfig, app_config: AppConfig | None = None) -> None:
52+
super().bootstrap(service_config, app_config)
53+
if app_config:
54+
app_config.middleware.append(
55+
OpenTelemetryConfig(
56+
tracer_provider=get_tracer_provider(),
57+
exclude=self.excluded_urls,
58+
).middleware,
59+
)
60+
61+
62+
@dataclasses.dataclass(kw_only=True, frozen=True)
63+
class LitestarSentryInstrument(SentryInstrument): ...
64+
65+
66+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
67+
class LitestarBootstrap(BaseBootstrap[AppConfig]):
68+
application: AppConfig
69+
instruments: typing.Sequence[
70+
LitestarOpenTelemetryInstrument
71+
| LitestarSentryInstrument
72+
| LitestarHealthChecksInstrument
73+
| LitestarLoggingInstrument
74+
]
75+
service_config: ServiceConfig

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ fastapi-otl = [
5555
fastapi-all = [
5656
"lite-bootstrap[sentry,otl,logging,fastapi,fastapi-otl]"
5757
]
58+
litestar = [
59+
"litestar>=2.9",
60+
]
61+
litestar-otl = [
62+
"opentelemetry-instrumentation-asgi>=0.46b0",
63+
]
64+
litestar-all = [
65+
"lite-bootstrap[sentry,otl,logging,litestar,litestar-otl]"
66+
]
5867

5968
[dependency-groups]
6069
dev = [

tests/test_litestar_bootstrap.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import litestar
2+
import structlog
3+
from litestar import status_codes
4+
from litestar.config.app import AppConfig
5+
from litestar.testing import TestClient
6+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
7+
8+
from lite_bootstrap.bootstraps.litestar_bootstrap import (
9+
LitestarBootstrap,
10+
LitestarHealthChecksInstrument,
11+
LitestarLoggingInstrument,
12+
LitestarOpenTelemetryInstrument,
13+
LitestarSentryInstrument,
14+
)
15+
from lite_bootstrap.service_config import ServiceConfig
16+
from tests.conftest import CustomInstrumentor
17+
18+
19+
logger = structlog.getLogger(__name__)
20+
21+
22+
def test_litestar_bootstrap(service_config: ServiceConfig) -> None:
23+
app_config = AppConfig()
24+
litestar_bootstrap = LitestarBootstrap(
25+
application=app_config,
26+
service_config=service_config,
27+
instruments=[
28+
LitestarOpenTelemetryInstrument(
29+
endpoint="otl",
30+
instrumentors=[CustomInstrumentor()],
31+
span_exporter=ConsoleSpanExporter(),
32+
),
33+
LitestarSentryInstrument(
34+
dsn="https://testdsn@localhost/1",
35+
),
36+
LitestarHealthChecksInstrument(
37+
path="/health/",
38+
),
39+
LitestarLoggingInstrument(logging_buffer_capacity=0),
40+
],
41+
)
42+
litestar_bootstrap.bootstrap()
43+
application = litestar.Litestar.from_config(app_config)
44+
logger.info("testing logging", key="value")
45+
46+
try:
47+
with TestClient(app=application) as async_client:
48+
response = async_client.get("/health/")
49+
assert response.status_code == status_codes.HTTP_200_OK
50+
assert response.json() == {
51+
"health_status": True,
52+
"service_name": "microservice",
53+
"service_version": "2.0.0",
54+
}
55+
finally:
56+
litestar_bootstrap.teardown()

0 commit comments

Comments
 (0)