Skip to content

Commit

Permalink
feat: Add exclude urls feature to HTTPX instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
zqumei0 committed Aug 26, 2023
1 parent 1beab82 commit 689afb9
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
- Added `exclude urls` feature to HTTPX instrumentation
([#1900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1900))

### Fixed

- `opentelemetry-instrumentation-asgi` Fix UnboundLocalError local variable 'start' referenced before assignment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ instruments = [
test = [
"opentelemetry-instrumentation-httpx[instruments]",
"opentelemetry-sdk ~= 1.12",
"httpretty ~= 1.0",
"opentelemetry-test-utils == 0.41b0.dev",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,13 @@ def response_hook(span, request, response):
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
from opentelemetry.trace.span import Span
from opentelemetry.trace.status import Status
from opentelemetry.utils.http import (
ExcludeList,
get_excluded_urls,
parse_excluded_urls,
)

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

URL = typing.Tuple[bytes, bytes, typing.Optional[int], bytes]
Expand Down Expand Up @@ -276,6 +282,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 @@ -284,6 +291,7 @@ def __init__(
tracer_provider: typing.Optional[TracerProvider] = None,
request_hook: typing.Optional[RequestHook] = None,
response_hook: typing.Optional[ResponseHook] = None,
excluded_urls: typing.Optional[ExcludeList] = None,
):
self._transport = transport
self._tracer = get_tracer(
Expand All @@ -293,6 +301,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 All @@ -317,10 +326,13 @@ def handle_request(
"""Add request info to span."""
if context.get_value("suppress_instrumentation"):
return self._transport.handle_request(*args, **kwargs)

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

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

span_attributes = _prepare_attributes(method, url)

request_info = RequestInfo(method, url, headers, stream, extensions)
Expand Down Expand Up @@ -370,6 +382,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 @@ -378,6 +391,7 @@ def __init__(
tracer_provider: typing.Optional[TracerProvider] = None,
request_hook: typing.Optional[RequestHook] = None,
response_hook: typing.Optional[ResponseHook] = None,
excluded_urls: typing.Optional[ExcludeList] = None,
):
self._transport = transport
self._tracer = get_tracer(
Expand All @@ -387,6 +401,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 All @@ -407,12 +422,16 @@ async def handle_async_request(
httpx.Response,
]:
"""Add request info to span."""
if context.get_value("suppress_instrumentation"):
return await self._transport.handle_async_request(*args, **kwargs)

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

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

if context.get_value("suppress_instrumentation"):
return await self._transport.handle_async_request(*args, **kwargs)

span_attributes = _prepare_attributes(method, url)

span_name = _get_default_span_name(
Expand Down Expand Up @@ -459,6 +478,7 @@ class _InstrumentedClient(httpx.Client):
_tracer_provider = None
_request_hook = None
_response_hook = None
_excluded_urls = None

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -478,6 +498,7 @@ class _InstrumentedAsyncClient(httpx.AsyncClient):
_tracer_provider = None
_request_hook = None
_response_hook = None
_excluded_urls = None

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -513,11 +534,18 @@ def _instrument(self, **kwargs):
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``: A string containing a comma-delimited
list of regexes used to exclude URLs from tracking
"""
self._original_client = httpx.Client
self._original_async_client = httpx.AsyncClient
request_hook = kwargs.get("request_hook")
response_hook = kwargs.get("response_hook")
excluded_urls = kwargs.get("excluded_urls")
if excluded_urls is None:
excluded_urls = _excluded_urls_from_env
else:
excluded_urls = parse_excluded_urls(excluded_urls)
if callable(request_hook):
_InstrumentedClient._request_hook = request_hook
_InstrumentedAsyncClient._request_hook = request_hook
Expand All @@ -536,9 +564,11 @@ def _uninstrument(self, **kwargs):
_InstrumentedClient._tracer_provider = None
_InstrumentedClient._request_hook = None
_InstrumentedClient._response_hook = None
_InstrumentedClient._excluded_urls = None
_InstrumentedAsyncClient._tracer_provider = None
_InstrumentedAsyncClient._request_hook = None
_InstrumentedAsyncClient._response_hook = None
_InstrumentedAsyncClient._excluded_urls = None

@staticmethod
def instrument_client(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import typing
from unittest import mock

import httpretty
import httpx
import respx

Expand All @@ -43,10 +44,10 @@
ResponseHook,
ResponseInfo,
)
from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.trace import TracerProvider
from opentelemetry.trace.span import Span

from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.trace import TracerProvider
from opentelemetry.trace.span import Span
from opentelemetry.util.http import get_excluded_urls

HTTP_RESPONSE_BODY = "http.response.body"

Expand Down Expand Up @@ -380,8 +381,24 @@ def create_client(

def setUp(self):
super().setUp()

self.env_patch = mock.patch.dict(
"os.environ",
{
"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg"
},
)
self.env_patch.start()

self.exclude_patch = mock.patch(
"opentelemetry.instrumentation.httpx._excluded_urls_from_env",
get_excluded_urls("HTTPX"),
)
self.exclude_patch.start()

HTTPXClientInstrumentor().instrument()
self.client = self.create_client()
self.env_patch.stop()
HTTPXClientInstrumentor().uninstrument()

def test_custom_tracer_provider(self):
Expand Down Expand Up @@ -495,6 +512,36 @@ def test_uninstrument(self):
self.assertEqual(result.text, "Hello!")
self.assert_span(num_spans=0)

def test_excluded_urls_explicit(self):
url_404 = "http://mock/status/404"
httpretty.register_uri(
httpretty.GET,
url_404,
status=404,
)

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

self.assert_span(num_spans=1)

def test_excluded_urls_from_env(self):
url = "http://localhost/env_excluded_arg/123"
httpretty.register_uri(
httpretty.GET,
url,
status=200,
)

HTTPXClientInstrumentor().instrument()
client = self.create_client()
self.perform_request(self.URL)
self.perform_request(url)

self.assert_span(num_spans=1)

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

Expand Down

0 comments on commit 689afb9

Please sign in to comment.