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: