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

Urllib3 request hook #660

Merged
merged 9 commits into from
Sep 13, 2021
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD)

### Changed

- `opentelemetry-instrumentation-urllib3` Added `_ExtendedRequestHookT` as an optional type of `request_hook`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this description still accurate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, it wasn't, pushed an update to it just now

this type extends the existing `_RequestHookT` with two more fields - the request body and the request headers

## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ def response_hook(span, request, response):
_RequestHookT = typing.Optional[
typing.Callable[[Span, urllib3.connectionpool.HTTPConnectionPool], None]
]
_ExtendedRequestHookT = typing.Optional[
typing.Callable[
[
Span,
urllib3.connectionpool.HTTPConnectionPool,
typing.Dict,
typing.Optional[str],
],
None,
]
]
_ResponseHookT = typing.Optional[
typing.Callable[
[
Expand Down Expand Up @@ -139,7 +150,7 @@ def _uninstrument(self, **kwargs):

def _instrument(
tracer,
request_hook: _RequestHookT = None,
request_hook: typing.Union[_RequestHookT, _ExtendedRequestHookT] = None,
response_hook: _ResponseHookT = None,
url_filter: _UrlFilterT = None,
):
Expand All @@ -150,6 +161,7 @@ def instrumented_urlopen(wrapped, instance, args, kwargs):
method = _get_url_open_arg("method", args, kwargs).upper()
url = _get_url(instance, args, kwargs, url_filter)
headers = _prepare_headers(kwargs)
body = _get_url_open_arg("body", args, kwargs)

span_name = "HTTP {}".format(method.strip())
span_attributes = {
Expand All @@ -161,7 +173,7 @@ def instrumented_urlopen(wrapped, instance, args, kwargs):
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
) as span:
if callable(request_hook):
request_hook(span, instance)
_call_request_hook(request_hook, span, instance, headers, body)
inject(headers)

with _suppress_further_instrumentation():
Expand All @@ -179,6 +191,21 @@ def instrumented_urlopen(wrapped, instance, args, kwargs):
)


def _call_request_hook(
request_hook: typing.Union[_RequestHookT, _ExtendedRequestHookT],
span: Span,
connection_pool: urllib3.connectionpool.HTTPConnectionPool,
headers: typing.Dict,
body: str,
):
try:
# First assume request_hook is a function of type _ExtendedRequestHookT
request_hook(span, connection_pool, headers, body)
ItayGibel-helios marked this conversation as resolved.
Show resolved Hide resolved
except TypeError:
# Fallback to call request_hook as a function of type _RequestHookT
request_hook(span, connection_pool)


def _get_url_open_arg(name: str, args: typing.List, kwargs: typing.Mapping):
arg_idx = _URL_OPEN_ARG_TO_INDEX_MAPPING.get(name)
if arg_idx is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import typing
from unittest import mock

Expand Down Expand Up @@ -45,6 +45,7 @@ def setUp(self):
httpretty.enable(allow_net_connect=False)
httpretty.register_uri(httpretty.GET, self.HTTP_URL, body="Hello!")
httpretty.register_uri(httpretty.GET, self.HTTPS_URL, body="Hello!")
httpretty.register_uri(httpretty.POST, self.HTTP_URL, body="Hello!")

def tearDown(self):
super().tearDown()
Expand Down Expand Up @@ -279,3 +280,30 @@ def response_hook(span, request, response):
self.assertEqual(span.name, "name set from hook")
self.assertIn("response_hook_attr", span.attributes)
self.assertEqual(span.attributes["response_hook_attr"], "value")

def test_extended_request_hook(self):
def extended_request_hook(span, request, headers, body):
span.set_attribute("request_hook_headers", json.dumps(headers))
span.set_attribute("request_hook_body", body)

URLLib3Instrumentor().uninstrument()
URLLib3Instrumentor().instrument(request_hook=extended_request_hook,)

headers = {"header1": "value1", "header2": "value2"}
body = "param1=1&param2=2"

pool = urllib3.HTTPConnectionPool("httpbin.org")
response = pool.request(
"POST", "/status/200", body=body, headers=headers
)

self.assertEqual(b"Hello!", response.data)

span = self.assert_span()

self.assertIn("request_hook_headers", span.attributes)
self.assertEqual(
span.attributes["request_hook_headers"], json.dumps(headers)
)
self.assertIn("request_hook_body", span.attributes)
self.assertEqual(span.attributes["request_hook_body"], body)