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
22 changes: 18 additions & 4 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import abc
import logging
import typing
import warnings

import structlog

from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
from lite_bootstrap.types import ApplicationT


try:
logger = structlog.getLogger(__name__)
except ImportError:
logger = logging.getLogger(__name__)


InstrumentT = typing.TypeVar("InstrumentT", bound=BaseInstrument)


Expand All @@ -24,10 +33,15 @@ def __init__(self, bootstrap_config: BaseConfig) -> None:
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)
if not instrument.check_dependencies():
warnings.warn(instrument.missing_dependency_message, stacklevel=2)
continue

if not instrument.is_ready():
logger.info(instrument.not_ready_message)
continue

self.instruments.append(instrument)

@property
@abc.abstractmethod
Expand Down
9 changes: 4 additions & 5 deletions lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,11 @@ 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"
)
missing_dependency_message = "prometheus_fastapi_instrumentator is not installed"

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

def bootstrap(self) -> None:
Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument(
Expand Down
11 changes: 6 additions & 5 deletions lite_bootstrap/bootstrappers/faststream_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,20 @@ class FastStreamPrometheusInstrument(PrometheusInstrument):
collector_registry: "prometheus_client.CollectorRegistry" = dataclasses.field(
default_factory=lambda: prometheus_client.CollectorRegistry(), init=False
)
not_ready_message = (
PrometheusInstrument.not_ready_message
+ " or prometheus_middleware_cls is missing or prometheus_client is not installed"
)
not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_middleware_cls is missing"
missing_dependency_message = "prometheus_client is not installed"

def is_ready(self) -> bool:
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
)

@staticmethod
def check_dependencies() -> bool:
return import_checker.is_prometheus_client_installed

def bootstrap(self) -> None:
self.bootstrap_config.application.mount(
self.bootstrap_config.prometheus_metrics_path, prometheus_client.make_asgi_app(self.collector_registry)
Expand Down
4 changes: 2 additions & 2 deletions lite_bootstrap/bootstrappers/free_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class FreeBootstrapper(BaseBootstrapper[None]):
__slots__ = "bootstrap_config", "instruments"

instruments_types: typing.ClassVar = [
OpenTelemetryInstrument,
SentryInstrument,
LoggingInstrument,
SentryInstrument,
OpenTelemetryInstrument,
]
bootstrap_config: FreeBootstrapperConfig
not_ready_message = ""
Expand Down
7 changes: 4 additions & 3 deletions lite_bootstrap/bootstrappers/litestar_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ 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"
missing_dependency_message = "prometheus_client is not installed"

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

def bootstrap(self) -> None:
class LitestarPrometheusController(PrometheusController):
Expand Down
5 changes: 5 additions & 0 deletions lite_bootstrap/instruments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class BaseConfig:
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class BaseInstrument(abc.ABC):
bootstrap_config: BaseConfig
missing_dependency_message = ""

@property
@abc.abstractmethod
Expand All @@ -25,3 +26,7 @@ def teardown(self) -> None: ... # noqa: B027

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

@staticmethod
def check_dependencies() -> bool:
return True
7 changes: 6 additions & 1 deletion lite_bootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,16 @@ class LoggingConfig(BaseConfig):
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class LoggingInstrument(BaseInstrument):
bootstrap_config: LoggingConfig
not_ready_message = "service_debug is True or structlog is not installed"
not_ready_message = "service_debug is True"
missing_dependency_message = "structlog is not installed"

def is_ready(self) -> bool:
return not self.bootstrap_config.service_debug and import_checker.is_structlog_installed

@staticmethod
def check_dependencies() -> bool:
return import_checker.is_structlog_installed

def bootstrap(self) -> None:
# Configure basic logging to allow structlog to catch its events
logging.basicConfig(
Expand Down
7 changes: 6 additions & 1 deletion lite_bootstrap/instruments/opentelemetry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ class OpentelemetryConfig(BaseConfig):
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class OpenTelemetryInstrument(BaseInstrument):
bootstrap_config: OpentelemetryConfig
not_ready_message = "opentelemetry_endpoint is empty or opentelemetry is not installed"
not_ready_message = "opentelemetry_endpoint is empty"
missing_dependency_message = "opentelemetry is not installed"

def is_ready(self) -> bool:
return bool(self.bootstrap_config.opentelemetry_endpoint) and import_checker.is_opentelemetry_installed

@staticmethod
def check_dependencies() -> bool:
return import_checker.is_opentelemetry_installed

def bootstrap(self) -> None:
attributes = {
resources.SERVICE_NAME: self.bootstrap_config.service_name
Expand Down
7 changes: 6 additions & 1 deletion lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ class SentryConfig(BaseConfig):
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class SentryInstrument(BaseInstrument):
bootstrap_config: SentryConfig
not_ready_message = "sentry_dsn is empty or sentry_sdk is not installed"
not_ready_message = "sentry_dsn is empty"
missing_dependency_message = "sentry_sdk is not installed"

def is_ready(self) -> bool:
return bool(self.bootstrap_config.sentry_dsn) and import_checker.is_sentry_installed

@staticmethod
def check_dependencies() -> bool:
return import_checker.is_sentry_installed

def bootstrap(self) -> None:
sentry_sdk.init(
dsn=self.bootstrap_config.sentry_dsn,
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,7 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]
exclude_also = [
"if typing.TYPE_CHECKING:",
"except ImportError:"
]
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import pytest
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
from structlog.testing import capture_logs
from structlog.typing import EventDict

from lite_bootstrap import import_checker

Expand Down Expand Up @@ -33,3 +35,9 @@ def emulate_package_missing(package_name: str) -> typing.Iterator[None]:
finally:
sys.modules[package_name] = old_module
reload(import_checker)


@pytest.fixture(name="log_output")
def fixture_log_output() -> typing.Iterator[list[EventDict]]:
with capture_logs() as cap_logs:
yield cap_logs
22 changes: 13 additions & 9 deletions tests/test_free_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import structlog
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
from structlog.typing import EventDict

from lite_bootstrap import FreeBootstrapper, FreeBootstrapperConfig
from tests.conftest import CustomInstrumentor, emulate_package_missing
Expand Down Expand Up @@ -30,15 +31,18 @@ def test_free_bootstrap(free_bootstrapper_config: FreeBootstrapperConfig) -> Non
bootstrapper.teardown()


def test_free_bootstrap_logging_not_ready() -> None:
with pytest.warns(UserWarning, match="service_debug is True or structlog is not installed"):
FreeBootstrapper(
bootstrap_config=FreeBootstrapperConfig(
service_debug=True,
opentelemetry_endpoint="otl",
sentry_dsn="https://testdsn@localhost/1",
),
)
def test_free_bootstrap_logging_not_ready(log_output: list[EventDict]) -> None:
FreeBootstrapper(
bootstrap_config=FreeBootstrapperConfig(
service_debug=True,
opentelemetry_endpoint="otl",
opentelemetry_instrumentors=[CustomInstrumentor()],
opentelemetry_span_exporter=ConsoleSpanExporter(),
sentry_dsn="https://testdsn@localhost/1",
logging_buffer_capacity=0,
),
)
assert log_output == [{"event": "service_debug is True", "log_level": "info"}]


@pytest.mark.parametrize(
Expand Down
Loading