Skip to content

Commit fb922c1

Browse files
authored
Merge pull request #29 from modern-python/warnings-on-missing-deps-only
raise warnings only if dependencies are missing
2 parents 53243d5 + c09910a commit fb922c1

12 files changed

+82
-32
lines changed

lite_bootstrap/bootstrappers/base.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import abc
2+
import logging
23
import typing
34
import warnings
45

6+
import structlog
7+
58
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
69
from lite_bootstrap.types import ApplicationT
710

811

12+
try:
13+
logger = structlog.getLogger(__name__)
14+
except ImportError:
15+
logger = logging.getLogger(__name__)
16+
17+
918
InstrumentT = typing.TypeVar("InstrumentT", bound=BaseInstrument)
1019

1120

@@ -24,10 +33,15 @@ def __init__(self, bootstrap_config: BaseConfig) -> None:
2433
self.instruments = []
2534
for instrument_type in self.instruments_types:
2635
instrument = instrument_type(bootstrap_config=bootstrap_config)
27-
if instrument.is_ready():
28-
self.instruments.append(instrument)
29-
else:
30-
warnings.warn(instrument.not_ready_message, stacklevel=2)
36+
if not instrument.check_dependencies():
37+
warnings.warn(instrument.missing_dependency_message, stacklevel=2)
38+
continue
39+
40+
if not instrument.is_ready():
41+
logger.info(instrument.not_ready_message)
42+
continue
43+
44+
self.instruments.append(instrument)
3145

3246
@property
3347
@abc.abstractmethod

lite_bootstrap/bootstrappers/fastapi_bootstrapper.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,11 @@ class FastAPISentryInstrument(SentryInstrument):
116116
@dataclasses.dataclass(kw_only=True, frozen=True)
117117
class FastAPIPrometheusInstrument(PrometheusInstrument):
118118
bootstrap_config: FastAPIConfig
119-
not_ready_message = (
120-
PrometheusInstrument.not_ready_message + " or prometheus_fastapi_instrumentator is not installed"
121-
)
119+
missing_dependency_message = "prometheus_fastapi_instrumentator is not installed"
122120

123-
def is_ready(self) -> bool:
124-
return super().is_ready() and import_checker.is_prometheus_fastapi_instrumentator_installed
121+
@staticmethod
122+
def check_dependencies() -> bool:
123+
return import_checker.is_prometheus_fastapi_instrumentator_installed
125124

126125
def bootstrap(self) -> None:
127126
Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument(

lite_bootstrap/bootstrappers/faststream_bootstrapper.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,20 @@ class FastStreamPrometheusInstrument(PrometheusInstrument):
113113
collector_registry: "prometheus_client.CollectorRegistry" = dataclasses.field(
114114
default_factory=lambda: prometheus_client.CollectorRegistry(), init=False
115115
)
116-
not_ready_message = (
117-
PrometheusInstrument.not_ready_message
118-
+ " or prometheus_middleware_cls is missing or prometheus_client is not installed"
119-
)
116+
not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_middleware_cls is missing"
117+
missing_dependency_message = "prometheus_client is not installed"
120118

121119
def is_ready(self) -> bool:
122120
return (
123121
super().is_ready()
124122
and import_checker.is_prometheus_client_installed
125123
and bool(self.bootstrap_config.prometheus_middleware_cls)
126-
and import_checker.is_prometheus_client_installed
127124
)
128125

126+
@staticmethod
127+
def check_dependencies() -> bool:
128+
return import_checker.is_prometheus_client_installed
129+
129130
def bootstrap(self) -> None:
130131
self.bootstrap_config.application.mount(
131132
self.bootstrap_config.prometheus_metrics_path, prometheus_client.make_asgi_app(self.collector_registry)

lite_bootstrap/bootstrappers/free_bootstrapper.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ class FreeBootstrapper(BaseBootstrapper[None]):
1515
__slots__ = "bootstrap_config", "instruments"
1616

1717
instruments_types: typing.ClassVar = [
18-
OpenTelemetryInstrument,
19-
SentryInstrument,
2018
LoggingInstrument,
19+
SentryInstrument,
20+
OpenTelemetryInstrument,
2121
]
2222
bootstrap_config: FreeBootstrapperConfig
2323
not_ready_message = ""

lite_bootstrap/bootstrappers/litestar_bootstrapper.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,11 @@ class LitestarSentryInstrument(SentryInstrument):
121121
@dataclasses.dataclass(kw_only=True, frozen=True)
122122
class LitestarPrometheusInstrument(PrometheusInstrument):
123123
bootstrap_config: LitestarConfig
124-
not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_client is not installed"
124+
missing_dependency_message = "prometheus_client is not installed"
125125

126-
def is_ready(self) -> bool:
127-
return super().is_ready() and import_checker.is_prometheus_client_installed
126+
@staticmethod
127+
def check_dependencies() -> bool:
128+
return import_checker.is_prometheus_client_installed
128129

129130
def bootstrap(self) -> None:
130131
class LitestarPrometheusController(PrometheusController):

lite_bootstrap/instruments/base.py

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class BaseConfig:
1414
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
1515
class BaseInstrument(abc.ABC):
1616
bootstrap_config: BaseConfig
17+
missing_dependency_message = ""
1718

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

2627
@abc.abstractmethod
2728
def is_ready(self) -> bool: ...
29+
30+
@staticmethod
31+
def check_dependencies() -> bool:
32+
return True

lite_bootstrap/instruments/logging_instrument.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,16 @@ class LoggingConfig(BaseConfig):
9393
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
9494
class LoggingInstrument(BaseInstrument):
9595
bootstrap_config: LoggingConfig
96-
not_ready_message = "service_debug is True or structlog is not installed"
96+
not_ready_message = "service_debug is True"
97+
missing_dependency_message = "structlog is not installed"
9798

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

102+
@staticmethod
103+
def check_dependencies() -> bool:
104+
return import_checker.is_structlog_installed
105+
101106
def bootstrap(self) -> None:
102107
# Configure basic logging to allow structlog to catch its events
103108
logging.basicConfig(

lite_bootstrap/instruments/opentelemetry_instrument.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,16 @@ class OpentelemetryConfig(BaseConfig):
4040
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
4141
class OpenTelemetryInstrument(BaseInstrument):
4242
bootstrap_config: OpentelemetryConfig
43-
not_ready_message = "opentelemetry_endpoint is empty or opentelemetry is not installed"
43+
not_ready_message = "opentelemetry_endpoint is empty"
44+
missing_dependency_message = "opentelemetry is not installed"
4445

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

49+
@staticmethod
50+
def check_dependencies() -> bool:
51+
return import_checker.is_opentelemetry_installed
52+
4853
def bootstrap(self) -> None:
4954
attributes = {
5055
resources.SERVICE_NAME: self.bootstrap_config.service_name

lite_bootstrap/instruments/sentry_instrument.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ class SentryConfig(BaseConfig):
2929
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
3030
class SentryInstrument(BaseInstrument):
3131
bootstrap_config: SentryConfig
32-
not_ready_message = "sentry_dsn is empty or sentry_sdk is not installed"
32+
not_ready_message = "sentry_dsn is empty"
33+
missing_dependency_message = "sentry_sdk is not installed"
3334

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

38+
@staticmethod
39+
def check_dependencies() -> bool:
40+
return import_checker.is_sentry_installed
41+
3742
def bootstrap(self) -> None:
3843
sentry_sdk.init(
3944
dsn=self.bootstrap_config.sentry_dsn,

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,7 @@ asyncio_mode = "auto"
161161
asyncio_default_fixture_loop_scope = "function"
162162

163163
[tool.coverage.report]
164-
exclude_also = ["if typing.TYPE_CHECKING:"]
164+
exclude_also = [
165+
"if typing.TYPE_CHECKING:",
166+
"except ImportError:"
167+
]

tests/conftest.py

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import pytest
88
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
9+
from structlog.testing import capture_logs
10+
from structlog.typing import EventDict
911

1012
from lite_bootstrap import import_checker
1113

@@ -33,3 +35,9 @@ def emulate_package_missing(package_name: str) -> typing.Iterator[None]:
3335
finally:
3436
sys.modules[package_name] = old_module
3537
reload(import_checker)
38+
39+
40+
@pytest.fixture(name="log_output")
41+
def fixture_log_output() -> typing.Iterator[list[EventDict]]:
42+
with capture_logs() as cap_logs:
43+
yield cap_logs

tests/test_free_bootstrap.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import structlog
33
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
4+
from structlog.typing import EventDict
45

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

3233

33-
def test_free_bootstrap_logging_not_ready() -> None:
34-
with pytest.warns(UserWarning, match="service_debug is True or structlog is not installed"):
35-
FreeBootstrapper(
36-
bootstrap_config=FreeBootstrapperConfig(
37-
service_debug=True,
38-
opentelemetry_endpoint="otl",
39-
sentry_dsn="https://testdsn@localhost/1",
40-
),
41-
)
34+
def test_free_bootstrap_logging_not_ready(log_output: list[EventDict]) -> None:
35+
FreeBootstrapper(
36+
bootstrap_config=FreeBootstrapperConfig(
37+
service_debug=True,
38+
opentelemetry_endpoint="otl",
39+
opentelemetry_instrumentors=[CustomInstrumentor()],
40+
opentelemetry_span_exporter=ConsoleSpanExporter(),
41+
sentry_dsn="https://testdsn@localhost/1",
42+
logging_buffer_capacity=0,
43+
),
44+
)
45+
assert log_output == [{"event": "service_debug is True", "log_level": "info"}]
4246

4347

4448
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)