Skip to content
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

Add hooks for aiohttp, asgi, starlette, fastAPI, urllib, urllib3 #576

Merged
merged 19 commits into from
Jul 26, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implemented hooks for urllib3 and updated documentation
Ryo Kather committed Jul 12, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 3e54425f550b328f17d72cb29613bd1b7976f9c1
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ def get(self):
*******

Tornado instrumentation supports extending tracing behaviour with the help of hooks.
It's ``instrument()`` method accepts three optional functions that get called back with the
Its ``instrument()`` method accepts three optional functions that get called back with the
created span and some other contextual information. Example:

.. code-block:: python
Original file line number Diff line number Diff line change
@@ -32,6 +32,29 @@
req = request.Request('https://postman-echo.com/post', method="POST")
r = request.urlopen(req)

Hooks
*******

The urllib instrumentation supports extending tracing behavior with the help of
request and response hooks. These are functions that are called back by the instrumentation
right after a Span is created for a request and right before the span is finished processing a response respectively.
The hooks can be configured as follows:

..code:: python

# `request` is an instance of urllib.request.Request
def request_hook(span, request):
pass

# `request` is an instance of urllib.request.Requests
# `response` is an instance of http.client.HTTPResponse
def response_hook(span, request, response)
pass

URLLibInstrumentor.instrument(
request_hook=request_hook, response_hook=response_hook)
)

API
---
"""
@@ -149,7 +172,7 @@ def _instrumented_open_call(
span_name, kind=SpanKind.CLIENT
) as span:
exception = None
if request_hook is not None:
if callable(request_hook):
request_hook(span, request)
if span.is_recording():
span.set_attribute(SpanAttributes.HTTP_METHOD, method)
@@ -184,7 +207,7 @@ def _instrumented_open_call(
ver_[:1], ver_[:-1]
)

if response_hook is not None:
if callable(response_hook):
response_hook(span, request, result)

if exception is not None:
Original file line number Diff line number Diff line change
@@ -40,6 +40,28 @@ def span_name_callback(method: str, url: str, headers):
http = urllib3.PoolManager()
response = http.request("GET", "https://www.example.org/")

Hooks
*******

The urllib3 instrumentation supports extending tracing behavior with the help of
request and response hooks. These are functions that are called back by the instrumentation
right after a Span is created for a request and right before the span is finished processing a response respectively.
The hooks can be configured as follows:

..code:: python

# `request` is an instance of urllib3.connectionpool.HTTPConnectionPool
def request_hook(span, request):
pass

# `request` is an instance of urllib3.connectionpool.HTTPConnectionPool
# `response` is an instance of urllib3.response.HTTPResponse
def response_hook(span, request, response)
pass

URLLib3Instrumentor.instrument(
request_hook=request_hook, response_hook=response_hook)
)
API
---
"""
@@ -72,8 +94,18 @@ def span_name_callback(method: str, url: str, headers):
)

_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
_SpanNameT = typing.Optional[
typing.Union[typing.Callable[[str, str, typing.Mapping], str], str]
_RequestHookT = typing.Optional[
typing.Callable[[Span, urllib3.connectionpool.HTTPConnectionPool], None]
]
_ResponseHookT = typing.Optional[
typing.Callable[
[
Span,
urllib3.connectionpool.HTTPConnectionPool,
urllib3.response.HTTPResponse,
],
None,
]
]

_URL_OPEN_ARG_TO_INDEX_MAPPING = {
@@ -92,15 +124,17 @@ def _instrument(self, **kwargs):
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global.
``span_name_or_callback``: Override the default span name.
``request_hook``: An optional callback invoked that is invoked right after a span is created.
``response_hook``: An optional callback which is invoked right before the span is finished processing a response
``url_filter``: A callback to process the requested URL prior
to adding it as a span attribute.
"""
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)
_instrument(
tracer,
span_name_or_callback=kwargs.get("span_name"),
request_hook=kwargs.get("request_hook"),
response_hook=kwargs.get("response_hook"),
url_filter=kwargs.get("url_filter"),
)

@@ -110,7 +144,8 @@ def _uninstrument(self, **kwargs):

def _instrument(
tracer,
span_name_or_callback: _SpanNameT = None,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
url_filter: _UrlFilterT = None,
):
def instrumented_urlopen(wrapped, instance, args, kwargs):
@@ -121,7 +156,7 @@ def instrumented_urlopen(wrapped, instance, args, kwargs):
url = _get_url(instance, args, kwargs, url_filter)
headers = _prepare_headers(kwargs)

span_name = _get_span_name(span_name_or_callback, method, url, headers)
span_name = "HTTP {}".format(method.strip())
span_attributes = {
SpanAttributes.HTTP_METHOD: method,
SpanAttributes.HTTP_URL: url,
@@ -130,12 +165,16 @@ def instrumented_urlopen(wrapped, instance, args, kwargs):
with tracer.start_as_current_span(
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
) as span:
if callable(request_hook):
request_hook(span, instance)
inject(headers)

with _suppress_further_instrumentation():
response = wrapped(*args, **kwargs)

_apply_response(span, response)
if callable(response_hook):
response_hook(span, instance, response)
return response

wrapt.wrap_function_wrapper(
@@ -195,20 +234,6 @@ def _prepare_headers(urlopen_kwargs: typing.Dict) -> typing.Dict:
return headers


def _get_span_name(
span_name_or_callback, method: str, url: str, headers: typing.Mapping
):
span_name = None
if callable(span_name_or_callback):
span_name = span_name_or_callback(method, url, headers)
elif isinstance(span_name_or_callback, str):
span_name = span_name_or_callback

if not span_name or not isinstance(span_name, str):
span_name = "HTTP {}".format(method.strip())
return span_name


def _apply_response(span: Span, response: urllib3.response.HTTPResponse):
if not span.is_recording():
return
Original file line number Diff line number Diff line change
@@ -244,44 +244,6 @@ def test_retries_do_not_create_spans(self, _):
# expect only a single span (retries are ignored)
self.assert_exception_span(self.HTTP_URL)

def test_span_name_callback(self):
def span_name_callback(method, url, headers):
self.assertEqual("GET", method)
self.assertEqual(self.HTTP_URL, url)
self.assertEqual({"key": "value"}, headers)

return "test_span_name"

URLLib3Instrumentor().uninstrument()
URLLib3Instrumentor().instrument(span_name=span_name_callback)

response = self.perform_request(
self.HTTP_URL, headers={"key": "value"}
)
self.assertEqual(b"Hello!", response.data)

span = self.assert_span()
self.assertEqual("test_span_name", span.name)

def test_span_name_callback_invalid(self):
invalid_span_names = (None, 123, "")

for span_name in invalid_span_names:
self.memory_exporter.clear()

# pylint: disable=unused-argument
def span_name_callback(method, url, headers):
return span_name # pylint: disable=cell-var-from-loop

URLLib3Instrumentor().uninstrument()
URLLib3Instrumentor().instrument(span_name=span_name_callback)
with self.subTest(span_name=span_name):
response = self.perform_request(self.HTTP_URL)
self.assertEqual(b"Hello!", response.data)

span = self.assert_span()
self.assertEqual("HTTP GET", span.name)

def test_url_filter(self):
def url_filter(url):
return url.split("?")[0]
@@ -297,3 +259,23 @@ def test_credential_removal(self):

response = self.perform_request(url)
self.assert_success_span(response, self.HTTP_URL)

def test_hooks(self):
def request_hook(span, request):
span.update_name("name set from hook")

def response_hook(span, request, response):
span.set_attribute("response_hook_attr", "value")

URLLib3Instrumentor().uninstrument()
URLLib3Instrumentor().instrument(
request_hook=request_hook, response_hook=response_hook
)
response = self.perform_request(self.HTTP_URL)
self.assertEqual(b"Hello!", response.data)

span = self.assert_span()

self.assertEqual(span.name, "name set from hook")
self.assertIn("response_hook_attr", span.attributes)
self.assertEqual(span.attributes["response_hook_attr"], "value")