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
12 changes: 10 additions & 2 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,13 @@ def bootstrap(self) -> ApplicationT:

def teardown(self) -> None:
self.is_bootstrapped = False
for one_instrument in self.instruments:
one_instrument.teardown()
errors: list[BaseException] = []
for one_instrument in reversed(self.instruments):
try:
one_instrument.teardown()
except Exception as e: # noqa: BLE001, PERF203
logger.warning(f"Error tearing down {type(one_instrument).__name__}: {e}")
errors.append(e)
if errors:
msg = f"{len(errors)} instrument(s) failed during teardown"
raise RuntimeError(msg) from errors[0]
4 changes: 4 additions & 0 deletions lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class FastAPIConfig(
prometheus_expose_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)

def __post_init__(self) -> None:
if not import_checker.is_fastapi_installed:
msg = "fastapi is not installed"
raise RuntimeError(msg)

if not self.application:
object.__setattr__(
self, "application", fastapi.FastAPI(docs_url=self.swagger_path, **self.application_kwargs)
Expand Down
2 changes: 1 addition & 1 deletion lite_bootstrap/instruments/opentelemetry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def bootstrap(self) -> None:
attributes={k: v for k, v in attributes.items() if v},
)
tracer_provider = TracerProvider(resource=resource)
set_tracer_provider(tracer_provider)
if import_checker.is_pyroscope_installed and getattr(self.bootstrap_config, "pyroscope_endpoint", None):
tracer_provider.add_span_processor(PyroscopeSpanProcessor())
if self.bootstrap_config.opentelemetry_log_traces:
Expand All @@ -125,7 +126,6 @@ def bootstrap(self) -> None:
)
else:
one_instrumentor.instrument(tracer_provider=tracer_provider)
set_tracer_provider(tracer_provider)

def teardown(self) -> None:
for one_instrumentor in self.bootstrap_config.opentelemetry_instrumentors:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_free_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import MagicMock

import pytest
import structlog
from structlog.testing import capture_logs
Expand Down Expand Up @@ -47,6 +49,25 @@ def test_free_bootstrap_logging_not_ready() -> None:
]


def test_teardown_error_isolation(free_bootstrapper_config: FreeBootstrapperConfig) -> None:
bootstrapper = FreeBootstrapper(bootstrap_config=free_bootstrapper_config)
bootstrapper.bootstrap()

# Replace instruments with mocks: first raises, second succeeds.
bad = MagicMock()
bad.teardown.side_effect = RuntimeError("boom")
good = MagicMock()
bootstrapper.instruments = [bad, good]

with capture_logs() as cap_logs, pytest.raises(RuntimeError, match="1 instrument"):
bootstrapper.teardown()

# Both instruments attempted teardown despite the error (LIFO: good first, bad second).
good.teardown.assert_called_once()
bad.teardown.assert_called_once()
assert any("boom" in entry.get("event", "") for entry in cap_logs)


@pytest.mark.parametrize(
"package_name",
[
Expand Down
Loading