Skip to content

Commit dca3e86

Browse files
authored
Merge pull request #75 from modern-python/fix-teardown-otel-fastapi
fix teardown isolation, OTel provider init order, and FastAPI install…
2 parents 63cfd9d + bff0d24 commit dca3e86

4 files changed

Lines changed: 36 additions & 3 deletions

File tree

lite_bootstrap/bootstrappers/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,13 @@ def bootstrap(self) -> ApplicationT:
6161

6262
def teardown(self) -> None:
6363
self.is_bootstrapped = False
64-
for one_instrument in self.instruments:
65-
one_instrument.teardown()
64+
errors: list[BaseException] = []
65+
for one_instrument in reversed(self.instruments):
66+
try:
67+
one_instrument.teardown()
68+
except Exception as e: # noqa: BLE001, PERF203
69+
logger.warning(f"Error tearing down {type(one_instrument).__name__}: {e}")
70+
errors.append(e)
71+
if errors:
72+
msg = f"{len(errors)} instrument(s) failed during teardown"
73+
raise RuntimeError(msg) from errors[0]

lite_bootstrap/bootstrappers/fastapi_bootstrapper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class FastAPIConfig(
5454
prometheus_expose_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
5555

5656
def __post_init__(self) -> None:
57+
if not import_checker.is_fastapi_installed:
58+
msg = "fastapi is not installed"
59+
raise RuntimeError(msg)
60+
5761
if not self.application:
5862
object.__setattr__(
5963
self, "application", fastapi.FastAPI(docs_url=self.swagger_path, **self.application_kwargs)

lite_bootstrap/instruments/opentelemetry_instrument.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def bootstrap(self) -> None:
104104
attributes={k: v for k, v in attributes.items() if v},
105105
)
106106
tracer_provider = TracerProvider(resource=resource)
107+
set_tracer_provider(tracer_provider)
107108
if import_checker.is_pyroscope_installed and getattr(self.bootstrap_config, "pyroscope_endpoint", None):
108109
tracer_provider.add_span_processor(PyroscopeSpanProcessor())
109110
if self.bootstrap_config.opentelemetry_log_traces:
@@ -125,7 +126,6 @@ def bootstrap(self) -> None:
125126
)
126127
else:
127128
one_instrumentor.instrument(tracer_provider=tracer_provider)
128-
set_tracer_provider(tracer_provider)
129129

130130
def teardown(self) -> None:
131131
for one_instrumentor in self.bootstrap_config.opentelemetry_instrumentors:

tests/test_free_bootstrap.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest.mock import MagicMock
2+
13
import pytest
24
import structlog
35
from structlog.testing import capture_logs
@@ -47,6 +49,25 @@ def test_free_bootstrap_logging_not_ready() -> None:
4749
]
4850

4951

52+
def test_teardown_error_isolation(free_bootstrapper_config: FreeBootstrapperConfig) -> None:
53+
bootstrapper = FreeBootstrapper(bootstrap_config=free_bootstrapper_config)
54+
bootstrapper.bootstrap()
55+
56+
# Replace instruments with mocks: first raises, second succeeds.
57+
bad = MagicMock()
58+
bad.teardown.side_effect = RuntimeError("boom")
59+
good = MagicMock()
60+
bootstrapper.instruments = [bad, good]
61+
62+
with capture_logs() as cap_logs, pytest.raises(RuntimeError, match="1 instrument"):
63+
bootstrapper.teardown()
64+
65+
# Both instruments attempted teardown despite the error (LIFO: good first, bad second).
66+
good.teardown.assert_called_once()
67+
bad.teardown.assert_called_once()
68+
assert any("boom" in entry.get("event", "") for entry in cap_logs)
69+
70+
5071
@pytest.mark.parametrize(
5172
"package_name",
5273
[

0 commit comments

Comments
 (0)