Skip to content

Commit 9637086

Browse files
committed
feat: Add exclude urls feature to HTTPX instrumentation
Issue: open-telemetry#539
1 parent e318c94 commit 9637086

File tree

4 files changed

+88
-8
lines changed

4 files changed

+88
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919

2020
### Added
2121

22+
- Added `exclude urls` feature to HTTPX instrumentation
23+
([#1900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1900))
2224
- `opentelemetry-resource-detector-azure` Add resource detectors for Azure App Service and VM
2325
([#1901](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1901))
2426

instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ test = [
3838
"opentelemetry-instrumentation-httpx[instruments]",
3939
"opentelemetry-sdk ~= 1.12",
4040
"opentelemetry-test-utils == 0.42b0.dev",
41+
"httpretty ~= 1.0",
4142
]
4243

4344
[project.entry-points.opentelemetry_instrumentor]

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

+34-4
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,13 @@ def response_hook(span, request, response):
176176
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
177177
from opentelemetry.trace.span import Span
178178
from opentelemetry.trace.status import Status
179+
from opentelemetry.utils.http import (
180+
ExcludeList,
181+
get_excluded_urls,
182+
parse_excluded_urls,
183+
)
179184

185+
_excluded_urls_from_env = get_excluded_urls("HTTPX")
180186
_logger = logging.getLogger(__name__)
181187

182188
URL = typing.Tuple[bytes, bytes, typing.Optional[int], bytes]
@@ -276,6 +282,7 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
276282
right after the span is created
277283
response_hook: A hook that receives the span, request, and response
278284
that is called right before the span ends
285+
excluded_urls: List of urls that should be excluded from tracing
279286
"""
280287

281288
def __init__(
@@ -284,6 +291,7 @@ def __init__(
284291
tracer_provider: typing.Optional[TracerProvider] = None,
285292
request_hook: typing.Optional[RequestHook] = None,
286293
response_hook: typing.Optional[ResponseHook] = None,
294+
excluded_urls: typing.Optional[ExcludeList] = None,
287295
):
288296
self._transport = transport
289297
self._tracer = get_tracer(
@@ -293,6 +301,7 @@ def __init__(
293301
)
294302
self._request_hook = request_hook
295303
self._response_hook = response_hook
304+
self._excluded_urls = excluded_urls
296305

297306
def __enter__(self) -> "SyncOpenTelemetryTransport":
298307
self._transport.__enter__()
@@ -317,10 +326,13 @@ def handle_request(
317326
"""Add request info to span."""
318327
if context.get_value("suppress_instrumentation"):
319328
return self._transport.handle_request(*args, **kwargs)
320-
321329
method, url, headers, stream, extensions = _extract_parameters(
322330
args, kwargs
323331
)
332+
333+
if self._excluded_urls and self._excluded_urls.url_disabled(url):
334+
return self._transport.handle_request(*args, **kwargs)
335+
324336
span_attributes = _prepare_attributes(method, url)
325337

326338
request_info = RequestInfo(method, url, headers, stream, extensions)
@@ -370,6 +382,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
370382
right after the span is created
371383
response_hook: A hook that receives the span, request, and response
372384
that is called right before the span ends
385+
excluded_urls: List of urls that should be excluded from tracing
373386
"""
374387

375388
def __init__(
@@ -378,6 +391,7 @@ def __init__(
378391
tracer_provider: typing.Optional[TracerProvider] = None,
379392
request_hook: typing.Optional[RequestHook] = None,
380393
response_hook: typing.Optional[ResponseHook] = None,
394+
excluded_urls: typing.Optional[ExcludeList] = None,
381395
):
382396
self._transport = transport
383397
self._tracer = get_tracer(
@@ -387,6 +401,7 @@ def __init__(
387401
)
388402
self._request_hook = request_hook
389403
self._response_hook = response_hook
404+
self._excluded_urls = excluded_urls
390405

391406
async def __aenter__(self) -> "AsyncOpenTelemetryTransport":
392407
await self._transport.__aenter__()
@@ -407,12 +422,16 @@ async def handle_async_request(
407422
httpx.Response,
408423
]:
409424
"""Add request info to span."""
410-
if context.get_value("suppress_instrumentation"):
411-
return await self._transport.handle_async_request(*args, **kwargs)
412-
413425
method, url, headers, stream, extensions = _extract_parameters(
414426
args, kwargs
415427
)
428+
429+
if self._excluded_urls and self._excluded_urls.url_disabled(url):
430+
return await self._transport.handle_async_request(*args, **kwargs)
431+
432+
if context.get_value("suppress_instrumentation"):
433+
return await self._transport.handle_async_request(*args, **kwargs)
434+
416435
span_attributes = _prepare_attributes(method, url)
417436

418437
span_name = _get_default_span_name(
@@ -459,6 +478,7 @@ class _InstrumentedClient(httpx.Client):
459478
_tracer_provider = None
460479
_request_hook = None
461480
_response_hook = None
481+
_excluded_urls = None
462482

463483
def __init__(self, *args, **kwargs):
464484
super().__init__(*args, **kwargs)
@@ -478,6 +498,7 @@ class _InstrumentedAsyncClient(httpx.AsyncClient):
478498
_tracer_provider = None
479499
_request_hook = None
480500
_response_hook = None
501+
_excluded_urls = None
481502

482503
def __init__(self, *args, **kwargs):
483504
super().__init__(*args, **kwargs)
@@ -513,11 +534,18 @@ def _instrument(self, **kwargs):
513534
right after the span is created
514535
``response_hook``: A hook that receives the span, request, and response
515536
that is called right before the span ends
537+
``excluded_urls``: A string containing a comma-delimited
538+
list of regexes used to exclude URLs from tracking
516539
"""
517540
self._original_client = httpx.Client
518541
self._original_async_client = httpx.AsyncClient
519542
request_hook = kwargs.get("request_hook")
520543
response_hook = kwargs.get("response_hook")
544+
excluded_urls = kwargs.get("excluded_urls")
545+
if excluded_urls is None:
546+
excluded_urls = _excluded_urls_from_env
547+
else:
548+
excluded_urls = parse_excluded_urls(excluded_urls)
521549
if callable(request_hook):
522550
_InstrumentedClient._request_hook = request_hook
523551
_InstrumentedAsyncClient._request_hook = request_hook
@@ -536,9 +564,11 @@ def _uninstrument(self, **kwargs):
536564
_InstrumentedClient._tracer_provider = None
537565
_InstrumentedClient._request_hook = None
538566
_InstrumentedClient._response_hook = None
567+
_InstrumentedClient._excluded_urls = None
539568
_InstrumentedAsyncClient._tracer_provider = None
540569
_InstrumentedAsyncClient._request_hook = None
541570
_InstrumentedAsyncClient._response_hook = None
571+
_InstrumentedAsyncClient._excluded_urls = None
542572

543573
@staticmethod
544574
def instrument_client(

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

+51-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import typing
1818
from unittest import mock
1919

20+
import httpretty
2021
import httpx
2122
import respx
2223

@@ -43,10 +44,10 @@
4344
ResponseHook,
4445
ResponseInfo,
4546
)
46-
from opentelemetry.sdk.trace.export import SpanExporter
47-
from opentelemetry.trace import TracerProvider
48-
from opentelemetry.trace.span import Span
49-
47+
from opentelemetry.sdk.trace.export import SpanExporter
48+
from opentelemetry.trace import TracerProvider
49+
from opentelemetry.trace.span import Span
50+
from opentelemetry.util.http import get_excluded_urls
5051

5152
HTTP_RESPONSE_BODY = "http.response.body"
5253

@@ -380,8 +381,24 @@ def create_client(
380381

381382
def setUp(self):
382383
super().setUp()
384+
385+
self.env_patch = mock.patch.dict(
386+
"os.environ",
387+
{
388+
"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg"
389+
},
390+
)
391+
self.env_patch.start()
392+
393+
self.exclude_patch = mock.patch(
394+
"opentelemetry.instrumentation.httpx._excluded_urls_from_env",
395+
get_excluded_urls("HTTPX"),
396+
)
397+
self.exclude_patch.start()
398+
383399
HTTPXClientInstrumentor().instrument()
384400
self.client = self.create_client()
401+
self.env_patch.stop()
385402
HTTPXClientInstrumentor().uninstrument()
386403

387404
def test_custom_tracer_provider(self):
@@ -495,6 +512,36 @@ def test_uninstrument(self):
495512
self.assertEqual(result.text, "Hello!")
496513
self.assert_span(num_spans=0)
497514

515+
def test_excluded_urls_explicit(self):
516+
url_404 = "http://mock/status/404"
517+
httpretty.register_uri(
518+
httpretty.GET,
519+
url_404,
520+
status=404,
521+
)
522+
523+
HTTPXClientInstrumentor().instrument(excluded_urls=".*/404")
524+
client = self.create_client()
525+
self.perform_request(self.URL)
526+
self.perform_request(url_404)
527+
528+
self.assert_span(num_spans=1)
529+
530+
def test_excluded_urls_from_env(self):
531+
url = "http://localhost/env_excluded_arg/123"
532+
httpretty.register_uri(
533+
httpretty.GET,
534+
url,
535+
status=200,
536+
)
537+
538+
HTTPXClientInstrumentor().instrument()
539+
client = self.create_client()
540+
self.perform_request(self.URL)
541+
self.perform_request(url)
542+
543+
self.assert_span(num_spans=1)
544+
498545
def test_uninstrument_client(self):
499546
HTTPXClientInstrumentor().uninstrument_client(self.client)
500547

0 commit comments

Comments
 (0)