Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import abc
import typing
import warnings

from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
from lite_bootstrap.types import ApplicationT
Expand All @@ -14,16 +15,28 @@ class BaseBootstrapper(abc.ABC, typing.Generic[ApplicationT]):
bootstrap_config: BaseConfig

def __init__(self, bootstrap_config: BaseConfig) -> None:
if not self.is_ready():
raise RuntimeError(self.not_ready_message)

self.bootstrap_config = bootstrap_config
self.instruments = []
for instrument_type in self.instruments_types:
instrument = instrument_type(bootstrap_config=bootstrap_config)
if instrument.is_ready():
self.instruments.append(instrument)
else:
warnings.warn(instrument.not_ready_message, stacklevel=2)

@property
@abc.abstractmethod
def not_ready_message(self) -> str: ...

@abc.abstractmethod
def _prepare_application(self) -> ApplicationT: ...

@abc.abstractmethod
def is_ready(self) -> bool: ...

def bootstrap(self) -> ApplicationT:
for one_instrument in self.instruments:
one_instrument.bootstrap()
Expand Down
25 changes: 20 additions & 5 deletions lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contextlib
import dataclasses
import typing

from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
from lite_bootstrap.instruments.healthchecks_instrument import (
HealthChecksConfig,
Expand All @@ -14,16 +14,20 @@
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument


with contextlib.suppress(ImportError):
if import_checker.is_fastapi_installed:
import fastapi

if import_checker.is_opentelemetry_installed:
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.trace import get_tracer_provider

if import_checker.is_prometheus_fastapi_instrumentator_installed:
from prometheus_fastapi_instrumentator import Instrumentator


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusConfig, SentryConfig):
application: fastapi.FastAPI = dataclasses.field(default_factory=fastapi.FastAPI)
application: "fastapi.FastAPI" = dataclasses.field(default_factory=fastapi.FastAPI)
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
Expand All @@ -34,7 +38,7 @@ class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, Prom
class FastAPIHealthChecksInstrument(HealthChecksInstrument):
bootstrap_config: FastAPIConfig

def build_fastapi_health_check_router(self) -> fastapi.APIRouter:
def build_fastapi_health_check_router(self) -> "fastapi.APIRouter":
fastapi_router = fastapi.APIRouter(
tags=["probes"],
include_in_schema=self.bootstrap_config.health_checks_include_in_schema,
Expand Down Expand Up @@ -87,6 +91,12 @@ class FastAPISentryInstrument(SentryInstrument):
@dataclasses.dataclass(kw_only=True, frozen=True)
class FastAPIPrometheusInstrument(PrometheusInstrument):
bootstrap_config: FastAPIConfig
not_ready_message = (
PrometheusInstrument.not_ready_message + " or prometheus_fastapi_instrumentator is not installed"
)

def is_ready(self) -> bool:
return super().is_ready() and import_checker.is_prometheus_fastapi_instrumentator_installed

def bootstrap(self) -> None:
Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument(
Expand All @@ -101,6 +111,8 @@ def bootstrap(self) -> None:


class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]):
__slots__ = "bootstrap_config", "instruments"

instruments_types: typing.ClassVar = [
FastAPIOpenTelemetryInstrument,
FastAPISentryInstrument,
Expand All @@ -109,7 +121,10 @@ class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]):
FastAPIPrometheusInstrument,
]
bootstrap_config: FastAPIConfig
__slots__ = "bootstrap_config", "instruments"
not_ready_message = "fastapi is not installed"

def is_ready(self) -> bool:
return import_checker.is_fastapi_installed

def _prepare_application(self) -> fastapi.FastAPI:
return self.bootstrap_config.application
31 changes: 25 additions & 6 deletions lite_bootstrap/bootstrappers/faststream_bootstrapper.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import annotations
import contextlib
import dataclasses
import json
import typing

from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksConfig, HealthChecksInstrument
from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument
Expand All @@ -12,12 +12,16 @@
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument


with contextlib.suppress(ImportError):
if import_checker.is_faststream_installed:
import faststream
import prometheus_client
from faststream.asgi import AsgiFastStream, AsgiResponse
from faststream.asgi import get as handle_get
from faststream.broker.core.usecase import BrokerUsecase

if import_checker.is_prometheus_client_installed:
import prometheus_client

if import_checker.is_opentelemetry_installed:
from opentelemetry.metrics import Meter, MeterProvider
from opentelemetry.trace import TracerProvider, get_tracer_provider

Expand Down Expand Up @@ -87,9 +91,10 @@ class FastStreamLoggingInstrument(LoggingInstrument):
@dataclasses.dataclass(kw_only=True, frozen=True)
class FastStreamOpenTelemetryInstrument(OpenTelemetryInstrument):
bootstrap_config: FastStreamConfig
not_ready_message = OpenTelemetryInstrument.not_ready_message + " or opentelemetry_middleware_cls is empty"

def is_ready(self) -> bool:
return bool(self.bootstrap_config.opentelemetry_middleware_cls and super().is_ready())
return super().is_ready() and bool(self.bootstrap_config.opentelemetry_middleware_cls)

def bootstrap(self) -> None:
if self.bootstrap_config.opentelemetry_middleware_cls and self.bootstrap_config.application.broker:
Expand All @@ -109,9 +114,18 @@ class FastStreamPrometheusInstrument(PrometheusInstrument):
collector_registry: prometheus_client.CollectorRegistry = dataclasses.field(
default_factory=prometheus_client.CollectorRegistry, init=False
)
not_ready_message = (
PrometheusInstrument.not_ready_message
+ " or prometheus_middleware_cls is missing or prometheus_client is not installed"
)

def is_ready(self) -> bool:
return bool(self.bootstrap_config.prometheus_middleware_cls and super().is_ready())
return (
super().is_ready()
and import_checker.is_prometheus_client_installed
and bool(self.bootstrap_config.prometheus_middleware_cls)
and import_checker.is_prometheus_client_installed
)

def bootstrap(self) -> None:
self.bootstrap_config.application.mount(
Expand All @@ -124,6 +138,8 @@ def bootstrap(self) -> None:


class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]):
__slots__ = "bootstrap_config", "instruments"

instruments_types: typing.ClassVar = [
FastStreamOpenTelemetryInstrument,
FastStreamSentryInstrument,
Expand All @@ -132,7 +148,10 @@ class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]):
FastStreamPrometheusInstrument,
]
bootstrap_config: FastStreamConfig
__slots__ = "bootstrap_config", "instruments"
not_ready_message = "faststream is not installed"

def is_ready(self) -> bool:
return import_checker.is_faststream_installed

def __init__(self, bootstrap_config: FastStreamConfig) -> None:
super().__init__(bootstrap_config)
Expand Down
7 changes: 6 additions & 1 deletion lite_bootstrap/bootstrappers/free_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ class FreeBootstrapperConfig(LoggingConfig, OpentelemetryConfig, SentryConfig):


class FreeBootstrapper(BaseBootstrapper[None]):
__slots__ = "bootstrap_config", "instruments"

instruments_types: typing.ClassVar = [
OpenTelemetryInstrument,
SentryInstrument,
LoggingInstrument,
]
bootstrap_config: FreeBootstrapperConfig
__slots__ = "bootstrap_config", "instruments"
not_ready_message = ""

def is_ready(self) -> bool:
return True

def __init__(self, bootstrap_config: FreeBootstrapperConfig) -> None:
super().__init__(bootstrap_config)
Expand Down
24 changes: 17 additions & 7 deletions lite_bootstrap/bootstrappers/litestar_bootstrapper.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import contextlib
import dataclasses
import typing

from litestar.plugins.prometheus import PrometheusConfig, PrometheusController

from lite_bootstrap import import_checker
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
from lite_bootstrap.instruments.healthchecks_instrument import (
HealthChecksConfig,
Expand All @@ -21,18 +19,21 @@
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument


with contextlib.suppress(ImportError):
if import_checker.is_litestar_installed:
import litestar
from litestar.config.app import AppConfig
from litestar.contrib.opentelemetry import OpenTelemetryConfig
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController

if import_checker.is_opentelemetry_installed:
from opentelemetry.trace import get_tracer_provider


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LitestarConfig(
HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusBootstrapperConfig, SentryConfig
):
application_config: AppConfig = dataclasses.field(default_factory=AppConfig)
application_config: "AppConfig" = dataclasses.field(default_factory=AppConfig)
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)

Expand All @@ -41,7 +42,7 @@ class LitestarConfig(
class LitestarHealthChecksInstrument(HealthChecksInstrument):
bootstrap_config: LitestarConfig

def build_litestar_health_check_router(self) -> litestar.Router:
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()
Expand Down Expand Up @@ -91,6 +92,10 @@ class LitestarSentryInstrument(SentryInstrument):
@dataclasses.dataclass(kw_only=True, frozen=True)
class LitestarPrometheusInstrument(PrometheusInstrument):
bootstrap_config: LitestarConfig
not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_client is not installed"

def is_ready(self) -> bool:
return super().is_ready() and import_checker.is_prometheus_client_installed

def bootstrap(self) -> None:
class LitestarPrometheusController(PrometheusController):
Expand All @@ -108,6 +113,8 @@ class LitestarPrometheusController(PrometheusController):


class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]):
__slots__ = "bootstrap_config", "instruments"

instruments_types: typing.ClassVar = [
LitestarOpenTelemetryInstrument,
LitestarSentryInstrument,
Expand All @@ -116,7 +123,10 @@ class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]):
LitestarPrometheusInstrument,
]
bootstrap_config: LitestarConfig
__slots__ = "bootstrap_config", "instruments"
not_ready_message = "litestar is not installed"

def is_ready(self) -> bool:
return import_checker.is_litestar_installed

def __init__(self, bootstrap_config: LitestarConfig) -> None:
super().__init__(bootstrap_config)
Expand Down
11 changes: 11 additions & 0 deletions lite_bootstrap/import_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from importlib.util import find_spec


is_opentelemetry_installed = find_spec("opentelemetry") is not None
is_sentry_installed = find_spec("sentry_sdk") is not None
is_structlog_installed = find_spec("structlog") is not None
is_prometheus_client_installed = find_spec("prometheus_client") is not None
is_fastapi_installed = find_spec("fastapi") is not None
is_litestar_installed = find_spec("litestar") is not None
is_faststream_installed = find_spec("faststream") is not None
is_prometheus_fastapi_instrumentator_installed = find_spec("prometheus_fastapi_instrumentator") is not None
4 changes: 4 additions & 0 deletions lite_bootstrap/instruments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class BaseConfig:
class BaseInstrument(abc.ABC):
bootstrap_config: BaseConfig

@property
@abc.abstractmethod
def not_ready_message(self) -> str: ...

def bootstrap(self) -> None: ... # noqa: B027

def teardown(self) -> None: ... # noqa: B027
Expand Down
1 change: 1 addition & 0 deletions lite_bootstrap/instruments/healthchecks_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class HealthChecksConfig(BaseConfig):
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class HealthChecksInstrument(BaseInstrument):
bootstrap_config: HealthChecksConfig
not_ready_message = "health_checks_enabled is False"

def is_ready(self) -> bool:
return self.bootstrap_config.health_checks_enabled
Expand Down
Loading