Skip to content

Commit 7e0c064

Browse files
committed
Merge branch 'issue_539' of github.com:zqumei0/opentelemetry-python-contrib into 539
2 parents ad2fe81 + 9637086 commit 7e0c064

File tree

4 files changed

+100
-5
lines changed

4 files changed

+100
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
537537

538538
### Added
539539

540+
- Added `exclude urls` feature to HTTPX instrumentation
541+
([#1900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1900))
540542
- `opentelemetry-resource-detector-azure` Add resource detectors for Azure App Service and VM
541543
([#1901](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1901))
542544
- `opentelemetry-instrumentation-flask` Add support for Flask 3.0.0

instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ dependencies = [
3737
instruments = [
3838
"httpx >= 0.18.0",
3939
]
40+
test = [
41+
"opentelemetry-instrumentation-httpx[instruments]",
42+
"opentelemetry-sdk ~= 1.12",
43+
"opentelemetry-test-utils == 0.42b0.dev",
44+
"httpretty ~= 1.0",
45+
]
4046

4147
[project.entry-points.opentelemetry_instrumentor]
4248
httpx = "opentelemetry.instrumentation.httpx:HTTPXClientInstrumentor"

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,13 @@ async def async_response_hook(span, request, response):
234234
from opentelemetry.trace.span import Span
235235
from opentelemetry.trace.status import StatusCode
236236
from opentelemetry.util.http import remove_url_credentials, sanitize_method
237+
from opentelemetry.utils.http import (
238+
ExcludeList,
239+
get_excluded_urls,
240+
parse_excluded_urls,
241+
)
237242

243+
_excluded_urls_from_env = get_excluded_urls("HTTPX")
238244
_logger = logging.getLogger(__name__)
239245

240246
RequestHook = typing.Callable[[Span, "RequestInfo"], None]
@@ -411,6 +417,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
411417
right after the span is created
412418
response_hook: A hook that receives the span, request, and response
413419
that is called right before the span ends
420+
excluded_urls: List of urls that should be excluded from tracing
414421
"""
415422

416423
def __init__(
@@ -419,6 +426,7 @@ def __init__(
419426
tracer_provider: TracerProvider | None = None,
420427
request_hook: RequestHook | None = None,
421428
response_hook: ResponseHook | None = None,
429+
excluded_urls: ExcludeList | None = None,
422430
):
423431
_OpenTelemetrySemanticConventionStability._initialize()
424432
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
@@ -434,6 +442,7 @@ def __init__(
434442
)
435443
self._request_hook = request_hook
436444
self._response_hook = response_hook
445+
self._excluded_urls = excluded_urls
437446

438447
def __enter__(self) -> SyncOpenTelemetryTransport:
439448
self._transport.__enter__()
@@ -459,10 +468,13 @@ def handle_request(
459468
"""Add request info to span."""
460469
if not is_http_instrumentation_enabled():
461470
return self._transport.handle_request(*args, **kwargs)
462-
463471
method, url, headers, stream, extensions = _extract_parameters(
464472
args, kwargs
465473
)
474+
475+
if self._excluded_urls and self._excluded_urls.url_disabled(url):
476+
return self._transport.handle_request(*args, **kwargs)
477+
466478
method_original = method.decode()
467479
span_name = _get_default_span_name(method_original)
468480
span_attributes = {}
@@ -536,6 +548,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
536548
right after the span is created
537549
response_hook: A hook that receives the span, request, and response
538550
that is called right before the span ends
551+
excluded_urls: List of urls that should be excluded from tracing
539552
"""
540553

541554
def __init__(
@@ -544,6 +557,7 @@ def __init__(
544557
tracer_provider: TracerProvider | None = None,
545558
request_hook: AsyncRequestHook | None = None,
546559
response_hook: AsyncResponseHook | None = None,
560+
excluded_urls: ExcludeList | None = None,
547561
):
548562
_OpenTelemetrySemanticConventionStability._initialize()
549563
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
@@ -559,6 +573,7 @@ def __init__(
559573
)
560574
self._request_hook = request_hook
561575
self._response_hook = response_hook
576+
self._excluded_urls = excluded_urls
562577

563578
async def __aenter__(self) -> "AsyncOpenTelemetryTransport":
564579
await self._transport.__aenter__()
@@ -586,6 +601,13 @@ async def handle_async_request(
586601
method, url, headers, stream, extensions = _extract_parameters(
587602
args, kwargs
588603
)
604+
605+
if self._excluded_urls and self._excluded_urls.url_disabled(url):
606+
return await self._transport.handle_async_request(*args, **kwargs)
607+
608+
if context.get_value("suppress_instrumentation"):
609+
return await self._transport.handle_async_request(*args, **kwargs)
610+
589611
method_original = method.decode()
590612
span_name = _get_default_span_name(method_original)
591613
span_attributes = {}
@@ -674,6 +696,8 @@ def _instrument(self, **kwargs: typing.Any):
674696
and response that is called right before the span ends
675697
``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient``
676698
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
699+
``excluded_urls``: A string containing a comma-delimited
700+
list of regexes used to exclude URLs from tracking
677701
"""
678702
tracer_provider = kwargs.get("tracer_provider")
679703
request_hook = kwargs.get("request_hook")
@@ -690,6 +714,12 @@ def _instrument(self, **kwargs: typing.Any):
690714
if iscoroutinefunction(async_response_hook)
691715
else None
692716
)
717+
718+
excluded_urls = kwargs.get("excluded_urls")
719+
if excluded_urls is None:
720+
excluded_urls = _excluded_urls_from_env
721+
else:
722+
excluded_urls = parse_excluded_urls(excluded_urls)
693723

694724
_OpenTelemetrySemanticConventionStability._initialize()
695725
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
@@ -711,6 +741,7 @@ def _instrument(self, **kwargs: typing.Any):
711741
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
712742
request_hook=request_hook,
713743
response_hook=response_hook,
744+
excluded_urls=excluded_urls,
714745
),
715746
)
716747
wrap_function_wrapper(
@@ -722,6 +753,7 @@ def _instrument(self, **kwargs: typing.Any):
722753
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
723754
async_request_hook=async_request_hook,
724755
async_response_hook=async_response_hook,
756+
excluded_urls=excluded_urls,
725757
),
726758
)
727759

@@ -739,13 +771,18 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
739771
sem_conv_opt_in_mode: _StabilityMode,
740772
request_hook: RequestHook,
741773
response_hook: ResponseHook,
774+
excluded_urls: ExcludeList | None = None,
742775
):
743776
if not is_http_instrumentation_enabled():
744777
return wrapped(*args, **kwargs)
745778

746779
method, url, headers, stream, extensions = _extract_parameters(
747780
args, kwargs
748781
)
782+
783+
if excluded_urls and excluded_urls.url_disabled(url):
784+
return wrapeed(*args, **kwargs)
785+
749786
method_original = method.decode()
750787
span_name = _get_default_span_name(method_original)
751788
span_attributes = {}
@@ -813,13 +850,18 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
813850
sem_conv_opt_in_mode: _StabilityMode,
814851
async_request_hook: AsyncRequestHook,
815852
async_response_hook: AsyncResponseHook,
853+
excluded_urls: ExcludeList | None = None,
816854
):
817855
if not is_http_instrumentation_enabled():
818856
return await wrapped(*args, **kwargs)
819857

820858
method, url, headers, stream, extensions = _extract_parameters(
821859
args, kwargs
822860
)
861+
862+
if excluded_urls and excluded_urls.url_disabled(url):
863+
return await wrapeed(*args, **kwargs)
864+
823865
method_original = method.decode()
824866
span_name = _get_default_span_name(method_original)
825867
span_attributes = {}

instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import typing
2020
from unittest import mock
2121

22+
import httpretty
2223
import httpx
2324
import respx
2425
from wrapt import ObjectProxy
@@ -67,10 +68,10 @@
6768
ResponseHook,
6869
ResponseInfo,
6970
)
70-
from opentelemetry.sdk.trace.export import SpanExporter
71-
from opentelemetry.trace import TracerProvider
72-
from opentelemetry.trace.span import Span
73-
71+
from opentelemetry.sdk.trace.export import SpanExporter
72+
from opentelemetry.trace import TracerProvider
73+
from opentelemetry.trace.span import Span
74+
from opentelemetry.util.http import get_excluded_urls
7475

7576
HTTP_RESPONSE_BODY = "http.response.body"
7677

@@ -747,8 +748,22 @@ def get_transport_handler(self, transport):
747748

748749
def setUp(self):
749750
super().setUp()
751+
self.env_patch = mock.patch.dict(
752+
"os.environ",
753+
{
754+
"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg"
755+
},
756+
)
757+
self.env_patch.start()
758+
759+
self.exclude_patch = mock.patch(
760+
"opentelemetry.instrumentation.httpx._excluded_urls_from_env",
761+
get_excluded_urls("HTTPX"),
762+
)
763+
self.exclude_patch.start()
750764
self.client = self.create_client()
751765
HTTPXClientInstrumentor().instrument_client(self.client)
766+
self.env_patch.stop()
752767

753768
def tearDown(self):
754769
HTTPXClientInstrumentor().uninstrument()
@@ -955,6 +970,36 @@ def test_uninstrument(self):
955970
self.assertEqual(result_no_client.text, "Hello!")
956971
self.assert_span(num_spans=0)
957972

973+
def test_excluded_urls_explicit(self):
974+
url_404 = "http://mock/status/404"
975+
httpretty.register_uri(
976+
httpretty.GET,
977+
url_404,
978+
status=404,
979+
)
980+
981+
HTTPXClientInstrumentor().instrument(excluded_urls=".*/404")
982+
client = self.create_client()
983+
self.perform_request(self.URL)
984+
self.perform_request(url_404)
985+
986+
self.assert_span(num_spans=1)
987+
988+
def test_excluded_urls_from_env(self):
989+
url = "http://localhost/env_excluded_arg/123"
990+
httpretty.register_uri(
991+
httpretty.GET,
992+
url,
993+
status=200,
994+
)
995+
996+
HTTPXClientInstrumentor().instrument()
997+
client = self.create_client()
998+
self.perform_request(self.URL)
999+
self.perform_request(url)
1000+
1001+
self.assert_span(num_spans=1)
1002+
9581003
def test_uninstrument_client(self):
9591004
HTTPXClientInstrumentor().uninstrument_client(self.client)
9601005

0 commit comments

Comments
 (0)