Skip to content

Commit da6e044

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 da6e044

File tree

4 files changed

+110
-25
lines changed

4 files changed

+110
-25
lines changed

microbootstrap/instruments/opentelemetry_instrument.py

Lines changed: 41 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 = structlog.get_logger(__name__)
25+
26+
2127
try:
2228
import pyroscope # type: ignore[import-untyped]
2329
except ImportError: # pragma: no cover
@@ -76,6 +82,38 @@ def _format_span(readable_span: ReadableSpan) -> str:
7682
return typing.cast("str", readable_span.to_json(indent=None)) + os.linesep
7783

7884

85+
def _load_instrumentors(tracer_provider: TracerProvider) -> None:
86+
package_to_exclude: typing.Final = [
87+
one_package_to_exclude.strip()
88+
for one_package_to_exclude in os.environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, "").split(",")
89+
]
90+
91+
for entry_point in entry_points(group="opentelemetry_instrumentor"):
92+
if entry_point.name in package_to_exclude:
93+
LOGGER_OBJ.info("Instrumentation skipped for library %s", entry_point.name)
94+
continue
95+
96+
try:
97+
entry_point.load()().instrument(tracer_provider=tracer_provider)
98+
LOGGER_OBJ.info("Instrumented %s", entry_point.name)
99+
except DependencyConflictError as exc:
100+
LOGGER_OBJ.warning(
101+
"Skipping instrumentation %s: %s",
102+
entry_point.name,
103+
exc.conflict,
104+
)
105+
continue
106+
except ModuleNotFoundError as exc:
107+
LOGGER_OBJ.warning("Skipping instrumentation %s: %s", entry_point.name, exc.msg)
108+
continue
109+
except ImportError:
110+
LOGGER_OBJ.warning("Importing of %s failed, skipping it", entry_point.name)
111+
continue
112+
except Exception:
113+
LOGGER_OBJ.warning("Instrumenting of %s failed", entry_point.name)
114+
raise
115+
116+
79117
class BaseOpentelemetryInstrument(Instrument[OpentelemetryConfigT]):
80118
instrument_name = "Opentelemetry"
81119
ready_condition = "Provide all necessary config parameters"
@@ -88,8 +126,6 @@ def teardown(self) -> None:
88126
instrumentor_with_params.instrumentor.uninstrument(**instrumentor_with_params.additional_params)
89127

90128
def bootstrap(self) -> None:
91-
auto_instrumentation.initialize()
92-
93129
attributes = {
94130
ResourceAttributes.SERVICE_NAME: self.instrument_config.opentelemetry_service_name
95131
or self.instrument_config.service_name,
@@ -123,6 +159,8 @@ def bootstrap(self) -> None:
123159
**opentelemetry_instrumentor.additional_params,
124160
)
125161
set_tracer_provider(self.tracer_provider)
162+
_load_instrumentors(tracer_provider=self.tracer_provider)
163+
set_tracer_provider(self.tracer_provider)
126164

127165

128166
class OpentelemetryInstrument(BaseOpentelemetryInstrument[OpentelemetryConfig]):

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: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import os
23
import typing
34
from unittest.mock import AsyncMock, MagicMock, Mock, patch
45

@@ -8,51 +9,54 @@
89
from fastapi.testclient import TestClient as FastAPITestClient
910
from litestar.middleware.base import DefineMiddleware
1011
from litestar.testing import TestClient as LitestarTestClient
12+
from opentelemetry.instrumentation.dependencies import DependencyConflictError
13+
from opentelemetry.instrumentation.environment_variables import OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
1114

1215
from microbootstrap import OpentelemetryConfig
1316
from microbootstrap.bootstrappers.fastapi import FastApiOpentelemetryInstrument
1417
from microbootstrap.bootstrappers.litestar import LitestarOpentelemetryInstrument
18+
from microbootstrap.instruments import opentelemetry_instrument
1519
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
1620

1721

1822
def test_opentelemetry_is_ready(
1923
minimal_opentelemetry_config: OpentelemetryConfig,
2024
) -> None:
21-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
22-
assert opentelemetry_instrument.is_ready()
25+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
26+
assert test_opentelemetry_instrument.is_ready()
2327

2428

2529
def test_opentelemetry_bootstrap_is_not_ready(minimal_opentelemetry_config: OpentelemetryConfig) -> None:
2630
minimal_opentelemetry_config.service_debug = False
2731
minimal_opentelemetry_config.opentelemetry_endpoint = None
28-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
29-
assert not opentelemetry_instrument.is_ready()
32+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
33+
assert not test_opentelemetry_instrument.is_ready()
3034

3135

3236
def test_opentelemetry_bootstrap_after(
3337
default_litestar_app: litestar.Litestar,
3438
minimal_opentelemetry_config: OpentelemetryConfig,
3539
) -> None:
36-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
37-
assert opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
40+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
41+
assert test_opentelemetry_instrument.bootstrap_after(default_litestar_app) == default_litestar_app
3842

3943

4044
def test_opentelemetry_teardown(
4145
minimal_opentelemetry_config: OpentelemetryConfig,
4246
) -> None:
43-
opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
44-
assert opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
47+
test_opentelemetry_instrument: typing.Final = OpentelemetryInstrument(minimal_opentelemetry_config)
48+
assert test_opentelemetry_instrument.teardown() is None # type: ignore[func-returns-value]
4549

4650

4751
def test_litestar_opentelemetry_bootstrap(
4852
minimal_opentelemetry_config: OpentelemetryConfig,
4953
magic_mock: MagicMock,
5054
) -> None:
5155
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
52-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
56+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
5357

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

5761
assert opentelemetry_bootstrap_result
5862
assert "middleware" in opentelemetry_bootstrap_result
@@ -66,18 +70,18 @@ def test_litestar_opentelemetry_teardown(
6670
magic_mock: MagicMock,
6771
) -> None:
6872
minimal_opentelemetry_config.opentelemetry_instrumentors = [magic_mock]
69-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
73+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
7074

71-
opentelemetry_instrument.teardown()
75+
test_opentelemetry_instrument.teardown()
7276

7377

7478
def test_litestar_opentelemetry_bootstrap_working(
7579
minimal_opentelemetry_config: OpentelemetryConfig,
7680
async_mock: AsyncMock,
7781
) -> None:
78-
opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
79-
opentelemetry_instrument.bootstrap()
80-
opentelemetry_bootstrap_result: typing.Final = opentelemetry_instrument.bootstrap_before()
82+
test_opentelemetry_instrument: typing.Final = LitestarOpentelemetryInstrument(minimal_opentelemetry_config)
83+
test_opentelemetry_instrument.bootstrap()
84+
opentelemetry_bootstrap_result: typing.Final = test_opentelemetry_instrument.bootstrap_before()
8185

8286
opentelemetry_middleware = opentelemetry_bootstrap_result["middleware"][0]
8387
assert isinstance(opentelemetry_middleware, DefineMiddleware)
@@ -104,9 +108,9 @@ def test_fastapi_opentelemetry_bootstrap_working(
104108
) -> None:
105109
monkeypatch.setattr("opentelemetry.sdk.trace.TracerProvider.shutdown", Mock())
106110

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

111115
@fastapi_application.get("/test-handler")
112116
async def test_handler() -> None:
@@ -115,3 +119,47 @@ async def test_handler() -> None:
115119
with patch("opentelemetry.trace.use_span") as mock_capture_event:
116120
FastAPITestClient(app=fastapi_application).get("/test-handler")
117121
assert mock_capture_event.called
122+
123+
124+
@pytest.mark.parametrize(
125+
("instruments", "result"),
126+
[
127+
(
128+
[
129+
MagicMock(),
130+
MagicMock(load=MagicMock(side_effect=ImportError)),
131+
MagicMock(load=MagicMock(side_effect=DependencyConflictError("Hello"))),
132+
MagicMock(load=MagicMock(side_effect=ModuleNotFoundError)),
133+
],
134+
"ok",
135+
),
136+
(
137+
[
138+
MagicMock(load=MagicMock(side_effect=ValueError)),
139+
],
140+
"raise",
141+
),
142+
(
143+
[
144+
MagicMock(load=MagicMock(side_effect=ValueError)),
145+
],
146+
"exclude",
147+
),
148+
],
149+
)
150+
def test_instrumentator_loader(instruments: list[MagicMock], result: str, monkeypatch: pytest.MonkeyPatch) -> None:
151+
if result == "exclude":
152+
monkeypatch.setenv(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, "exclude_this,exclude_that")
153+
instruments[0].name = "exclude_this"
154+
monkeypatch.setattr(
155+
opentelemetry_instrument,
156+
"entry_points",
157+
MagicMock(return_value=[*instruments]),
158+
)
159+
160+
if result != "raise":
161+
opentelemetry_instrument._load_instrumentors(MagicMock()) # noqa: SLF001
162+
return
163+
164+
with pytest.raises(ValueError): # noqa: PT011
165+
opentelemetry_instrument._load_instrumentors(MagicMock()) # noqa: SLF001

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)