Skip to content

Commit 3e4e41d

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 3e4e41d

File tree

4 files changed

+109
-25
lines changed

4 files changed

+109
-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(f"Instrumentation skipped for library {entry_point.name}")
99+
continue
100+
101+
try:
102+
entry_point.load()().instrument(tracer_provider=self.tracer_provider)
103+
LOGGER_OBJ.debug(f"Instrumented {entry_point.name}")
104+
except DependencyConflictError as exc:
105+
LOGGER_OBJ.debug(f"Skipping instrumentation {entry_point.name}: {exc.conflict}")
106+
continue
107+
except ModuleNotFoundError as exc:
108+
LOGGER_OBJ.debug(f"Skipping instrumentation {entry_point.name}: {exc.msg}")
109+
continue
110+
except ImportError:
111+
LOGGER_OBJ.debug(f"Importing of {entry_point.name} failed, skipping it")
112+
continue
113+
except Exception:
114+
LOGGER_OBJ.debug(f"Instrumenting of {entry_point.name} failed")
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: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,54 @@
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
12+
from opentelemetry.instrumentation.environment_variables import OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
1113

1214
from microbootstrap import OpentelemetryConfig
1315
from microbootstrap.bootstrappers.fastapi import FastApiOpentelemetryInstrument
1416
from microbootstrap.bootstrappers.litestar import LitestarOpentelemetryInstrument
17+
from microbootstrap.instruments import opentelemetry_instrument
1518
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
1619

1720

1821
def test_opentelemetry_is_ready(
1922
minimal_opentelemetry_config: OpentelemetryConfig,
2023
) -> None:
21-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
22-
assert opentelemetry_instrument.is_ready()
24+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
25+
assert test_opentelemetry_instrument.is_ready()
2326

2427

2528
def test_opentelemetry_bootstrap_is_not_ready(minimal_opentelemetry_config: OpentelemetryConfig) -> None:
2629
minimal_opentelemetry_config.service_debug = False
2730
minimal_opentelemetry_config.opentelemetry_endpoint = None
28-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
29-
assert not opentelemetry_instrument.is_ready()
31+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
32+
assert not test_opentelemetry_instrument.is_ready()
3033

3134

3235
def test_opentelemetry_bootstrap_after(
3336
default_litestar_app: litestar.Litestar,
3437
minimal_opentelemetry_config: OpentelemetryConfig,
3538
) -> None:
36-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
37-
assert opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
39+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
40+
assert test_opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
3841

3942

4043
def test_opentelemetry_teardown(
4144
minimal_opentelemetry_config: OpentelemetryConfig,
4245
) -> None:
43-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
44-
assert opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
46+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
47+
assert test_opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
4548

4649

4750
def test_litestar_opentelemetry_bootstrap(
4851
minimal_opentelemetry_config: OpentelemetryConfig,
4952
magic_mock: MagicMock,
5053
) -> None:
5154
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
52-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
55+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
5356

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

5760
assert opentelemetry_bootstrap_result
5861
assert "middleware" in opentelemetry_bootstrap_result
@@ -66,18 +69,18 @@ def test_litestar_opentelemetry_teardown(
6669
magic_mock: MagicMock,
6770
) -> None:
6871
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
69-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
72+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
7073

71-
opentelemetry_instrument.teardown()
74+
test_opentelemetry_instrument.teardown()
7275

7376

7477
def test_litestar_opentelemetry_bootstrap_working(
7578
minimal_opentelemetry_config: OpentelemetryConfig,
7679
async_mock: AsyncMock,
7780
) -> None:
78-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
79-
opentelemetry_instrument.bootstrap()
80-
opentelemetry_bootstrap_result: typing.Final = opentelemetry_instrument.bootstrap_before()
81+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
82+
test_opentelemetry_instrument.bootstrap()
83+
opentelemetry_bootstrap_result: typing.Final = test_opentelemetry_instrument.bootstrap_before()
8184

8285
opentelemetry_middleware = opentelemetry_bootstrap_result["middleware"][0]
8386
assert isinstance(opentelemetry_middleware, DefineMiddleware)
@@ -104,9 +107,9 @@ def test_fastapi_opentelemetry_bootstrap_working(
104107
) -> None:
105108
monkeypatch.setattr("opentelemetry.sdk.trace.TracerProvider.shutdown", Mock())
106109

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

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