Skip to content

Commit e51605a

Browse files
committed
instruments: open_telemetry: port auto_instrumentation
- auto_instrumentation.initialize() uses a distro for loading instrumentation. This overrides some needed defaults, that we use in our loader. E.g. it starts exporting telemetry somewhere undesirable. So these changes port the loader to our infrastructure.
1 parent 78333f0 commit e51605a

File tree

4 files changed

+108
-25
lines changed

4 files changed

+108
-25
lines changed

microbootstrap/instruments/opentelemetry_instrument.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
import typing
66

77
import pydantic
8+
import structlog
89
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
9-
from opentelemetry.instrumentation import auto_instrumentation
10+
from opentelemetry.instrumentation.dependencies import DependencyConflictError
11+
from opentelemetry.instrumentation.environment_variables import OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
1012
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined] # noqa: TC002
1113
from opentelemetry.sdk import resources
1214
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
1315
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
1416
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor
1517
from opentelemetry.semconv.resource import ResourceAttributes
1618
from opentelemetry.trace import format_span_id, set_tracer_provider
19+
from opentelemetry.util._importlib_metadata import entry_points
1720

1821
from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
1922

2023

24+
LOGGER_OBJ: typing.Final = structlog.get_logger(__name__)
25+
26+
2127
try:
2228
import pyroscope # type: ignore[import-untyped]
2329
except ImportError: # pragma: no cover
@@ -54,6 +60,12 @@ class OpentelemetryConfig(BaseInstrumentConfig):
5460
opentelemetry_insecure: bool = pydantic.Field(default=True)
5561
opentelemetry_instrumentors: list[OpenTelemetryInstrumentor] = pydantic.Field(default_factory=list)
5662
opentelemetry_exclude_urls: list[str] = pydantic.Field(default=["/metrics"])
63+
opentelemetry_disabled_instrumentations: list[str] = pydantic.Field(
64+
default=[
65+
one_package_to_exclude.strip()
66+
for one_package_to_exclude in os.environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, "").split(",")
67+
]
68+
)
5769

5870

5971
@typing.runtime_checkable
@@ -80,6 +92,28 @@ class BaseOpentelemetryInstrument(Instrument[OpentelemetryConfigT]):
8092
instrument_name = "Opentelemetry"
8193
ready_condition = "Provide all necessary config parameters"
8294

95+
def _load_instrumentors(self) -> None:
96+
for entry_point in entry_points(group="opentelemetry_instrumentor"):
97+
if entry_point.name in self.instrument_config.opentelemetry_disabled_instrumentations:
98+
LOGGER_OBJ.debug("Instrumentation skipped for library", entry_point_name=entry_point.name)
99+
continue
100+
101+
try:
102+
entry_point.load()().instrument(tracer_provider=self.tracer_provider)
103+
LOGGER_OBJ.debug("Instrumented", entry_point_name=entry_point.name)
104+
except DependencyConflictError as exc:
105+
LOGGER_OBJ.debug("Skipping instrumentation", entry_point_name=entry_point.name, reason=exc.conflict)
106+
continue
107+
except ModuleNotFoundError as exc:
108+
LOGGER_OBJ.debug("Skipping instrumentation", entry_point_name=entry_point.name, reason=exc.msg)
109+
continue
110+
except ImportError:
111+
LOGGER_OBJ.debug("Importing failed, skipping it", entry_point_name=entry_point.name)
112+
continue
113+
except Exception:
114+
LOGGER_OBJ.debug("Instrumenting failed", entry_point_name=entry_point.name)
115+
raise
116+
83117
def is_ready(self) -> bool:
84118
return bool(self.instrument_config.opentelemetry_endpoint) or self.instrument_config.service_debug
85119

@@ -88,8 +122,6 @@ def teardown(self) -> None:
88122
instrumentor_with_params.instrumentor.uninstrument(**instrumentor_with_params.additional_params)
89123

90124
def bootstrap(self) -> None:
91-
auto_instrumentation.initialize()
92-
93125
attributes = {
94126
ResourceAttributes.SERVICE_NAME: self.instrument_config.opentelemetry_service_name
95127
or self.instrument_config.service_name,
@@ -122,6 +154,7 @@ def bootstrap(self) -> None:
122154
tracer_provider=self.tracer_provider,
123155
**opentelemetry_instrumentor.additional_params,
124156
)
157+
self._load_instrumentors()
125158
set_tracer_provider(self.tracer_provider)
126159

127160

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,5 @@ def reset_reloaded_settings_module() -> typing.Iterator[None]:
130130

131131

132132
@pytest.fixture(autouse=True)
133-
def disable_auto_instrumentation(monkeypatch: pytest.MonkeyPatch) -> None:
134-
monkeypatch.setattr(opentelemetry_instrument, "auto_instrumentation", MagicMock())
133+
def patch_out_entry_points(monkeypatch: pytest.MonkeyPatch) -> None:
134+
monkeypatch.setattr(opentelemetry_instrument, "entry_points", MagicMock(retrun_value=[]))

tests/instruments/test_opentelemetry.py

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,53 @@
88
from fastapi.testclient import TestClient as FastAPITestClient
99
from litestar.middleware.base import DefineMiddleware
1010
from litestar.testing import TestClient as LitestarTestClient
11+
from opentelemetry.instrumentation.dependencies import DependencyConflictError
1112

1213
from microbootstrap import OpentelemetryConfig
1314
from microbootstrap.bootstrappers.fastapi import FastApiOpentelemetryInstrument
1415
from microbootstrap.bootstrappers.litestar import LitestarOpentelemetryInstrument
16+
from microbootstrap.instruments import opentelemetry_instrument
1517
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
1618

1719

1820
def test_opentelemetry_is_ready(
1921
minimal_opentelemetry_config: OpentelemetryConfig,
2022
) -> None:
21-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
22-
assert opentelemetry_instrument.is_ready()
23+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
24+
assert test_opentelemetry_instrument.is_ready()
2325

2426

2527
def test_opentelemetry_bootstrap_is_not_ready(minimal_opentelemetry_config: OpentelemetryConfig) -> None:
2628
minimal_opentelemetry_config.service_debug = False
2729
minimal_opentelemetry_config.opentelemetry_endpoint = None
28-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
29-
assert not opentelemetry_instrument.is_ready()
30+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
31+
assert not test_opentelemetry_instrument.is_ready()
3032

3133

3234
def test_opentelemetry_bootstrap_after(
3335
default_litestar_app: litestar.Litestar,
3436
minimal_opentelemetry_config: OpentelemetryConfig,
3537
) -> None:
36-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
37-
assert opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
38+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
39+
assert test_opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
3840

3941

4042
def test_opentelemetry_teardown(
4143
minimal_opentelemetry_config: OpentelemetryConfig,
4244
) -> None:
43-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
44-
assert opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
45+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
46+
assert test_opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
4547

4648

4749
def test_litestar_opentelemetry_bootstrap(
4850
minimal_opentelemetry_config: OpentelemetryConfig,
4951
magic_mock: MagicMock,
5052
) -> None:
5153
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
52-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
54+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
5355

54-
opentelemetry_instrument.bootstrap()
55-
opentelemetry_bootstrap_result: typing.Final = opentelemetry_instrument.bootstrap_before()
56+
test_opentelemetry_instrument.bootstrap()
57+
opentelemetry_bootstrap_result: typing.Final = test_opentelemetry_instrument.bootstrap_before()
5658

5759
assert opentelemetry_bootstrap_result
5860
assert "middleware" in opentelemetry_bootstrap_result
@@ -66,18 +68,18 @@ def test_litestar_opentelemetry_teardown(
6668
magic_mock: MagicMock,
6769
) -> None:
6870
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
69-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
71+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
7072

71-
opentelemetry_instrument.teardown()
73+
test_opentelemetry_instrument.teardown()
7274

7375

7476
def test_litestar_opentelemetry_bootstrap_working(
7577
minimal_opentelemetry_config: OpentelemetryConfig,
7678
async_mock: AsyncMock,
7779
) -> None:
78-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
79-
opentelemetry_instrument.bootstrap()
80-
opentelemetry_bootstrap_result: typing.Final = opentelemetry_instrument.bootstrap_before()
80+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
81+
test_opentelemetry_instrument.bootstrap()
82+
opentelemetry_bootstrap_result: typing.Final = test_opentelemetry_instrument.bootstrap_before()
8183

8284
opentelemetry_middleware = opentelemetry_bootstrap_result["middleware"][0]
8385
assert isinstance(opentelemetry_middleware, DefineMiddleware)
@@ -104,9 +106,9 @@ def test_fastapi_opentelemetry_bootstrap_working(
104106
) -> None:
105107
monkeypatch.setattr("opentelemetry.sdk.trace.TracerProvider.shutdown", Mock())
106108

107-
opentelemetry_instrument: typing.Final = FastApiOpentelemetryInstrument(minimal_opentelemetry_config)
108-
opentelemetry_instrument.bootstrap()
109-
fastapi_application: typing.Final = opentelemetry_instrument.bootstrap_after(fastapi.FastAPI())
109+
test_opentelemetry_instrument: typing.Final = FastApiOpentelemetryInstrument(minimal_opentelemetry_config)
110+
test_opentelemetry_instrument.bootstrap()
111+
fastapi_application: typing.Final = test_opentelemetry_instrument.bootstrap_after(fastapi.FastAPI())
110112

111113
@fastapi_application.get("/test-handler")
112114
async def test_handler() -> None:
@@ -115,3 +117,52 @@ async def test_handler() -> None:
115117
with patch("opentelemetry.trace.use_span") as mock_capture_event:
116118
FastAPITestClient(app=fastapi_application).get("/test-handler")
117119
assert mock_capture_event.called
120+
121+
122+
@pytest.mark.parametrize(
123+
("instruments", "result"),
124+
[
125+
(
126+
[
127+
MagicMock(),
128+
MagicMock(load=MagicMock(side_effect=ImportError)),
129+
MagicMock(load=MagicMock(side_effect=DependencyConflictError("Hello"))),
130+
MagicMock(load=MagicMock(side_effect=ModuleNotFoundError)),
131+
],
132+
"ok",
133+
),
134+
(
135+
[
136+
MagicMock(load=MagicMock(side_effect=ValueError)),
137+
],
138+
"raise",
139+
),
140+
(
141+
[
142+
MagicMock(load=MagicMock(side_effect=ValueError)),
143+
],
144+
"exclude",
145+
),
146+
],
147+
)
148+
def test_instrumentors_loader(
149+
minimal_opentelemetry_config: OpentelemetryConfig,
150+
instruments: list[MagicMock],
151+
result: str,
152+
monkeypatch: pytest.MonkeyPatch,
153+
) -> None:
154+
if result == "exclude":
155+
minimal_opentelemetry_config.opentelemetry_disabled_instrumentations = ["exclude_this", "exclude_that"]
156+
instruments[0].name = "exclude_this"
157+
monkeypatch.setattr(
158+
opentelemetry_instrument,
159+
"entry_points",
160+
MagicMock(return_value=[*instruments]),
161+
)
162+
163+
if result != "raise":
164+
opentelemetry_instrument.OpentelemetryInstrument(instrument_config=minimal_opentelemetry_config).bootstrap()
165+
return
166+
167+
with pytest.raises(ValueError): # noqa: PT011
168+
opentelemetry_instrument.OpentelemetryInstrument(instrument_config=minimal_opentelemetry_config).bootstrap()

tests/instruments/test_pyroscope.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def test_opentelemetry_includes_pyroscope_2(
5050
async def test_handler() -> None: ...
5151

5252
FastAPITestClient(app=fastapi_application).get("/test-handler")
53-
5453
assert (
5554
add_thread_tag_mock.mock_calls
5655
== remove_thread_tag_mock.mock_calls

0 commit comments

Comments
 (0)