-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlitestar_bootstrapper.py
More file actions
263 lines (214 loc) · 10.6 KB
/
litestar_bootstrapper.py
File metadata and controls
263 lines (214 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import dataclasses
import pathlib
import typing
from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
from lite_bootstrap.helpers.path import is_valid_path
from lite_bootstrap.instruments.cors_instrument import CorsConfig, CorsInstrument
from lite_bootstrap.instruments.healthchecks_instrument import (
HealthChecksConfig,
HealthChecksInstrument,
HealthCheckTypedDict,
)
from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument
from lite_bootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig, OpenTelemetryInstrument
from lite_bootstrap.instruments.prometheus_instrument import (
PrometheusConfig as PrometheusBootstrapperConfig,
)
from lite_bootstrap.instruments.prometheus_instrument import (
PrometheusInstrument,
)
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
from lite_bootstrap.instruments.swagger_instrument import SwaggerConfig, SwaggerInstrument
if import_checker.is_litestar_installed:
import litestar
from litestar.config.app import AppConfig
from litestar.config.cors import CORSConfig
from litestar.openapi import OpenAPIConfig
from litestar.openapi.plugins import SwaggerRenderPlugin
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
from litestar.static_files import create_static_files_router
if import_checker.is_litestar_opentelemetry_installed:
from litestar.middleware import ASGIMiddleware
from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.trace import TracerProvider
if import_checker.is_opentelemetry_installed:
from opentelemetry.trace import get_tracer_provider
def build_span_name(method: str, route: str) -> str:
if not route:
return method
return f"{method} {route}"
def build_litestar_route_details_from_scope(
scope: typing.MutableMapping[str, typing.Any],
) -> tuple[str, dict[str, str]]:
path_template: typing.Final = scope.get("path_template")
method: typing.Final = str(scope.get("method", "HTTP")).strip()
if path_template is not None:
path_template_stripped: typing.Final = path_template.strip()
return build_span_name(method, path_template_stripped), {"http.route": path_template_stripped}
path: typing.Final = scope.get("path")
if path is not None:
path_stripped: typing.Final = path.strip()
return build_span_name(method, path_stripped), {"http.route": path_stripped}
return method, {}
if import_checker.is_litestar_opentelemetry_installed:
class LitestarOpenTelemetryInstrumentationMiddleware(ASGIMiddleware):
def __init__(self, tracer_provider: "TracerProvider", excluded_urls: set[str]) -> None:
self._tracer_provider = tracer_provider
self._excluded_urls = ",".join(excluded_urls)
async def handle(
self,
scope: "Scope",
receive: "Receive",
send: "Send",
next_app: "ASGIApp",
) -> None:
await OpenTelemetryMiddleware(
app=next_app,
default_span_details=build_litestar_route_details_from_scope,
excluded_urls=self._excluded_urls,
tracer_provider=self._tracer_provider,
)(scope, receive, send) # ty: ignore[invalid-argument-type]
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LitestarConfig(
CorsConfig,
HealthChecksConfig,
LoggingConfig,
OpentelemetryConfig,
PrometheusBootstrapperConfig,
PyroscopeConfig,
SentryConfig,
SwaggerConfig,
):
application_config: "AppConfig" = dataclasses.field(default_factory=lambda: AppConfig()) # noqa: PLW0108
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
swagger_extra_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LitestarCorsInstrument(CorsInstrument):
bootstrap_config: LitestarConfig
def bootstrap(self) -> None:
self.bootstrap_config.application_config.cors_config = CORSConfig(
allow_origins=self.bootstrap_config.cors_allowed_origins,
allow_methods=self.bootstrap_config.cors_allowed_methods, # ty: ignore[invalid-argument-type]
allow_headers=self.bootstrap_config.cors_allowed_headers,
allow_credentials=self.bootstrap_config.cors_allowed_credentials,
allow_origin_regex=self.bootstrap_config.cors_allowed_origin_regex,
expose_headers=self.bootstrap_config.cors_exposed_headers,
max_age=self.bootstrap_config.cors_max_age,
)
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LitestarHealthChecksInstrument(HealthChecksInstrument):
bootstrap_config: LitestarConfig
def build_litestar_health_check_router(self) -> "litestar.Router":
@litestar.get(media_type=litestar.MediaType.JSON)
async def health_check_handler() -> HealthCheckTypedDict:
return self.render_health_check_data()
return litestar.Router(
path=self.bootstrap_config.health_checks_path,
route_handlers=[health_check_handler],
tags=["probes"],
include_in_schema=self.bootstrap_config.health_checks_include_in_schema,
)
def bootstrap(self) -> None:
self.bootstrap_config.application_config.route_handlers.append(self.build_litestar_health_check_router())
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarLoggingInstrument(LoggingInstrument):
bootstrap_config: LitestarConfig
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarOpenTelemetryInstrument(OpenTelemetryInstrument):
bootstrap_config: LitestarConfig
def _build_excluded_urls(self) -> set[str]:
excluded_urls = set(self.bootstrap_config.opentelemetry_excluded_urls)
excluded_urls.add(self.bootstrap_config.prometheus_metrics_path)
if not self.bootstrap_config.opentelemetry_generate_health_check_spans:
excluded_urls.add(self.bootstrap_config.health_checks_path)
return excluded_urls
def bootstrap(self) -> None:
super().bootstrap()
self.bootstrap_config.application_config.middleware.append(
LitestarOpenTelemetryInstrumentationMiddleware(
tracer_provider=get_tracer_provider(),
excluded_urls=self._build_excluded_urls(),
)
)
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarSentryInstrument(SentryInstrument):
bootstrap_config: LitestarConfig
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarPrometheusInstrument(PrometheusInstrument):
bootstrap_config: LitestarConfig
missing_dependency_message = "prometheus_client is not installed"
@staticmethod
def check_dependencies() -> bool:
return import_checker.is_prometheus_client_installed
def bootstrap(self) -> None:
class LitestarPrometheusController(PrometheusController):
path = self.bootstrap_config.prometheus_metrics_path
include_in_schema = self.bootstrap_config.prometheus_metrics_include_in_schema
openmetrics_format = True
litestar_prometheus_config = PrometheusConfig(
app_name=self.bootstrap_config.service_name,
**self.bootstrap_config.prometheus_additional_params,
)
self.bootstrap_config.application_config.route_handlers.append(LitestarPrometheusController)
self.bootstrap_config.application_config.middleware.append(litestar_prometheus_config.middleware)
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarSwaggerInstrument(SwaggerInstrument):
bootstrap_config: LitestarConfig
not_ready_message = "swagger_path is empty or not valid"
def is_ready(self) -> bool:
return bool(self.bootstrap_config.swagger_path) and is_valid_path(self.bootstrap_config.swagger_path)
def bootstrap(self) -> None:
render_plugins: typing.Final = (
(
SwaggerRenderPlugin(
js_url=f"{self.bootstrap_config.swagger_static_path}/swagger-ui-bundle.js",
css_url=f"{self.bootstrap_config.swagger_static_path}/swagger-ui.css",
standalone_preset_js_url=(
f"{self.bootstrap_config.swagger_static_path}/swagger-ui-standalone-preset.js"
),
),
)
if self.bootstrap_config.swagger_offline_docs
else (SwaggerRenderPlugin(),)
)
self.bootstrap_config.application_config.openapi_config = OpenAPIConfig(
path=self.bootstrap_config.swagger_path,
title=self.bootstrap_config.service_name,
version=self.bootstrap_config.service_version,
description=self.bootstrap_config.service_description,
render_plugins=render_plugins,
**self.bootstrap_config.swagger_extra_params,
)
if self.bootstrap_config.swagger_offline_docs:
static_dir_path = pathlib.Path(__file__).parent.parent / "static/litestar_docs"
self.bootstrap_config.application_config.route_handlers.append(
create_static_files_router(
path=self.bootstrap_config.swagger_static_path, directories=[static_dir_path]
)
)
class LitestarBootstrapper(BaseBootstrapper["litestar.Litestar"]):
__slots__ = "bootstrap_config", "instruments"
instruments_types: typing.ClassVar = [
LitestarCorsInstrument,
LitestarOpenTelemetryInstrument,
PyroscopeInstrument,
LitestarSentryInstrument,
LitestarHealthChecksInstrument,
LitestarLoggingInstrument,
LitestarPrometheusInstrument,
LitestarSwaggerInstrument,
]
bootstrap_config: LitestarConfig
not_ready_message = "litestar is not installed"
def __init__(self, bootstrap_config: LitestarConfig) -> None:
super().__init__(bootstrap_config)
self.bootstrap_config.application_config.debug = bootstrap_config.service_debug
self.bootstrap_config.application_config.on_shutdown.append(self.teardown)
def is_ready(self) -> bool:
return import_checker.is_litestar_installed
def _prepare_application(self) -> "litestar.Litestar":
return litestar.Litestar.from_config(self.bootstrap_config.application_config)