Skip to content

Exclude list for httpx instrumentation #3345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added `exclude urls` feature to HTTPX instrumentation
([#3345](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3345))
- `opentelemetry-instrumentation-openai-v2` Update doc for OpenAI Instrumentation to support OpenAI Compatible Platforms
([#3279](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3279))
- `opentelemetry-instrumentation-system-metrics` Add `process` metrics and deprecated `process.runtime` prefixed ones
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,15 @@ async def async_response_hook(span, request, response):
from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer
from opentelemetry.trace.span import Span
from opentelemetry.trace.status import StatusCode
from opentelemetry.util.http import remove_url_credentials, sanitize_method
from opentelemetry.util.http import (
ExcludeList,
get_excluded_urls,
parse_excluded_urls,
remove_url_credentials,
sanitize_method,
)

_excluded_urls_from_env = get_excluded_urls("HTTPX")
_logger = logging.getLogger(__name__)

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

def __init__(
Expand All @@ -419,6 +427,7 @@ def __init__(
tracer_provider: TracerProvider | None = None,
request_hook: RequestHook | None = None,
response_hook: ResponseHook | None = None,
excluded_urls: ExcludeList | None = None,
):
_OpenTelemetrySemanticConventionStability._initialize()
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
Expand All @@ -434,6 +443,7 @@ def __init__(
)
self._request_hook = request_hook
self._response_hook = response_hook
self._excluded_urls = excluded_urls

def __enter__(self) -> SyncOpenTelemetryTransport:
self._transport.__enter__()
Expand Down Expand Up @@ -463,6 +473,10 @@ def handle_request(
method, url, headers, stream, extensions = _extract_parameters(
args, kwargs
)

if self._excluded_urls and self._excluded_urls.url_disabled(str(url)):
return self._transport.handle_request(*args, **kwargs)

method_original = method.decode()
span_name = _get_default_span_name(method_original)
span_attributes = {}
Expand Down Expand Up @@ -536,6 +550,7 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
right after the span is created
response_hook: A hook that receives the span, request, and response
that is called right before the span ends
excluded_urls: List of urls that should be excluded from tracing
"""

def __init__(
Expand All @@ -544,6 +559,7 @@ def __init__(
tracer_provider: TracerProvider | None = None,
request_hook: AsyncRequestHook | None = None,
response_hook: AsyncResponseHook | None = None,
excluded_urls: ExcludeList | None = None,
):
_OpenTelemetrySemanticConventionStability._initialize()
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
Expand All @@ -559,6 +575,7 @@ def __init__(
)
self._request_hook = request_hook
self._response_hook = response_hook
self._excluded_urls = excluded_urls

async def __aenter__(self) -> "AsyncOpenTelemetryTransport":
await self._transport.__aenter__()
Expand Down Expand Up @@ -586,6 +603,10 @@ async def handle_async_request(
method, url, headers, stream, extensions = _extract_parameters(
args, kwargs
)

if self._excluded_urls and self._excluded_urls.url_disabled(str(url)):
return await self._transport.handle_async_request(*args, **kwargs)

method_original = method.decode()
span_name = _get_default_span_name(method_original)
span_attributes = {}
Expand Down Expand Up @@ -674,6 +695,8 @@ def _instrument(self, **kwargs: typing.Any):
and response that is called right before the span ends
``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient``
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
``excluded_urls``: A string containing a comma-delimited
list of regexes used to exclude URLs from tracking
"""
tracer_provider = kwargs.get("tracer_provider")
request_hook = kwargs.get("request_hook")
Expand All @@ -690,6 +713,14 @@ def _instrument(self, **kwargs: typing.Any):
if iscoroutinefunction(async_response_hook)
else None
)

excluded_urls_raw = kwargs.get("excluded_urls")

excluded_urls = (
parse_excluded_urls(excluded_urls_raw)
if excluded_urls_raw
else _excluded_urls_from_env
)

_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
Expand All @@ -711,6 +742,7 @@ def _instrument(self, **kwargs: typing.Any):
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
request_hook=request_hook,
response_hook=response_hook,
excluded_urls=excluded_urls,
),
)
wrap_function_wrapper(
Expand All @@ -722,6 +754,7 @@ def _instrument(self, **kwargs: typing.Any):
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook,
excluded_urls=excluded_urls,
),
)

Expand All @@ -739,13 +772,18 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
sem_conv_opt_in_mode: _StabilityMode,
request_hook: RequestHook,
response_hook: ResponseHook,
excluded_urls: ExcludeList | None = None,
):
if not is_http_instrumentation_enabled():
return wrapped(*args, **kwargs)

method, url, headers, stream, extensions = _extract_parameters(
args, kwargs
)

if excluded_urls and excluded_urls.url_disabled(str(url)):
return wrapped(*args, **kwargs)

method_original = method.decode()
span_name = _get_default_span_name(method_original)
span_attributes = {}
Expand Down Expand Up @@ -813,13 +851,18 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
sem_conv_opt_in_mode: _StabilityMode,
async_request_hook: AsyncRequestHook,
async_response_hook: AsyncResponseHook,
excluded_urls: ExcludeList | None = None,
):
if not is_http_instrumentation_enabled():
return await wrapped(*args, **kwargs)

method, url, headers, stream, extensions = _extract_parameters(
args, kwargs
)

if excluded_urls and excluded_urls.url_disabled(str(url)):
return await wrapped(*args, **kwargs)

method_original = method.decode()
span_name = _get_default_span_name(method_original)
span_attributes = {}
Expand Down Expand Up @@ -885,6 +928,7 @@ def instrument_client(
tracer_provider: TracerProvider | None = None,
request_hook: RequestHook | AsyncRequestHook | None = None,
response_hook: ResponseHook | AsyncResponseHook | None = None,
excluded_urls: ExcludeList | None = None,
) -> None:
"""Instrument httpx Client or AsyncClient

Expand Down Expand Up @@ -938,6 +982,7 @@ def instrument_client(
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
request_hook=request_hook,
response_hook=response_hook,
excluded_urls=excluded_urls,
),
)
for transport in client._mounts.values():
Expand All @@ -951,6 +996,7 @@ def instrument_client(
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
request_hook=request_hook,
response_hook=response_hook,
excluded_urls=excluded_urls,
),
)
client._is_instrumented_by_opentelemetry = True
Expand All @@ -964,6 +1010,7 @@ def instrument_client(
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook,
excluded_urls=excluded_urls,
),
)
for transport in client._mounts.values():
Expand All @@ -977,6 +1024,7 @@ def instrument_client(
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook,
excluded_urls=excluded_urls,
),
)
client._is_instrumented_by_opentelemetry = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from opentelemetry.test.mock_textmap import MockTextMapPropagator
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace import StatusCode
from opentelemetry.util.http import get_excluded_urls

if typing.TYPE_CHECKING:
from opentelemetry.instrumentation.httpx import (
Expand Down Expand Up @@ -153,6 +154,7 @@ def setUp(self):
self.env_patch = mock.patch.dict(
"os.environ",
{
"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg",
OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode,
},
)
Expand Down Expand Up @@ -955,6 +957,27 @@ def test_uninstrument(self):
self.assertEqual(result_no_client.text, "Hello!")
self.assert_span(num_spans=0)

def test_excluded_urls_explicit(self):
url_404 = "http://mock/status/404"
respx.get(url_404).mock(httpx.Response(404))

HTTPXClientInstrumentor().instrument(excluded_urls=".*/404")
client = self.create_client()
self.perform_request(self.URL, client=client)
self.perform_request(url_404, client=client)

self.assert_span(num_spans=1)

def test_excluded_urls_from_env(self):
url = "http://localhost/env_excluded_arg/123"
respx.get(url=url).mock(httpx.Response(200))
HTTPXClientInstrumentor().instrument()
client = self.create_client()
self.perform_request(self.URL, client=client)
self.perform_request(url, client=client)

self.assert_span(num_spans=1)

def test_uninstrument_client(self):
HTTPXClientInstrumentor().uninstrument_client(self.client)

Expand Down
Loading