Skip to content

Commit 26444f3

Browse files
authored
Merge branch 'main' into tnorth/fix
2 parents 10f6c81 + ccea42c commit 26444f3

File tree

19 files changed

+983
-266
lines changed

19 files changed

+983
-266
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
([#3884](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3884))
2828
- `opentelemetry-instrumentation-aiohttp-server`: add support for custom header captures via `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`
2929
([#3916](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3916))
30+
- `opentelemetry-instrumentation-redis`: add support for `suppress_instrumentation` context manager for both sync and async Redis clients and pipelines
3031

3132
### Fixed
3233

@@ -50,6 +51,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5051
([#3904](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3904))
5152
- build: bump ruff to 0.14.1
5253
([#3842](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3842))
54+
- `opentelemetry-instrumentation-redis`: Add default span name for pipeline operations
55+
([#3941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3941))
56+
- `opentelemetry-instrumentation-pymongo`: Fix invalid mongodb collection attribute type
57+
([#3942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3942))
58+
- `opentelemetry-instrumentation-aiohttp-client`: Fix metric attribute leakage
59+
([#3936](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3936))
60+
- `opentelemetry-instrumentation-aiohttp-client`: Update instrumentor to respect suppressing http instrumentation
61+
([#3957](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3957))
5362

5463
## Version 1.38.0/0.59b0 (2025-10-16)
5564

instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def response_hook(span: Span, params: typing.Union[
137137
from opentelemetry.instrumentation.aiohttp_client.version import __version__
138138
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
139139
from opentelemetry.instrumentation.utils import (
140-
is_instrumentation_enabled,
140+
is_http_instrumentation_enabled,
141141
unwrap,
142142
)
143143
from opentelemetry.metrics import MeterProvider, get_meter
@@ -287,8 +287,6 @@ def create_trace_config(
287287
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
288288
)
289289

290-
metric_attributes = {}
291-
292290
excluded_urls = get_excluded_urls("AIOHTTP_CLIENT")
293291

294292
def _end_trace(trace_config_ctx: types.SimpleNamespace):
@@ -299,7 +297,7 @@ def _end_trace(trace_config_ctx: types.SimpleNamespace):
299297

300298
if trace_config_ctx.duration_histogram_old is not None:
301299
duration_attrs_old = _filter_semconv_duration_attrs(
302-
metric_attributes,
300+
trace_config_ctx.metric_attributes,
303301
_client_duration_attrs_old,
304302
_client_duration_attrs_new,
305303
_StabilityMode.DEFAULT,
@@ -310,7 +308,7 @@ def _end_trace(trace_config_ctx: types.SimpleNamespace):
310308
)
311309
if trace_config_ctx.duration_histogram_new is not None:
312310
duration_attrs_new = _filter_semconv_duration_attrs(
313-
metric_attributes,
311+
trace_config_ctx.metric_attributes,
314312
_client_duration_attrs_old,
315313
_client_duration_attrs_new,
316314
_StabilityMode.HTTP,
@@ -325,7 +323,7 @@ async def on_request_start(
325323
params: aiohttp.TraceRequestStartParams,
326324
):
327325
if (
328-
not is_instrumentation_enabled()
326+
not is_http_instrumentation_enabled()
329327
or trace_config_ctx.excluded_urls.url_disabled(str(params.url))
330328
):
331329
trace_config_ctx.span = None
@@ -348,7 +346,7 @@ async def on_request_start(
348346
sem_conv_opt_in_mode,
349347
)
350348
_set_http_method(
351-
metric_attributes,
349+
trace_config_ctx.metric_attributes,
352350
method,
353351
sanitize_method(method),
354352
sem_conv_opt_in_mode,
@@ -359,12 +357,12 @@ async def on_request_start(
359357
parsed_url = urlparse(request_url)
360358
if parsed_url.hostname:
361359
_set_http_host_client(
362-
metric_attributes,
360+
trace_config_ctx.metric_attributes,
363361
parsed_url.hostname,
364362
sem_conv_opt_in_mode,
365363
)
366364
_set_http_net_peer_name_client(
367-
metric_attributes,
365+
trace_config_ctx.metric_attributes,
368366
parsed_url.hostname,
369367
sem_conv_opt_in_mode,
370368
)
@@ -376,7 +374,9 @@ async def on_request_start(
376374
)
377375
if parsed_url.port:
378376
_set_http_peer_port_client(
379-
metric_attributes, parsed_url.port, sem_conv_opt_in_mode
377+
trace_config_ctx.metric_attributes,
378+
parsed_url.port,
379+
sem_conv_opt_in_mode,
380380
)
381381
if _report_new(sem_conv_opt_in_mode):
382382
_set_http_peer_port_client(
@@ -411,7 +411,7 @@ async def on_request_end(
411411
_set_http_status_code_attribute(
412412
trace_config_ctx.span,
413413
params.response.status,
414-
metric_attributes,
414+
trace_config_ctx.metric_attributes,
415415
sem_conv_opt_in_mode,
416416
)
417417

@@ -429,7 +429,7 @@ async def on_request_exception(
429429
exc_type = type(params.exception).__qualname__
430430
if _report_new(sem_conv_opt_in_mode):
431431
trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type)
432-
metric_attributes[ERROR_TYPE] = exc_type
432+
trace_config_ctx.metric_attributes[ERROR_TYPE] = exc_type
433433

434434
trace_config_ctx.span.set_status(
435435
Status(StatusCode.ERROR, exc_type)
@@ -450,6 +450,7 @@ def _trace_config_ctx_factory(**kwargs):
450450
duration_histogram_old=duration_histogram_old,
451451
duration_histogram_new=duration_histogram_new,
452452
excluded_urls=excluded_urls,
453+
metric_attributes={},
453454
**kwargs,
454455
)
455456

@@ -485,9 +486,6 @@ def _instrument(
485486

486487
# pylint:disable=unused-argument
487488
def instrumented_init(wrapped, instance, args, kwargs):
488-
if not is_instrumentation_enabled():
489-
return wrapped(*args, **kwargs)
490-
491489
client_trace_configs = list(kwargs.get("trace_configs") or [])
492490
client_trace_configs.extend(trace_configs)
493491

instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
from opentelemetry.instrumentation.aiohttp_client import (
4141
AioHttpClientInstrumentor,
4242
)
43-
from opentelemetry.instrumentation.utils import suppress_instrumentation
43+
from opentelemetry.instrumentation.utils import (
44+
suppress_http_instrumentation,
45+
suppress_instrumentation,
46+
)
4447
from opentelemetry.semconv._incubating.attributes.http_attributes import (
4548
HTTP_HOST,
4649
HTTP_METHOD,
@@ -828,6 +831,56 @@ async def request_handler(request):
828831
self._assert_spans([], 0)
829832
self._assert_metrics(0)
830833

834+
def test_metric_attributes_isolation(self):
835+
async def success_handler(request):
836+
assert "traceparent" in request.headers
837+
return aiohttp.web.Response(status=HTTPStatus.OK)
838+
839+
async def timeout_handler(request):
840+
await asyncio.sleep(60)
841+
assert "traceparent" in request.headers
842+
return aiohttp.web.Response(status=HTTPStatus.OK)
843+
844+
trace_config: aiohttp.TraceConfig = (
845+
aiohttp_client.create_trace_config()
846+
)
847+
848+
success_host, success_port = self._http_request(
849+
trace_config=trace_config,
850+
url="/success",
851+
request_handler=success_handler,
852+
)
853+
854+
timeout_host, timeout_port = self._http_request(
855+
trace_config=trace_config,
856+
url="/timeout",
857+
request_handler=timeout_handler,
858+
timeout=aiohttp.ClientTimeout(sock_read=0.01),
859+
)
860+
861+
metrics = self._assert_metrics(1)
862+
duration_dp_attributes = [
863+
dict(dp.attributes) for dp in metrics[0].data.data_points
864+
]
865+
self.assertEqual(
866+
[
867+
{
868+
HTTP_METHOD: "GET",
869+
HTTP_HOST: success_host,
870+
HTTP_STATUS_CODE: int(HTTPStatus.OK),
871+
NET_PEER_NAME: success_host,
872+
NET_PEER_PORT: success_port,
873+
},
874+
{
875+
HTTP_METHOD: "GET",
876+
HTTP_HOST: timeout_host,
877+
NET_PEER_NAME: timeout_host,
878+
NET_PEER_PORT: timeout_port,
879+
},
880+
],
881+
duration_dp_attributes,
882+
)
883+
831884

832885
class TestAioHttpClientInstrumentor(TestBase):
833886
URL = "/test-path"
@@ -1068,33 +1121,60 @@ async def uninstrument_request(server: aiohttp.test_utils.TestServer):
10681121
self._assert_spans(1)
10691122

10701123
def test_suppress_instrumentation(self):
1071-
with suppress_instrumentation():
1072-
run_with_test_server(
1073-
self.get_default_request(), self.URL, self.default_handler
1074-
)
1075-
self._assert_spans(0)
1124+
for suppress_ctx in (
1125+
suppress_instrumentation,
1126+
suppress_http_instrumentation,
1127+
):
1128+
with self.subTest(suppress_ctx=suppress_ctx.__name__):
1129+
with suppress_ctx():
1130+
run_with_test_server(
1131+
self.get_default_request(),
1132+
self.URL,
1133+
self.default_handler,
1134+
)
1135+
self._assert_spans(0)
1136+
self._assert_metrics(0)
10761137

10771138
@staticmethod
1078-
async def suppressed_request(server: aiohttp.test_utils.TestServer):
1079-
async with aiohttp.test_utils.TestClient(server) as client:
1080-
with suppress_instrumentation():
1081-
await client.get(TestAioHttpClientInstrumentor.URL)
1139+
def make_suppressed_request(suppress_ctx):
1140+
async def suppressed_request(server: aiohttp.test_utils.TestServer):
1141+
async with aiohttp.test_utils.TestClient(server) as client:
1142+
with suppress_ctx():
1143+
await client.get(TestAioHttpClientInstrumentor.URL)
1144+
1145+
return suppressed_request
10821146

10831147
def test_suppress_instrumentation_after_creation(self):
1084-
run_with_test_server(
1085-
self.suppressed_request, self.URL, self.default_handler
1086-
)
1087-
self._assert_spans(0)
1148+
for suppress_ctx in (
1149+
suppress_instrumentation,
1150+
suppress_http_instrumentation,
1151+
):
1152+
with self.subTest(suppress_ctx=suppress_ctx.__name__):
1153+
run_with_test_server(
1154+
self.make_suppressed_request(suppress_ctx),
1155+
self.URL,
1156+
self.default_handler,
1157+
)
1158+
self._assert_spans(0)
1159+
self._assert_metrics(0)
10881160

10891161
def test_suppress_instrumentation_with_server_exception(self):
10901162
# pylint:disable=unused-argument
10911163
async def raising_handler(request):
10921164
raise aiohttp.web.HTTPFound(location=self.URL)
10931165

1094-
run_with_test_server(
1095-
self.suppressed_request, self.URL, raising_handler
1096-
)
1097-
self._assert_spans(0)
1166+
for suppress_ctx in (
1167+
suppress_instrumentation,
1168+
suppress_http_instrumentation,
1169+
):
1170+
with self.subTest(suppress_ctx=suppress_ctx.__name__):
1171+
run_with_test_server(
1172+
self.make_suppressed_request(suppress_ctx),
1173+
self.URL,
1174+
raising_handler,
1175+
)
1176+
self._assert_spans(0)
1177+
self._assert_metrics(0)
10981178

10991179
def test_url_filter(self):
11001180
def strip_query_params(url: yarl.URL) -> str:

instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def started(self, event: monitoring.CommandStartedEvent):
138138
command_name = event.command_name
139139
span_name = f"{event.database_name}.{command_name}"
140140
statement = self._get_statement_by_command_name(command_name, event)
141-
collection = event.command.get(event.command_name)
141+
collection = _get_command_collection_name(event)
142142

143143
try:
144144
span = self._tracer.start_span(span_name, kind=SpanKind.CLIENT)
@@ -226,6 +226,13 @@ def _get_statement_by_command_name(
226226
return statement
227227

228228

229+
def _get_command_collection_name(event: CommandEvent) -> str | None:
230+
collection_name = event.command.get(event.command_name)
231+
if not collection_name or not isinstance(collection_name, str):
232+
return None
233+
return collection_name
234+
235+
229236
def _get_span_dict_key(
230237
event: CommandEvent,
231238
) -> int | tuple[int, tuple[str, int | None]]:

instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,43 @@ def test_capture_statement_disabled_aggregate(self):
278278
span.attributes[SpanAttributes.DB_STATEMENT], "aggregate"
279279
)
280280

281+
def test_collection_name_attribute(self):
282+
scenarios = [
283+
(
284+
{
285+
"command_name": "find",
286+
"find": "test_collection",
287+
},
288+
"test_collection",
289+
),
290+
({"command_name": "find"}, None),
291+
({"command_name": "find", "find": b"invalid"}, None),
292+
]
293+
for command_attrs, expected in scenarios:
294+
with self.subTest(command_attrs=command_attrs, expected=expected):
295+
mock_event = MockEvent(command_attrs)
296+
297+
command_tracer = CommandTracer(
298+
self.tracer, capture_statement=True
299+
)
300+
command_tracer.started(event=mock_event)
301+
command_tracer.succeeded(event=mock_event)
302+
303+
spans_list = self.memory_exporter.get_finished_spans()
304+
305+
self.assertEqual(len(spans_list), 1)
306+
span = spans_list[0]
307+
308+
self.assertEqual(
309+
span.attributes[SpanAttributes.DB_STATEMENT], "find"
310+
)
311+
312+
self.assertEqual(
313+
span.attributes.get(SpanAttributes.DB_MONGODB_COLLECTION),
314+
expected,
315+
)
316+
self.memory_exporter.clear()
317+
281318

282319
class MockCommand:
283320
def __init__(self, command_attrs):

0 commit comments

Comments
 (0)