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
1 change: 0 additions & 1 deletion docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion docs/integrations/faststream.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
1 change: 0 additions & 1 deletion docs/integrations/free.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
1 change: 0 additions & 1 deletion docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 28 additions & 3 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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),
)
```
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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)
```
6 changes: 6 additions & 0 deletions lite_bootstrap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from lite_bootstrap.exceptions import (
BootstrapperNotReadyError,
ConfigurationError,
InstrumentDependencyMissingWarning,
InstrumentNotReadyWarning,
InstrumentSkippedWarning,
LiteBootstrapError,
TeardownError,
)
Expand All @@ -20,6 +23,9 @@
"FastStreamConfig",
"FreeBootstrapper",
"FreeBootstrapperConfig",
"InstrumentDependencyMissingWarning",
"InstrumentNotReadyWarning",
"InstrumentSkippedWarning",
"LiteBootstrapError",
"LitestarBootstrapper",
"LitestarConfig",
Expand Down
37 changes: 26 additions & 11 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lite_bootstrap/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
16 changes: 10 additions & 6 deletions tests/test_free_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
Loading