From 992b3dbbbdd3d3357a3ecce6853f1e9d72264b37 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 2 May 2026 20:05:04 +0300 Subject: [PATCH] feat: unify instrument skip feedback through warning subclasses Both bootstrap skip paths (missing dependency, not ready) now emit warnings.warn with distinct UserWarning subclasses (InstrumentDependencyMissingWarning, InstrumentNotReadyWarning, sharing InstrumentSkippedWarning base) so users can filter, capture, or escalate each independently. Previously not-ready skips went to logger.info and were silently dropped before the logging instrument finished bootstrapping. Also drop stale service_debug=False notes from the docs (logging is now gated by logging_enabled, default True). Co-Authored-By: Claude Opus 4.7 --- docs/integrations/fastapi.md | 1 - docs/integrations/faststream.md | 1 - docs/integrations/free.md | 1 - docs/integrations/litestar.md | 1 - docs/introduction/configuration.md | 31 ++++++++++++++++++++--- lite_bootstrap/__init__.py | 6 +++++ lite_bootstrap/bootstrappers/base.py | 37 +++++++++++++++++++--------- lite_bootstrap/exceptions.py | 12 +++++++++ tests/test_free_bootstrap.py | 16 +++++++----- 9 files changed, 82 insertions(+), 24 deletions(-) diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 737b086..b75892c 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -34,7 +34,6 @@ bootstrapper_config = FastAPIConfig( service_name="microservice", service_version="2.0.0", service_environment="test", - service_debug=False, cors_allowed_origins=["http://test"], health_checks_path="/custom-health/", opentelemetry_endpoint="otl", diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index c10fc08..3b67c6a 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -37,7 +37,6 @@ bootstrapper_config = FastStreamConfig( service_name="microservice", service_version="2.0.0", service_environment="test", - service_debug=False, opentelemetry_endpoint="otl", opentelemetry_middleware_cls=RedisTelemetryMiddleware, prometheus_metrics_path="/custom-metrics/", diff --git a/docs/integrations/free.md b/docs/integrations/free.md index 2a75b84..9656f0f 100644 --- a/docs/integrations/free.md +++ b/docs/integrations/free.md @@ -29,7 +29,6 @@ from lite_bootstrap import FreeBootstrapperConfig, FreeBootstrapper bootstrapper_config = FreeBootstrapperConfig( - service_debug=False, opentelemetry_endpoint="otl", sentry_dsn="https://testdsn@localhost/1", ) diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index e8ca88f..576c559 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -34,7 +34,6 @@ bootstrapper_config = LitestarConfig( service_name="microservice", service_version="2.0.0", service_environment="test", - service_debug=False, cors_allowed_origins=["http://test"], health_checks_path="/custom-health/", opentelemetry_endpoint="otl", diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index f15d768..109bc43 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -105,10 +105,11 @@ When OpenTelemetry is also enabled, a `PyroscopeSpanProcessor` is automatically ## Structlog -To bootstrap Structlog, you must set `service_debug` to False +Structlog is bootstrapped by default. To opt out, set `logging_enabled=False`. Additional parameters: +- `logging_enabled` - whether to configure structlog (default: `True`). - `logging_log_level` - `logging_flush_level` - `logging_buffer_capacity` @@ -121,7 +122,6 @@ import structlog from lite_bootstrap import FastAPIConfig config = FastAPIConfig( - service_debug=False, logging_time_stamper=structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=True), ) ``` @@ -157,7 +157,6 @@ import logging from lite_bootstrap import FastStreamConfig config = FastStreamConfig( - service_debug=False, logging_log_level=logging.INFO, # your application logs faststream_log_level=logging.WARNING, # broker "Received"/"Processed" messages (default) ) @@ -193,3 +192,29 @@ Additional params: - `health_checks_path` - `health_checks_include_in_schema` + +## Skipped instrument warnings + +When a bootstrapper is constructed, each registered instrument is checked. If it can't run, the instrument is skipped and a `UserWarning` subclass is emitted so the skip is visible at the call site: + +- `InstrumentDependencyMissingWarning` — the instrument's optional package is not installed (e.g. `[sentry]` extra missing). +- `InstrumentNotReadyWarning` — the instrument's required config is missing or disabled (e.g. `sentry_dsn` not set, `logging_enabled=False`, `pyroscope_endpoint` empty). +- `InstrumentSkippedWarning` — base class for both, useful if you want to filter every skip with one rule. + +Both go through Python's `warnings` module, so they show up in stderr by default and can be filtered, captured, or escalated like any other warning. Example — silence intentional opt-outs but keep dependency-missing warnings loud: + +```python +import warnings +from lite_bootstrap import InstrumentNotReadyWarning + +warnings.filterwarnings("ignore", category=InstrumentNotReadyWarning) +``` + +Or treat any skip as an error in CI: + +```python +import warnings +from lite_bootstrap import InstrumentSkippedWarning + +warnings.filterwarnings("error", category=InstrumentSkippedWarning) +``` diff --git a/lite_bootstrap/__init__.py b/lite_bootstrap/__init__.py index 42eb54f..1c6799c 100644 --- a/lite_bootstrap/__init__.py +++ b/lite_bootstrap/__init__.py @@ -5,6 +5,9 @@ from lite_bootstrap.exceptions import ( BootstrapperNotReadyError, ConfigurationError, + InstrumentDependencyMissingWarning, + InstrumentNotReadyWarning, + InstrumentSkippedWarning, LiteBootstrapError, TeardownError, ) @@ -20,6 +23,9 @@ "FastStreamConfig", "FreeBootstrapper", "FreeBootstrapperConfig", + "InstrumentDependencyMissingWarning", + "InstrumentNotReadyWarning", + "InstrumentSkippedWarning", "LiteBootstrapError", "LitestarBootstrapper", "LitestarConfig", diff --git a/lite_bootstrap/bootstrappers/base.py b/lite_bootstrap/bootstrappers/base.py index ca1b59d..1e49bba 100644 --- a/lite_bootstrap/bootstrappers/base.py +++ b/lite_bootstrap/bootstrappers/base.py @@ -3,7 +3,12 @@ import typing import warnings -from lite_bootstrap.exceptions import BootstrapperNotReadyError, TeardownError +from lite_bootstrap.exceptions import ( + BootstrapperNotReadyError, + InstrumentDependencyMissingWarning, + InstrumentNotReadyWarning, + TeardownError, +) from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument from lite_bootstrap.types import ApplicationT @@ -33,16 +38,26 @@ def __init__(self, bootstrap_config: BaseConfig) -> None: self.bootstrap_config = bootstrap_config self.instruments = [] for instrument_type in self.instruments_types: - instrument = instrument_type(bootstrap_config=bootstrap_config) - if not instrument.check_dependencies(): - warnings.warn(instrument.missing_dependency_message, stacklevel=2) - continue - - if not instrument.is_ready(): - logger.info(f"{instrument_type.__name__} is not ready: {instrument.not_ready_message}") - continue - - self.instruments.append(instrument) + if (instrument := self._register_or_skip(instrument_type)) is not None: + self.instruments.append(instrument) + + def _register_or_skip(self, instrument_type: type[BaseInstrument]) -> BaseInstrument | None: + instrument = instrument_type(bootstrap_config=self.bootstrap_config) + if not instrument.check_dependencies(): + warnings.warn( + instrument.missing_dependency_message, + category=InstrumentDependencyMissingWarning, + stacklevel=4, + ) + return None + if not instrument.is_ready(): + warnings.warn( + f"{instrument_type.__name__} is not ready: {instrument.not_ready_message}", + category=InstrumentNotReadyWarning, + stacklevel=4, + ) + return None + return instrument @property @abc.abstractmethod diff --git a/lite_bootstrap/exceptions.py b/lite_bootstrap/exceptions.py index 400688a..69db96e 100644 --- a/lite_bootstrap/exceptions.py +++ b/lite_bootstrap/exceptions.py @@ -17,3 +17,15 @@ def __init__(self, errors: list[tuple[str, BaseException]]) -> None: self.errors = errors details = "; ".join(f"{name}: {err}" for name, err in errors) super().__init__(f"{len(errors)} instrument(s) failed during teardown: {details}") + + +class InstrumentSkippedWarning(UserWarning): + """Base class for warnings emitted when an instrument is skipped during bootstrap.""" + + +class InstrumentDependencyMissingWarning(InstrumentSkippedWarning): + """Emitted when an instrument is skipped because its optional dependency is not installed.""" + + +class InstrumentNotReadyWarning(InstrumentSkippedWarning): + """Emitted when an instrument is skipped because its config indicates it should not run.""" diff --git a/tests/test_free_bootstrap.py b/tests/test_free_bootstrap.py index d9eb228..1fb1df4 100644 --- a/tests/test_free_bootstrap.py +++ b/tests/test_free_bootstrap.py @@ -4,7 +4,12 @@ import structlog from structlog.testing import capture_logs -from lite_bootstrap import FreeBootstrapper, FreeBootstrapperConfig, TeardownError +from lite_bootstrap import ( + FreeBootstrapper, + FreeBootstrapperConfig, + InstrumentNotReadyWarning, + TeardownError, +) from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing @@ -32,7 +37,7 @@ def test_free_bootstrap(free_bootstrapper_config: FreeBootstrapperConfig) -> Non def test_free_bootstrap_logging_disabled() -> None: - with capture_logs() as cap_logs: + with pytest.warns(InstrumentNotReadyWarning) as records: FreeBootstrapper( bootstrap_config=FreeBootstrapperConfig( logging_enabled=False, @@ -43,10 +48,9 @@ def test_free_bootstrap_logging_disabled() -> None: logging_buffer_capacity=0, ), ) - assert cap_logs == [ - {"event": "LoggingInstrument is not ready: logging_enabled is False", "log_level": "info"}, - {"event": "PyroscopeInstrument is not ready: pyroscope_endpoint is empty", "log_level": "info"}, - ] + messages = [str(r.message) for r in records] + assert "LoggingInstrument is not ready: logging_enabled is False" in messages + assert "PyroscopeInstrument is not ready: pyroscope_endpoint is empty" in messages def test_teardown_error_isolation(free_bootstrapper_config: FreeBootstrapperConfig) -> None: