Add net.peer.ip in requests & urllib3 instrumentations. (#661)
This commit is contained in:
parent
201aa2bb1b
commit
efaa257a63
|
|
@ -6,7 +6,7 @@ on:
|
|||
- 'release/*'
|
||||
pull_request:
|
||||
env:
|
||||
CORE_REPO_SHA: c49ad57bfe35cfc69bfa863d74058ca9bec55fc3
|
||||
CORE_REPO_SHA: d9c22a87b6bfc5ec332588c764f82c32f068b2c3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `opentelemetry-instrumentation-urllib3` Updated `_RequestHookT` with two additional fields - the request body and the request headers
|
||||
([#660](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/660))
|
||||
|
||||
### Added
|
||||
|
||||
- `opentelemetry-instrumentation-urllib3`, `opentelemetry-instrumentation-requests`
|
||||
The `net.peer.ip` attribute is set to the IP of the connected HTTP server or proxy
|
||||
using a new instrumentor in `opententelemetry-util-http`
|
||||
([#661](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/661))
|
||||
|
||||
## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ from opentelemetry.semconv.trace import SpanAttributes
|
|||
from opentelemetry.trace import SpanKind, get_tracer
|
||||
from opentelemetry.trace.status import Status
|
||||
from opentelemetry.util.http import remove_url_credentials
|
||||
from opentelemetry.util.http.httplib import set_ip_on_next_http_connection
|
||||
|
||||
# A key to a context variable to avoid creating duplicate spans when instrumenting
|
||||
# both, Session.request and Session.send, since Session.request calls into Session.send
|
||||
|
|
@ -133,7 +134,7 @@ def _instrument(tracer, span_callback=None, name_callback=None):
|
|||
|
||||
with tracer.start_as_current_span(
|
||||
span_name, kind=SpanKind.CLIENT
|
||||
) as span:
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
exception = None
|
||||
if span.is_recording():
|
||||
span.set_attribute(SpanAttributes.HTTP_METHOD, method)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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 requests
|
||||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
||||
from opentelemetry.test.httptest import HttpTestBase
|
||||
from opentelemetry.test.test_base import TestBase
|
||||
from opentelemetry.util.http.httplib import HttpClientInstrumentor
|
||||
|
||||
|
||||
class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.assert_ip = self.server.server_address[0]
|
||||
self.http_host = ":".join(map(str, self.server.server_address[:2]))
|
||||
self.http_url_base = "http://" + self.http_host
|
||||
self.http_url = self.http_url_base + "/status/200"
|
||||
HttpClientInstrumentor().instrument()
|
||||
RequestsInstrumentor().instrument()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
HttpClientInstrumentor().uninstrument()
|
||||
RequestsInstrumentor().uninstrument()
|
||||
|
||||
@staticmethod
|
||||
def perform_request(url: str) -> requests.Response:
|
||||
return requests.get(url)
|
||||
|
||||
def test_basic_http_success(self):
|
||||
response = self.perform_request(self.http_url)
|
||||
self.assert_success_span(response)
|
||||
|
||||
def test_basic_http_success_using_connection_pool(self):
|
||||
with requests.Session() as session:
|
||||
response = session.get(self.http_url)
|
||||
|
||||
self.assert_success_span(response)
|
||||
|
||||
# Test that when re-using an existing connection, everything still works.
|
||||
# Especially relevant for IP capturing.
|
||||
response = session.get(self.http_url)
|
||||
|
||||
self.assert_success_span(response)
|
||||
|
||||
def assert_span(self, num_spans=1): # TODO: Move this to TestBase
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(num_spans, len(span_list))
|
||||
if num_spans == 0:
|
||||
return None
|
||||
self.memory_exporter.clear()
|
||||
if num_spans == 1:
|
||||
return span_list[0]
|
||||
return span_list
|
||||
|
||||
def assert_success_span(self, response: requests.Response):
|
||||
self.assertEqual("Hello!", response.text)
|
||||
|
||||
span = self.assert_span()
|
||||
self.assertIs(trace.SpanKind.CLIENT, span.kind)
|
||||
self.assertEqual("HTTP GET", span.name)
|
||||
|
||||
attributes = {
|
||||
"http.status_code": 200,
|
||||
"net.peer.ip": self.assert_ip,
|
||||
}
|
||||
self.assertGreaterEqual(span.attributes.items(), attributes.items())
|
||||
|
|
@ -41,6 +41,7 @@ install_requires =
|
|||
opentelemetry-api ~= 1.3
|
||||
opentelemetry-semantic-conventions == 0.24b0
|
||||
opentelemetry-instrumentation == 0.24b0
|
||||
opentelemetry-util-http == 0.24b0
|
||||
wrapt >= 1.0.0, < 2.0.0
|
||||
|
||||
[options.extras_require]
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ from opentelemetry.propagate import inject
|
|||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace import Span, SpanKind, get_tracer
|
||||
from opentelemetry.trace.status import Status
|
||||
from opentelemetry.util.http.httplib import set_ip_on_next_http_connection
|
||||
|
||||
# A key to a context variable to avoid creating duplicate spans when instrumenting
|
||||
# both, Session.request and Session.send, since Session.request calls into Session.send
|
||||
|
|
@ -168,7 +169,7 @@ def _instrument(
|
|||
|
||||
with tracer.start_as_current_span(
|
||||
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
|
||||
) as span:
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
if callable(request_hook):
|
||||
request_hook(span, instance, headers, body)
|
||||
inject(headers)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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 urllib3
|
||||
import urllib3.exceptions
|
||||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
|
||||
from opentelemetry.test.httptest import HttpTestBase
|
||||
from opentelemetry.test.test_base import TestBase
|
||||
from opentelemetry.util.http.httplib import HttpClientInstrumentor
|
||||
|
||||
|
||||
class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.assert_ip = self.server.server_address[0]
|
||||
self.http_host = ":".join(map(str, self.server.server_address[:2]))
|
||||
self.http_url_base = "http://" + self.http_host
|
||||
self.http_url = self.http_url_base + "/status/200"
|
||||
HttpClientInstrumentor().instrument()
|
||||
URLLib3Instrumentor().instrument()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
HttpClientInstrumentor().uninstrument()
|
||||
URLLib3Instrumentor().uninstrument()
|
||||
|
||||
@staticmethod
|
||||
def perform_request(url: str) -> urllib3.response.HTTPResponse:
|
||||
with urllib3.PoolManager() as pool:
|
||||
resp = pool.request("GET", url)
|
||||
resp.close()
|
||||
return resp
|
||||
|
||||
def test_basic_http_success(self):
|
||||
response = self.perform_request(self.http_url)
|
||||
self.assert_success_span(response, self.http_url)
|
||||
|
||||
def test_basic_http_success_using_connection_pool(self):
|
||||
with urllib3.HTTPConnectionPool(self.http_host, timeout=3) as pool:
|
||||
response = pool.request("GET", "/status/200")
|
||||
|
||||
self.assert_success_span(response, self.http_url)
|
||||
|
||||
# Test that when re-using an existing connection, everything still works.
|
||||
# Especially relevant for IP capturing.
|
||||
response = pool.request("GET", "/status/200")
|
||||
|
||||
self.assert_success_span(response, self.http_url)
|
||||
|
||||
def assert_span(self, num_spans=1):
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(num_spans, len(span_list))
|
||||
if num_spans == 0:
|
||||
return None
|
||||
self.memory_exporter.clear()
|
||||
if num_spans == 1:
|
||||
return span_list[0]
|
||||
return span_list
|
||||
|
||||
def assert_success_span(
|
||||
self, response: urllib3.response.HTTPResponse, url: str
|
||||
):
|
||||
self.assertEqual(b"Hello!", response.data)
|
||||
|
||||
span = self.assert_span()
|
||||
self.assertIs(trace.SpanKind.CLIENT, span.kind)
|
||||
self.assertEqual("HTTP GET", span.name)
|
||||
|
||||
attributes = {
|
||||
"http.status_code": 200,
|
||||
"net.peer.ip": self.assert_ip,
|
||||
}
|
||||
self.assertGreaterEqual(span.attributes.items(), attributes.items())
|
||||
2
tox.ini
2
tox.ini
|
|
@ -236,7 +236,7 @@ commands_pre =
|
|||
|
||||
grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test]
|
||||
|
||||
falcon,flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
|
||||
falcon,flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
|
||||
wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
|
||||
asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
|
||||
|
||||
|
|
|
|||
|
|
@ -40,3 +40,7 @@ packages=find_namespace:
|
|||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[options.entry_points]
|
||||
opentelemetry_instrumentor =
|
||||
httplib = opentelemetry.util.http.httplib:HttpClientInstrumentor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This library provides functionality to enrich HTTP client spans with IPs. It does
|
||||
not create spans on its own.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import http.client
|
||||
import logging
|
||||
import socket # pylint:disable=unused-import # Used for typing
|
||||
import typing
|
||||
from typing import Collection
|
||||
|
||||
import wrapt
|
||||
|
||||
from opentelemetry import context
|
||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||
from opentelemetry.instrumentation.utils import unwrap
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace.span import Span
|
||||
|
||||
_STATE_KEY = "httpbase_instrumentation_state"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpClientInstrumentor(BaseInstrumentor):
|
||||
def instrumentation_dependencies(self) -> Collection[str]:
|
||||
return () # This instruments http.client from stdlib; no extra deps.
|
||||
|
||||
def _instrument(self, **kwargs):
|
||||
"""Instruments the http.client module (not creating spans on its own)"""
|
||||
_instrument()
|
||||
|
||||
def _uninstrument(self, **kwargs):
|
||||
_uninstrument()
|
||||
|
||||
|
||||
def _remove_nonrecording(spanlist: typing.List[Span]):
|
||||
idx = len(spanlist) - 1
|
||||
while idx >= 0:
|
||||
if not spanlist[idx].is_recording():
|
||||
logger.debug("Span is not recording: %s", spanlist[idx])
|
||||
islast = idx + 1 == len(spanlist)
|
||||
if not islast:
|
||||
spanlist[idx] = spanlist[len(spanlist) - 1]
|
||||
spanlist.pop()
|
||||
if islast:
|
||||
if idx == 0:
|
||||
return False # We removed everything
|
||||
idx -= 1
|
||||
else:
|
||||
idx -= 1
|
||||
return True
|
||||
|
||||
|
||||
def trysetip(conn: http.client.HTTPConnection, loglevel=logging.DEBUG) -> bool:
|
||||
"""Tries to set the net.peer.ip semantic attribute on the current span from the given
|
||||
HttpConnection.
|
||||
|
||||
Returns False if the connection is not yet established, False if the IP was captured
|
||||
or there is no need to capture it.
|
||||
"""
|
||||
|
||||
state = _getstate()
|
||||
if not state:
|
||||
return True
|
||||
spanlist = state.get("need_ip") # type: typing.List[Span]
|
||||
if not spanlist:
|
||||
return True
|
||||
|
||||
# Remove all non-recording spans from the list.
|
||||
if not _remove_nonrecording(spanlist):
|
||||
return True
|
||||
|
||||
sock = "<property not accessed>"
|
||||
try:
|
||||
sock = conn.sock # type: typing.Optional[socket.socket]
|
||||
logger.debug("Got socket: %s", sock)
|
||||
if sock is None:
|
||||
return False
|
||||
addr = sock.getpeername()
|
||||
if addr and addr[0]:
|
||||
ip = addr[0]
|
||||
except Exception: # pylint:disable=broad-except
|
||||
logger.log(
|
||||
loglevel,
|
||||
"Failed to get peer address from %s",
|
||||
sock,
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
else:
|
||||
for span in spanlist:
|
||||
span.set_attribute(SpanAttributes.NET_PEER_IP, ip)
|
||||
return True
|
||||
|
||||
|
||||
def _instrumented_connect(
|
||||
wrapped, instance: http.client.HTTPConnection, args, kwargs
|
||||
):
|
||||
result = wrapped(*args, **kwargs)
|
||||
trysetip(instance, loglevel=logging.WARNING)
|
||||
return result
|
||||
|
||||
|
||||
def instrument_connect(module, name="connect"):
|
||||
"""Instrument additional connect() methods, e.g. for derived classes."""
|
||||
|
||||
wrapt.wrap_function_wrapper(
|
||||
module, name, _instrumented_connect,
|
||||
)
|
||||
|
||||
|
||||
def _instrument():
|
||||
def instrumented_send(
|
||||
wrapped, instance: http.client.HTTPConnection, args, kwargs
|
||||
):
|
||||
done = trysetip(instance)
|
||||
result = wrapped(*args, **kwargs)
|
||||
if not done:
|
||||
trysetip(instance, loglevel=logging.WARNING)
|
||||
return result
|
||||
|
||||
wrapt.wrap_function_wrapper(
|
||||
http.client.HTTPConnection, "send", instrumented_send,
|
||||
)
|
||||
|
||||
instrument_connect(http.client.HTTPConnection)
|
||||
# No need to instrument HTTPSConnection, as it calls super().connect()
|
||||
|
||||
|
||||
def _getstate() -> typing.Optional[dict]:
|
||||
return context.get_value(_STATE_KEY)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_ip_on_next_http_connection(span: Span):
|
||||
state = _getstate()
|
||||
if not state:
|
||||
token = context.attach(
|
||||
context.set_value(_STATE_KEY, {"need_ip": [span]})
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
context.detach(token)
|
||||
else:
|
||||
spans = state["need_ip"] # type: typing.List[Span]
|
||||
spans.append(span)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
try:
|
||||
spans.remove(span)
|
||||
except ValueError: # Span might have become non-recording
|
||||
pass
|
||||
|
||||
|
||||
def _uninstrument():
|
||||
unwrap(http.client.HTTPConnection, "send")
|
||||
unwrap(http.client.HTTPConnection, "connect")
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
|
||||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.test.httptest import HttpTestBase
|
||||
from opentelemetry.test.test_base import TestBase
|
||||
from opentelemetry.trace.span import INVALID_SPAN
|
||||
from opentelemetry.util.http.httplib import (
|
||||
HttpClientInstrumentor,
|
||||
set_ip_on_next_http_connection,
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
|
||||
class TestHttpBase(TestBase, HttpTestBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
HttpClientInstrumentor().instrument()
|
||||
self.server_thread, self.server = self.run_server()
|
||||
|
||||
def tearDown(self):
|
||||
HttpClientInstrumentor().uninstrument()
|
||||
self.server.shutdown()
|
||||
self.server_thread.join()
|
||||
super().tearDown()
|
||||
|
||||
def assert_span(self, exporter=None, num_spans=1):
|
||||
if exporter is None:
|
||||
exporter = self.memory_exporter
|
||||
span_list = exporter.get_finished_spans()
|
||||
self.assertEqual(num_spans, len(span_list))
|
||||
if num_spans == 0:
|
||||
return None
|
||||
if num_spans == 1:
|
||||
return span_list[0]
|
||||
return span_list
|
||||
|
||||
def test_basic(self):
|
||||
resp, body = self.perform_request()
|
||||
assert resp.status == 200
|
||||
assert body == b"Hello!"
|
||||
self.assert_span(num_spans=0)
|
||||
|
||||
def test_basic_with_span(self):
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span(
|
||||
"HTTP GET"
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
resp, body = self.perform_request()
|
||||
assert resp.status == 200
|
||||
assert body == b"Hello!"
|
||||
span = self.assert_span(num_spans=1)
|
||||
self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"})
|
||||
|
||||
def test_with_nested_span(self):
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span(
|
||||
"requests HTTP GET"
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
with tracer.start_as_current_span(
|
||||
"urllib3 HTTP GET"
|
||||
) as span2, set_ip_on_next_http_connection(span2):
|
||||
resp, body = self.perform_request()
|
||||
assert resp.status == 200
|
||||
assert body == b"Hello!"
|
||||
for span in self.assert_span(num_spans=2):
|
||||
self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"})
|
||||
|
||||
def test_with_nested_nonrecording_span(self):
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span(
|
||||
"requests HTTP GET"
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
with trace.use_span(INVALID_SPAN), set_ip_on_next_http_connection(
|
||||
INVALID_SPAN
|
||||
):
|
||||
resp, body = self.perform_request()
|
||||
assert resp.status == 200
|
||||
assert body == b"Hello!"
|
||||
span = self.assert_span(num_spans=1)
|
||||
self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"})
|
||||
|
||||
def test_with_only_nonrecording_span(self):
|
||||
with trace.use_span(INVALID_SPAN), set_ip_on_next_http_connection(
|
||||
INVALID_SPAN
|
||||
):
|
||||
resp, body = self.perform_request()
|
||||
assert resp.status == 200
|
||||
assert body == b"Hello!"
|
||||
self.assert_span(num_spans=0)
|
||||
|
||||
def perform_request(self, secure=False) -> HTTPResponse:
|
||||
conn_cls = HTTPSConnection if secure else HTTPConnection
|
||||
conn = conn_cls(self.server.server_address[0], self.server.server_port)
|
||||
resp = None
|
||||
try:
|
||||
conn.request("GET", "/", headers={"Connection": "close"})
|
||||
resp = conn.getresponse()
|
||||
return resp, resp.read()
|
||||
finally:
|
||||
if resp:
|
||||
resp.close()
|
||||
conn.close()
|
||||
|
||||
def test_uninstrument(self):
|
||||
HttpClientInstrumentor().uninstrument()
|
||||
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span(
|
||||
"HTTP GET"
|
||||
) as span, set_ip_on_next_http_connection(span):
|
||||
body = self.perform_request()[1]
|
||||
self.assertEqual(b"Hello!", body)
|
||||
|
||||
# We should have a span, but it should have no attributes
|
||||
self.assertFalse(self.assert_span(num_spans=1).attributes)
|
||||
|
||||
# instrument again to avoid warning message on tearDown
|
||||
HttpClientInstrumentor().instrument()
|
||||
Loading…
Reference in New Issue