microbootstrap 0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microbootstrap/__init__.py +44 -0
- microbootstrap/bootstrappers/__init__.py +0 -0
- microbootstrap/bootstrappers/base.py +114 -0
- microbootstrap/bootstrappers/fastapi.py +148 -0
- microbootstrap/bootstrappers/faststream.py +121 -0
- microbootstrap/bootstrappers/litestar.py +166 -0
- microbootstrap/config/__init__.py +0 -0
- microbootstrap/config/fastapi.py +64 -0
- microbootstrap/config/faststream.py +27 -0
- microbootstrap/config/litestar.py +21 -0
- microbootstrap/console_writer.py +34 -0
- microbootstrap/exceptions.py +10 -0
- microbootstrap/granian_server.py +41 -0
- microbootstrap/helpers.py +111 -0
- microbootstrap/instruments/__init__.py +0 -0
- microbootstrap/instruments/base.py +62 -0
- microbootstrap/instruments/cors_instrument.py +29 -0
- microbootstrap/instruments/health_checks_instrument.py +39 -0
- microbootstrap/instruments/instrument_box.py +50 -0
- microbootstrap/instruments/logging_instrument.py +202 -0
- microbootstrap/instruments/opentelemetry_instrument.py +203 -0
- microbootstrap/instruments/prometheus_instrument.py +63 -0
- microbootstrap/instruments/pyroscope_instrument.py +54 -0
- microbootstrap/instruments/sentry_instrument.py +124 -0
- microbootstrap/instruments/swagger_instrument.py +30 -0
- microbootstrap/instruments_setupper.py +72 -0
- microbootstrap/middlewares/__init__.py +0 -0
- microbootstrap/middlewares/fastapi.py +42 -0
- microbootstrap/middlewares/litestar.py +49 -0
- microbootstrap/py.typed +0 -0
- microbootstrap/settings.py +112 -0
- microbootstrap-0.dist-info/METADATA +920 -0
- microbootstrap-0.dist-info/RECORD +34 -0
- microbootstrap-0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from microbootstrap.instruments.cors_instrument import CorsConfig
|
|
2
|
+
from microbootstrap.instruments.health_checks_instrument import HealthChecksConfig
|
|
3
|
+
from microbootstrap.instruments.logging_instrument import LoggingConfig
|
|
4
|
+
from microbootstrap.instruments.opentelemetry_instrument import (
|
|
5
|
+
FastStreamOpentelemetryConfig,
|
|
6
|
+
FastStreamTelemetryMiddlewareProtocol,
|
|
7
|
+
OpentelemetryConfig,
|
|
8
|
+
)
|
|
9
|
+
from microbootstrap.instruments.prometheus_instrument import (
|
|
10
|
+
FastApiPrometheusConfig,
|
|
11
|
+
FastStreamPrometheusConfig,
|
|
12
|
+
FastStreamPrometheusMiddlewareProtocol,
|
|
13
|
+
LitestarPrometheusConfig,
|
|
14
|
+
)
|
|
15
|
+
from microbootstrap.instruments.pyroscope_instrument import PyroscopeConfig
|
|
16
|
+
from microbootstrap.instruments.sentry_instrument import SentryConfig
|
|
17
|
+
from microbootstrap.instruments.swagger_instrument import SwaggerConfig
|
|
18
|
+
from microbootstrap.settings import (
|
|
19
|
+
FastApiSettings,
|
|
20
|
+
FastStreamSettings,
|
|
21
|
+
InstrumentsSetupperSettings,
|
|
22
|
+
LitestarSettings,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
"CorsConfig",
|
|
28
|
+
"FastApiPrometheusConfig",
|
|
29
|
+
"FastApiSettings",
|
|
30
|
+
"FastStreamOpentelemetryConfig",
|
|
31
|
+
"FastStreamPrometheusConfig",
|
|
32
|
+
"FastStreamPrometheusMiddlewareProtocol",
|
|
33
|
+
"FastStreamSettings",
|
|
34
|
+
"FastStreamTelemetryMiddlewareProtocol",
|
|
35
|
+
"HealthChecksConfig",
|
|
36
|
+
"InstrumentsSetupperSettings",
|
|
37
|
+
"LitestarPrometheusConfig",
|
|
38
|
+
"LitestarSettings",
|
|
39
|
+
"LoggingConfig",
|
|
40
|
+
"OpentelemetryConfig",
|
|
41
|
+
"PyroscopeConfig",
|
|
42
|
+
"SentryConfig",
|
|
43
|
+
"SwaggerConfig",
|
|
44
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import abc
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from microbootstrap.console_writer import ConsoleWriter
|
|
6
|
+
from microbootstrap.helpers import dataclass_to_dict_no_defaults, merge_dataclasses_configs, merge_dict_configs
|
|
7
|
+
from microbootstrap.instruments.instrument_box import InstrumentBox
|
|
8
|
+
from microbootstrap.settings import SettingsT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
import typing_extensions
|
|
13
|
+
|
|
14
|
+
from microbootstrap.instruments.base import Instrument, InstrumentConfigT
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataclassInstance(typing.Protocol):
|
|
18
|
+
__dataclass_fields__: typing.ClassVar[dict[str, typing.Any]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ApplicationT = typing.TypeVar("ApplicationT", bound=typing.Any)
|
|
22
|
+
DataclassT = typing.TypeVar("DataclassT", bound=DataclassInstance)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApplicationBootstrapper(abc.ABC, typing.Generic[SettingsT, ApplicationT, DataclassT]):
|
|
26
|
+
application_type: type[ApplicationT]
|
|
27
|
+
application_config: DataclassT
|
|
28
|
+
console_writer: ConsoleWriter
|
|
29
|
+
instrument_box: InstrumentBox
|
|
30
|
+
|
|
31
|
+
def __init__(self, settings: SettingsT) -> None:
|
|
32
|
+
self.settings = settings
|
|
33
|
+
self.console_writer = ConsoleWriter(writer_enabled=settings.service_debug)
|
|
34
|
+
|
|
35
|
+
if not hasattr(self, "instrument_box"):
|
|
36
|
+
self.instrument_box = InstrumentBox()
|
|
37
|
+
self.instrument_box.initialize(self.settings)
|
|
38
|
+
|
|
39
|
+
def configure_application(
|
|
40
|
+
self,
|
|
41
|
+
application_config: DataclassT,
|
|
42
|
+
) -> typing_extensions.Self:
|
|
43
|
+
self.application_config = merge_dataclasses_configs(self.application_config, application_config)
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def configure_instrument(
|
|
47
|
+
self,
|
|
48
|
+
instrument_config: InstrumentConfigT,
|
|
49
|
+
) -> typing_extensions.Self:
|
|
50
|
+
self.instrument_box.configure_instrument(instrument_config)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def configure_instruments(
|
|
54
|
+
self,
|
|
55
|
+
*instrument_configs: InstrumentConfigT,
|
|
56
|
+
) -> typing_extensions.Self:
|
|
57
|
+
for instrument_config in instrument_configs:
|
|
58
|
+
self.configure_instrument(instrument_config)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def use_instrument(
|
|
63
|
+
cls,
|
|
64
|
+
) -> typing.Callable[
|
|
65
|
+
[type[Instrument[InstrumentConfigT]]],
|
|
66
|
+
type[Instrument[InstrumentConfigT]],
|
|
67
|
+
]:
|
|
68
|
+
if not hasattr(cls, "instrument_box"):
|
|
69
|
+
cls.instrument_box = InstrumentBox()
|
|
70
|
+
return cls.instrument_box.extend_instruments
|
|
71
|
+
|
|
72
|
+
def bootstrap(self) -> ApplicationT:
|
|
73
|
+
resulting_application_config: dict[str, typing.Any] = {}
|
|
74
|
+
for instrument in self.instrument_box.instruments:
|
|
75
|
+
if instrument.is_ready():
|
|
76
|
+
instrument.bootstrap()
|
|
77
|
+
resulting_application_config = merge_dict_configs(
|
|
78
|
+
resulting_application_config,
|
|
79
|
+
instrument.bootstrap_before(),
|
|
80
|
+
)
|
|
81
|
+
instrument.write_status(self.console_writer)
|
|
82
|
+
|
|
83
|
+
resulting_application_config = merge_dict_configs(
|
|
84
|
+
resulting_application_config,
|
|
85
|
+
dataclass_to_dict_no_defaults(self.application_config),
|
|
86
|
+
)
|
|
87
|
+
application = self.application_type(
|
|
88
|
+
**merge_dict_configs(resulting_application_config, self.bootstrap_before()),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.bootstrap_before_instruments_after_app_created(application)
|
|
92
|
+
|
|
93
|
+
for instrument in self.instrument_box.instruments:
|
|
94
|
+
if instrument.is_ready():
|
|
95
|
+
application = instrument.bootstrap_after(application)
|
|
96
|
+
|
|
97
|
+
return self.bootstrap_after(application)
|
|
98
|
+
|
|
99
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
100
|
+
"""Add some framework-related parameters to final bootstrap result before application creation."""
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
def bootstrap_before_instruments_after_app_created(self, application: ApplicationT) -> ApplicationT:
|
|
104
|
+
"""Add some framework-related parameters to bootstrap result after application creation, but before instruments are applied.""" # noqa: E501
|
|
105
|
+
return application
|
|
106
|
+
|
|
107
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
108
|
+
"""Add some framework-related parameters to final bootstrap result after application creation."""
|
|
109
|
+
return application
|
|
110
|
+
|
|
111
|
+
def teardown(self) -> None:
|
|
112
|
+
for instrument in self.instrument_box.instruments:
|
|
113
|
+
if instrument.is_ready():
|
|
114
|
+
instrument.teardown()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
from fastapi_offline_docs import enable_offline_docs
|
|
7
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
8
|
+
from prometheus_fastapi_instrumentator import Instrumentator
|
|
9
|
+
|
|
10
|
+
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
|
|
11
|
+
from microbootstrap.config.fastapi import FastApiConfig
|
|
12
|
+
from microbootstrap.instruments.cors_instrument import CorsInstrument
|
|
13
|
+
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument, HealthCheckTypedDict
|
|
14
|
+
from microbootstrap.instruments.logging_instrument import LoggingInstrument
|
|
15
|
+
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
|
|
16
|
+
from microbootstrap.instruments.prometheus_instrument import FastApiPrometheusConfig, PrometheusInstrument
|
|
17
|
+
from microbootstrap.instruments.pyroscope_instrument import PyroscopeInstrument
|
|
18
|
+
from microbootstrap.instruments.sentry_instrument import SentryInstrument
|
|
19
|
+
from microbootstrap.instruments.swagger_instrument import SwaggerInstrument
|
|
20
|
+
from microbootstrap.middlewares.fastapi import build_fastapi_logging_middleware
|
|
21
|
+
from microbootstrap.settings import FastApiSettings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ApplicationT = typing.TypeVar("ApplicationT", bound=fastapi.FastAPI)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FastApiBootstrapper(
|
|
28
|
+
ApplicationBootstrapper[FastApiSettings, fastapi.FastAPI, FastApiConfig],
|
|
29
|
+
):
|
|
30
|
+
application_config = FastApiConfig()
|
|
31
|
+
application_type = fastapi.FastAPI
|
|
32
|
+
|
|
33
|
+
@contextlib.asynccontextmanager
|
|
34
|
+
async def _lifespan_manager(self, _: fastapi.FastAPI) -> typing.AsyncIterator[None]:
|
|
35
|
+
try:
|
|
36
|
+
self.console_writer.print_bootstrap_table()
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
self.teardown()
|
|
40
|
+
|
|
41
|
+
@contextlib.asynccontextmanager
|
|
42
|
+
async def _wrapped_lifespan_manager(self, app: fastapi.FastAPI) -> typing.AsyncIterator[None]:
|
|
43
|
+
assert self.application_config.lifespan # noqa: S101
|
|
44
|
+
async with self._lifespan_manager(app), self.application_config.lifespan(app):
|
|
45
|
+
yield None
|
|
46
|
+
|
|
47
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
48
|
+
return {
|
|
49
|
+
"debug": self.settings.service_debug,
|
|
50
|
+
"lifespan": self._wrapped_lifespan_manager if self.application_config.lifespan else self._lifespan_manager,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
FastApiBootstrapper.use_instrument()(SentryInstrument)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@FastApiBootstrapper.use_instrument()
|
|
58
|
+
class FastApiSwaggerInstrument(SwaggerInstrument):
|
|
59
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
60
|
+
return {
|
|
61
|
+
"title": self.instrument_config.service_name,
|
|
62
|
+
"description": self.instrument_config.service_description,
|
|
63
|
+
"docs_url": self.instrument_config.swagger_path,
|
|
64
|
+
"version": self.instrument_config.service_version,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
68
|
+
if self.instrument_config.swagger_offline_docs:
|
|
69
|
+
enable_offline_docs(application, static_files_handler=self.instrument_config.service_static_path)
|
|
70
|
+
return application
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@FastApiBootstrapper.use_instrument()
|
|
74
|
+
class FastApiCorsInstrument(CorsInstrument):
|
|
75
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
76
|
+
application.add_middleware(
|
|
77
|
+
CORSMiddleware,
|
|
78
|
+
allow_origins=self.instrument_config.cors_allowed_origins,
|
|
79
|
+
allow_methods=self.instrument_config.cors_allowed_methods,
|
|
80
|
+
allow_headers=self.instrument_config.cors_allowed_headers,
|
|
81
|
+
allow_credentials=self.instrument_config.cors_allowed_credentials,
|
|
82
|
+
allow_origin_regex=self.instrument_config.cors_allowed_origin_regex,
|
|
83
|
+
expose_headers=self.instrument_config.cors_exposed_headers,
|
|
84
|
+
max_age=self.instrument_config.cors_max_age,
|
|
85
|
+
)
|
|
86
|
+
return application
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
FastApiBootstrapper.use_instrument()(PyroscopeInstrument)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@FastApiBootstrapper.use_instrument()
|
|
93
|
+
class FastApiOpentelemetryInstrument(OpentelemetryInstrument):
|
|
94
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
95
|
+
FastAPIInstrumentor.instrument_app(
|
|
96
|
+
application,
|
|
97
|
+
tracer_provider=self.tracer_provider,
|
|
98
|
+
excluded_urls=",".join(self.define_exclude_urls()),
|
|
99
|
+
)
|
|
100
|
+
return application
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@FastApiBootstrapper.use_instrument()
|
|
104
|
+
class FastApiLoggingInstrument(LoggingInstrument):
|
|
105
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
106
|
+
if not self.instrument_config.logging_turn_off_middleware:
|
|
107
|
+
application.add_middleware(
|
|
108
|
+
build_fastapi_logging_middleware(self.instrument_config.logging_exclude_endpoints),
|
|
109
|
+
)
|
|
110
|
+
return application
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@FastApiBootstrapper.use_instrument()
|
|
114
|
+
class FastApiPrometheusInstrument(PrometheusInstrument[FastApiPrometheusConfig]):
|
|
115
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
116
|
+
Instrumentator(**self.instrument_config.prometheus_instrumentator_params).instrument(
|
|
117
|
+
application,
|
|
118
|
+
**self.instrument_config.prometheus_instrument_params,
|
|
119
|
+
).expose(
|
|
120
|
+
application,
|
|
121
|
+
endpoint=self.instrument_config.prometheus_metrics_path,
|
|
122
|
+
include_in_schema=self.instrument_config.prometheus_metrics_include_in_schema,
|
|
123
|
+
**self.instrument_config.prometheus_expose_params,
|
|
124
|
+
)
|
|
125
|
+
return application
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_config_type(cls) -> type[FastApiPrometheusConfig]:
|
|
129
|
+
return FastApiPrometheusConfig
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@FastApiBootstrapper.use_instrument()
|
|
133
|
+
class FastApiHealthChecksInstrument(HealthChecksInstrument):
|
|
134
|
+
def build_fastapi_health_check_router(self) -> fastapi.APIRouter:
|
|
135
|
+
fastapi_router: typing.Final = fastapi.APIRouter(
|
|
136
|
+
tags=["probes"],
|
|
137
|
+
include_in_schema=self.instrument_config.health_checks_include_in_schema,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@fastapi_router.get(self.instrument_config.health_checks_path)
|
|
141
|
+
async def health_check_handler() -> HealthCheckTypedDict:
|
|
142
|
+
return self.render_health_check_data()
|
|
143
|
+
|
|
144
|
+
return fastapi_router
|
|
145
|
+
|
|
146
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
147
|
+
application.include_router(self.build_fastapi_health_check_router())
|
|
148
|
+
return application
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import prometheus_client
|
|
6
|
+
import structlog
|
|
7
|
+
import typing_extensions
|
|
8
|
+
from faststream.asgi import AsgiFastStream, AsgiResponse
|
|
9
|
+
from faststream.asgi import get as handle_get
|
|
10
|
+
|
|
11
|
+
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
|
|
12
|
+
from microbootstrap.config.faststream import FastStreamConfig
|
|
13
|
+
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument
|
|
14
|
+
from microbootstrap.instruments.logging_instrument import LoggingInstrument
|
|
15
|
+
from microbootstrap.instruments.opentelemetry_instrument import (
|
|
16
|
+
BaseOpentelemetryInstrument,
|
|
17
|
+
FastStreamOpentelemetryConfig,
|
|
18
|
+
)
|
|
19
|
+
from microbootstrap.instruments.prometheus_instrument import FastStreamPrometheusConfig, PrometheusInstrument
|
|
20
|
+
from microbootstrap.instruments.pyroscope_instrument import PyroscopeInstrument
|
|
21
|
+
from microbootstrap.instruments.sentry_instrument import SentryInstrument
|
|
22
|
+
from microbootstrap.settings import FastStreamSettings
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KwargsAsgiFastStream(AsgiFastStream):
|
|
26
|
+
def __init__(self, **kwargs: typing.Any) -> None: # noqa: ANN401
|
|
27
|
+
# `broker` argument is positional-only
|
|
28
|
+
super().__init__(kwargs.pop("broker", None), **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FastStreamBootstrapper(ApplicationBootstrapper[FastStreamSettings, AsgiFastStream, FastStreamConfig]):
|
|
32
|
+
application_config = FastStreamConfig()
|
|
33
|
+
application_type = KwargsAsgiFastStream
|
|
34
|
+
|
|
35
|
+
def bootstrap_before(self: typing_extensions.Self) -> dict[str, typing.Any]:
|
|
36
|
+
return {
|
|
37
|
+
"title": self.settings.service_name,
|
|
38
|
+
"version": self.settings.service_version,
|
|
39
|
+
"description": self.settings.service_description,
|
|
40
|
+
"on_shutdown": [self.teardown],
|
|
41
|
+
"on_startup": [self.console_writer.print_bootstrap_table],
|
|
42
|
+
"asyncapi_path": self.settings.asyncapi_path,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
FastStreamBootstrapper.use_instrument()(SentryInstrument)
|
|
47
|
+
FastStreamBootstrapper.use_instrument()(PyroscopeInstrument)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@FastStreamBootstrapper.use_instrument()
|
|
51
|
+
class FastStreamOpentelemetryInstrument(BaseOpentelemetryInstrument[FastStreamOpentelemetryConfig]):
|
|
52
|
+
def is_ready(self) -> bool:
|
|
53
|
+
return bool(self.instrument_config.opentelemetry_middleware_cls and super().is_ready())
|
|
54
|
+
|
|
55
|
+
def bootstrap_after(self, application: AsgiFastStream) -> AsgiFastStream: # type: ignore[override]
|
|
56
|
+
if self.instrument_config.opentelemetry_middleware_cls and application.broker:
|
|
57
|
+
application.broker.add_middleware(
|
|
58
|
+
self.instrument_config.opentelemetry_middleware_cls(tracer_provider=self.tracer_provider)
|
|
59
|
+
)
|
|
60
|
+
return application
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_config_type(cls) -> type[FastStreamOpentelemetryConfig]:
|
|
64
|
+
return FastStreamOpentelemetryConfig
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@FastStreamBootstrapper.use_instrument()
|
|
68
|
+
class FastStreamLoggingInstrument(LoggingInstrument):
|
|
69
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
70
|
+
return {"logger": structlog.get_logger("microbootstrap-faststream")}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@FastStreamBootstrapper.use_instrument()
|
|
74
|
+
class FastStreamPrometheusInstrument(PrometheusInstrument[FastStreamPrometheusConfig]):
|
|
75
|
+
def is_ready(self) -> bool:
|
|
76
|
+
return bool(self.instrument_config.prometheus_middleware_cls and super().is_ready())
|
|
77
|
+
|
|
78
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
79
|
+
return {
|
|
80
|
+
"asgi_routes": (
|
|
81
|
+
(
|
|
82
|
+
self.instrument_config.prometheus_metrics_path,
|
|
83
|
+
prometheus_client.make_asgi_app(prometheus_client.REGISTRY),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def bootstrap_after(self, application: AsgiFastStream) -> AsgiFastStream: # type: ignore[override]
|
|
89
|
+
if self.instrument_config.prometheus_middleware_cls and application.broker:
|
|
90
|
+
application.broker.add_middleware(
|
|
91
|
+
self.instrument_config.prometheus_middleware_cls(registry=prometheus_client.REGISTRY)
|
|
92
|
+
)
|
|
93
|
+
return application
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def get_config_type(cls) -> type[FastStreamPrometheusConfig]:
|
|
97
|
+
return FastStreamPrometheusConfig
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@FastStreamBootstrapper.use_instrument()
|
|
101
|
+
class FastStreamHealthChecksInstrument(HealthChecksInstrument):
|
|
102
|
+
def bootstrap(self) -> None: ...
|
|
103
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
104
|
+
@handle_get
|
|
105
|
+
async def check_health(scope: typing.Any) -> AsgiResponse: # noqa: ANN401, ARG001
|
|
106
|
+
return (
|
|
107
|
+
AsgiResponse(
|
|
108
|
+
json.dumps(self.render_health_check_data()).encode(), 200, headers={"content-type": "text/plain"}
|
|
109
|
+
)
|
|
110
|
+
if await self.define_health_status()
|
|
111
|
+
else AsgiResponse(b"Service is unhealthy", 500, headers={"content-type": "application/json"})
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return {"asgi_routes": ((self.instrument_config.health_checks_path, check_health),)}
|
|
115
|
+
|
|
116
|
+
async def define_health_status(self) -> bool:
|
|
117
|
+
return await self.application.broker.ping(timeout=5) if self.application and self.application.broker else False
|
|
118
|
+
|
|
119
|
+
def bootstrap_after(self, application: AsgiFastStream) -> AsgiFastStream: # type: ignore[override]
|
|
120
|
+
self.application = application
|
|
121
|
+
return application
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import litestar
|
|
5
|
+
import litestar.exceptions
|
|
6
|
+
import litestar.types
|
|
7
|
+
import typing_extensions
|
|
8
|
+
from litestar import openapi
|
|
9
|
+
from litestar.config.cors import CORSConfig as LitestarCorsConfig
|
|
10
|
+
from litestar.contrib.opentelemetry.config import OpenTelemetryConfig as LitestarOpentelemetryConfig
|
|
11
|
+
from litestar.contrib.prometheus import PrometheusConfig, PrometheusController
|
|
12
|
+
from litestar.openapi.plugins import SwaggerRenderPlugin
|
|
13
|
+
from litestar_offline_docs import generate_static_files_config
|
|
14
|
+
from sentry_sdk.integrations.litestar import LitestarIntegration
|
|
15
|
+
|
|
16
|
+
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
|
|
17
|
+
from microbootstrap.config.litestar import LitestarConfig
|
|
18
|
+
from microbootstrap.instruments.cors_instrument import CorsInstrument
|
|
19
|
+
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument, HealthCheckTypedDict
|
|
20
|
+
from microbootstrap.instruments.logging_instrument import LoggingInstrument
|
|
21
|
+
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
|
|
22
|
+
from microbootstrap.instruments.prometheus_instrument import LitestarPrometheusConfig, PrometheusInstrument
|
|
23
|
+
from microbootstrap.instruments.pyroscope_instrument import PyroscopeInstrument
|
|
24
|
+
from microbootstrap.instruments.sentry_instrument import SentryInstrument
|
|
25
|
+
from microbootstrap.instruments.swagger_instrument import SwaggerInstrument
|
|
26
|
+
from microbootstrap.middlewares.litestar import build_litestar_logging_middleware
|
|
27
|
+
from microbootstrap.settings import LitestarSettings
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LitestarBootstrapper(
|
|
31
|
+
ApplicationBootstrapper[LitestarSettings, litestar.Litestar, LitestarConfig],
|
|
32
|
+
):
|
|
33
|
+
application_config = LitestarConfig()
|
|
34
|
+
application_type = litestar.Litestar
|
|
35
|
+
|
|
36
|
+
def bootstrap_before(self: typing_extensions.Self) -> dict[str, typing.Any]:
|
|
37
|
+
return {
|
|
38
|
+
"debug": self.settings.service_debug,
|
|
39
|
+
"on_shutdown": [self.teardown],
|
|
40
|
+
"on_startup": [self.console_writer.print_bootstrap_table],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@LitestarBootstrapper.use_instrument()
|
|
45
|
+
class LitestarSentryInstrument(SentryInstrument):
|
|
46
|
+
def bootstrap(self) -> None:
|
|
47
|
+
for sentry_integration in self.instrument_config.sentry_integrations:
|
|
48
|
+
if isinstance(sentry_integration, LitestarIntegration):
|
|
49
|
+
break
|
|
50
|
+
else:
|
|
51
|
+
self.instrument_config.sentry_integrations.append(LitestarIntegration())
|
|
52
|
+
super().bootstrap()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@LitestarBootstrapper.use_instrument()
|
|
56
|
+
class LitestarSwaggerInstrument(SwaggerInstrument):
|
|
57
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
58
|
+
render_plugins: typing.Final = (
|
|
59
|
+
(
|
|
60
|
+
SwaggerRenderPlugin(
|
|
61
|
+
js_url=f"{self.instrument_config.service_static_path}/swagger-ui-bundle.js",
|
|
62
|
+
css_url=f"{self.instrument_config.service_static_path}/swagger-ui.css",
|
|
63
|
+
standalone_preset_js_url=(
|
|
64
|
+
f"{self.instrument_config.service_static_path}/swagger-ui-standalone-preset.js"
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
if self.instrument_config.swagger_offline_docs
|
|
69
|
+
else (SwaggerRenderPlugin(),)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
all_swagger_params: typing.Final = {
|
|
73
|
+
"path": self.instrument_config.swagger_path,
|
|
74
|
+
"title": self.instrument_config.service_name,
|
|
75
|
+
"version": self.instrument_config.service_version,
|
|
76
|
+
"description": self.instrument_config.service_description,
|
|
77
|
+
"render_plugins": render_plugins,
|
|
78
|
+
} | self.instrument_config.swagger_extra_params
|
|
79
|
+
|
|
80
|
+
bootstrap_result: typing.Final[dict[str, typing.Any]] = {
|
|
81
|
+
"openapi_config": openapi.OpenAPIConfig(**all_swagger_params),
|
|
82
|
+
}
|
|
83
|
+
if self.instrument_config.swagger_offline_docs:
|
|
84
|
+
bootstrap_result["static_files_config"] = [
|
|
85
|
+
generate_static_files_config(static_files_handler_path=self.instrument_config.service_static_path),
|
|
86
|
+
]
|
|
87
|
+
return bootstrap_result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@LitestarBootstrapper.use_instrument()
|
|
91
|
+
class LitestarCorsInstrument(CorsInstrument):
|
|
92
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
93
|
+
return {
|
|
94
|
+
"cors_config": LitestarCorsConfig(
|
|
95
|
+
allow_origins=self.instrument_config.cors_allowed_origins,
|
|
96
|
+
allow_methods=self.instrument_config.cors_allowed_methods, # type: ignore[arg-type]
|
|
97
|
+
allow_headers=self.instrument_config.cors_allowed_headers,
|
|
98
|
+
allow_credentials=self.instrument_config.cors_allowed_credentials,
|
|
99
|
+
allow_origin_regex=self.instrument_config.cors_allowed_origin_regex,
|
|
100
|
+
expose_headers=self.instrument_config.cors_exposed_headers,
|
|
101
|
+
max_age=self.instrument_config.cors_max_age,
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
LitestarBootstrapper.use_instrument()(PyroscopeInstrument)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@LitestarBootstrapper.use_instrument()
|
|
110
|
+
class LitestarOpentelemetryInstrument(OpentelemetryInstrument):
|
|
111
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
112
|
+
return {
|
|
113
|
+
"middleware": [
|
|
114
|
+
LitestarOpentelemetryConfig(
|
|
115
|
+
tracer_provider=self.tracer_provider,
|
|
116
|
+
exclude=self.define_exclude_urls(),
|
|
117
|
+
).middleware,
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@LitestarBootstrapper.use_instrument()
|
|
123
|
+
class LitestarLoggingInstrument(LoggingInstrument):
|
|
124
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
125
|
+
if self.instrument_config.logging_turn_off_middleware:
|
|
126
|
+
return {}
|
|
127
|
+
|
|
128
|
+
return {"middleware": [build_litestar_logging_middleware(self.instrument_config.logging_exclude_endpoints)]}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@LitestarBootstrapper.use_instrument()
|
|
132
|
+
class LitestarPrometheusInstrument(PrometheusInstrument[LitestarPrometheusConfig]):
|
|
133
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
134
|
+
class LitestarPrometheusController(PrometheusController):
|
|
135
|
+
path = self.instrument_config.prometheus_metrics_path
|
|
136
|
+
include_in_schema = self.instrument_config.prometheus_metrics_include_in_schema
|
|
137
|
+
openmetrics_format = True
|
|
138
|
+
|
|
139
|
+
litestar_prometheus_config: typing.Final = PrometheusConfig(
|
|
140
|
+
app_name=self.instrument_config.service_name,
|
|
141
|
+
**self.instrument_config.prometheus_additional_params,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return {"route_handlers": [LitestarPrometheusController], "middleware": [litestar_prometheus_config.middleware]}
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_config_type(cls) -> type[LitestarPrometheusConfig]:
|
|
148
|
+
return LitestarPrometheusConfig
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@LitestarBootstrapper.use_instrument()
|
|
152
|
+
class LitestarHealthChecksInstrument(HealthChecksInstrument):
|
|
153
|
+
def build_litestar_health_check_router(self) -> litestar.Router:
|
|
154
|
+
@litestar.get(media_type=litestar.MediaType.JSON)
|
|
155
|
+
async def health_check_handler() -> HealthCheckTypedDict:
|
|
156
|
+
return self.render_health_check_data()
|
|
157
|
+
|
|
158
|
+
return litestar.Router(
|
|
159
|
+
path=self.instrument_config.health_checks_path,
|
|
160
|
+
route_handlers=[health_check_handler],
|
|
161
|
+
tags=["probes"],
|
|
162
|
+
include_in_schema=self.instrument_config.health_checks_include_in_schema,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def bootstrap_before(self) -> dict[str, typing.Any]:
|
|
166
|
+
return {"route_handlers": [self.build_litestar_health_check_router()]}
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import dataclasses
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from fastapi.datastructures import Default
|
|
6
|
+
from fastapi.utils import generate_unique_id
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from fastapi import Request, routing
|
|
12
|
+
from fastapi.applications import AppType
|
|
13
|
+
from fastapi.middleware import Middleware
|
|
14
|
+
from fastapi.params import Depends
|
|
15
|
+
from starlette.responses import Response
|
|
16
|
+
from starlette.routing import BaseRoute
|
|
17
|
+
from starlette.types import Lifespan
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclasses.dataclass
|
|
21
|
+
class FastApiConfig:
|
|
22
|
+
debug: bool = False
|
|
23
|
+
routes: list[BaseRoute] | None = None
|
|
24
|
+
title: str = "FastAPI"
|
|
25
|
+
summary: str | None = None
|
|
26
|
+
description: str = ""
|
|
27
|
+
version: str = "0.1.0"
|
|
28
|
+
openapi_url: str | None = "/openapi.json"
|
|
29
|
+
openapi_tags: list[dict[str, typing.Any]] | None = None
|
|
30
|
+
servers: list[dict[str, str | typing.Any]] | None = None
|
|
31
|
+
dependencies: typing.Sequence[Depends] | None = None
|
|
32
|
+
default_response_class: type[Response] = dataclasses.field(default_factory=lambda: Default(JSONResponse))
|
|
33
|
+
redirect_slashes: bool = True
|
|
34
|
+
docs_url: str | None = "/docs"
|
|
35
|
+
redoc_url: str | None = "/redoc"
|
|
36
|
+
swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect"
|
|
37
|
+
swagger_ui_init_oauth: dict[str, typing.Any] | None = None
|
|
38
|
+
middleware: typing.Sequence[Middleware] | None = None
|
|
39
|
+
exception_handlers: (
|
|
40
|
+
dict[
|
|
41
|
+
int | type[Exception],
|
|
42
|
+
typing.Callable[[Request, typing.Any], typing.Coroutine[typing.Any, typing.Any, Response]],
|
|
43
|
+
]
|
|
44
|
+
| None
|
|
45
|
+
) = None
|
|
46
|
+
on_startup: typing.Sequence[typing.Callable[[], typing.Any]] | None = None
|
|
47
|
+
on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None
|
|
48
|
+
lifespan: Lifespan[AppType] | None = None # type: ignore[valid-type]
|
|
49
|
+
terms_of_service: str | None = None
|
|
50
|
+
contact: dict[str, str | typing.Any] | None = None
|
|
51
|
+
license_info: dict[str, str | typing.Any] | None = None
|
|
52
|
+
openapi_prefix: str = ""
|
|
53
|
+
root_path: str = ""
|
|
54
|
+
root_path_in_servers: bool = True
|
|
55
|
+
responses: dict[int | str, dict[str, typing.Any]] | None = None
|
|
56
|
+
callbacks: list[BaseRoute] | None = None
|
|
57
|
+
webhooks: routing.APIRouter | None = None
|
|
58
|
+
deprecated: bool | None = None
|
|
59
|
+
include_in_schema: bool = True
|
|
60
|
+
swagger_ui_parameters: dict[str, typing.Any] | None = None
|
|
61
|
+
generate_unique_id_function: typing.Callable[[routing.APIRoute], str] = dataclasses.field(
|
|
62
|
+
default_factory=lambda: Default(generate_unique_id),
|
|
63
|
+
)
|
|
64
|
+
separate_input_output_schemas: bool = True
|