Feature/metrics instrumentation urllib3 (#1198)
This commit is contained in:
		
							parent
							
								
									cbf005be6f
								
							
						
					
					
						commit
						be964036ae
					
				|  | @ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||||
|   ([#1127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1127)) |   ([#1127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1127)) | ||||||
| - Add metric instrumentation for WSGI | - Add metric instrumentation for WSGI | ||||||
|   ([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128)) |   ([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128)) | ||||||
|  | - Add metric instrumentation for Urllib3 | ||||||
|  |   ([#1198](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1198)) | ||||||
| - `opentelemetry-instrumentation-aio-pika` added RabbitMQ aio-pika module instrumentation. | - `opentelemetry-instrumentation-aio-pika` added RabbitMQ aio-pika module instrumentation. | ||||||
|   ([#1095](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1095)) |   ([#1095](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1095)) | ||||||
| - `opentelemetry-instrumentation-requests` Restoring metrics in requests | - `opentelemetry-instrumentation-requests` Restoring metrics in requests | ||||||
|  |  | ||||||
|  | @ -40,5 +40,5 @@ | ||||||
| | [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | | [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | ||||||
| | [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | No | | [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | No | ||||||
| | [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | No | | [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | No | ||||||
| | [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 2.0.0 | No | | [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 2.0.0 | Yes | ||||||
| | [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | | [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | ||||||
|  | @ -66,6 +66,7 @@ API | ||||||
| 
 | 
 | ||||||
| import contextlib | import contextlib | ||||||
| import typing | import typing | ||||||
|  | from timeit import default_timer | ||||||
| from typing import Collection | from typing import Collection | ||||||
| 
 | 
 | ||||||
| import urllib3.connectionpool | import urllib3.connectionpool | ||||||
|  | @ -83,9 +84,10 @@ from opentelemetry.instrumentation.utils import ( | ||||||
|     http_status_to_status_code, |     http_status_to_status_code, | ||||||
|     unwrap, |     unwrap, | ||||||
| ) | ) | ||||||
|  | from opentelemetry.metrics import Histogram, get_meter | ||||||
| from opentelemetry.propagate import inject | from opentelemetry.propagate import inject | ||||||
| from opentelemetry.semconv.trace import SpanAttributes | from opentelemetry.semconv.trace import SpanAttributes | ||||||
| from opentelemetry.trace import Span, SpanKind, get_tracer | from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer | ||||||
| from opentelemetry.trace.status import Status | from opentelemetry.trace.status import Status | ||||||
| from opentelemetry.util.http.httplib import set_ip_on_next_http_connection | from opentelemetry.util.http.httplib import set_ip_on_next_http_connection | ||||||
| 
 | 
 | ||||||
|  | @ -135,8 +137,31 @@ class URLLib3Instrumentor(BaseInstrumentor): | ||||||
|         """ |         """ | ||||||
|         tracer_provider = kwargs.get("tracer_provider") |         tracer_provider = kwargs.get("tracer_provider") | ||||||
|         tracer = get_tracer(__name__, __version__, tracer_provider) |         tracer = get_tracer(__name__, __version__, tracer_provider) | ||||||
|  | 
 | ||||||
|  |         meter_provider = kwargs.get("meter_provider") | ||||||
|  |         meter = get_meter(__name__, __version__, meter_provider) | ||||||
|  | 
 | ||||||
|  |         duration_histogram = meter.create_histogram( | ||||||
|  |             name="http.client.duration", | ||||||
|  |             unit="ms", | ||||||
|  |             description="measures the duration outbound HTTP requests", | ||||||
|  |         ) | ||||||
|  |         request_size_histogram = meter.create_histogram( | ||||||
|  |             name="http.client.request.size", | ||||||
|  |             unit="By", | ||||||
|  |             description="measures the size of HTTP request messages (compressed)", | ||||||
|  |         ) | ||||||
|  |         response_size_histogram = meter.create_histogram( | ||||||
|  |             name="http.client.response.size", | ||||||
|  |             unit="By", | ||||||
|  |             description="measures the size of HTTP response messages (compressed)", | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         _instrument( |         _instrument( | ||||||
|             tracer, |             tracer, | ||||||
|  |             duration_histogram, | ||||||
|  |             request_size_histogram, | ||||||
|  |             response_size_histogram, | ||||||
|             request_hook=kwargs.get("request_hook"), |             request_hook=kwargs.get("request_hook"), | ||||||
|             response_hook=kwargs.get("response_hook"), |             response_hook=kwargs.get("response_hook"), | ||||||
|             url_filter=kwargs.get("url_filter"), |             url_filter=kwargs.get("url_filter"), | ||||||
|  | @ -147,7 +172,10 @@ class URLLib3Instrumentor(BaseInstrumentor): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _instrument( | def _instrument( | ||||||
|     tracer, |     tracer: Tracer, | ||||||
|  |     duration_histogram: Histogram, | ||||||
|  |     request_size_histogram: Histogram, | ||||||
|  |     response_size_histogram: Histogram, | ||||||
|     request_hook: _RequestHookT = None, |     request_hook: _RequestHookT = None, | ||||||
|     response_hook: _ResponseHookT = None, |     response_hook: _ResponseHookT = None, | ||||||
|     url_filter: _UrlFilterT = None, |     url_filter: _UrlFilterT = None, | ||||||
|  | @ -175,11 +203,30 @@ def _instrument( | ||||||
|             inject(headers) |             inject(headers) | ||||||
| 
 | 
 | ||||||
|             with _suppress_further_instrumentation(): |             with _suppress_further_instrumentation(): | ||||||
|  |                 start_time = default_timer() | ||||||
|                 response = wrapped(*args, **kwargs) |                 response = wrapped(*args, **kwargs) | ||||||
|  |                 elapsed_time = round((default_timer() - start_time) * 1000) | ||||||
| 
 | 
 | ||||||
|             _apply_response(span, response) |             _apply_response(span, response) | ||||||
|             if callable(response_hook): |             if callable(response_hook): | ||||||
|                 response_hook(span, instance, response) |                 response_hook(span, instance, response) | ||||||
|  | 
 | ||||||
|  |             request_size = 0 if body is None else len(body) | ||||||
|  |             response_size = int(response.headers.get("Content-Length", 0)) | ||||||
|  |             metric_attributes = _create_metric_attributes( | ||||||
|  |                 instance, response, method | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             duration_histogram.record( | ||||||
|  |                 elapsed_time, attributes=metric_attributes | ||||||
|  |             ) | ||||||
|  |             request_size_histogram.record( | ||||||
|  |                 request_size, attributes=metric_attributes | ||||||
|  |             ) | ||||||
|  |             response_size_histogram.record( | ||||||
|  |                 response_size, attributes=metric_attributes | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|             return response |             return response | ||||||
| 
 | 
 | ||||||
|     wrapt.wrap_function_wrapper( |     wrapt.wrap_function_wrapper( | ||||||
|  | @ -254,6 +301,29 @@ def _is_instrumentation_suppressed() -> bool: | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def _create_metric_attributes( | ||||||
|  |     instance: urllib3.connectionpool.HTTPConnectionPool, | ||||||
|  |     response: urllib3.response.HTTPResponse, | ||||||
|  |     method: str, | ||||||
|  | ) -> dict: | ||||||
|  |     metric_attributes = { | ||||||
|  |         SpanAttributes.HTTP_METHOD: method, | ||||||
|  |         SpanAttributes.HTTP_HOST: instance.host, | ||||||
|  |         SpanAttributes.HTTP_SCHEME: instance.scheme, | ||||||
|  |         SpanAttributes.HTTP_STATUS_CODE: response.status, | ||||||
|  |         SpanAttributes.NET_PEER_NAME: instance.host, | ||||||
|  |         SpanAttributes.NET_PEER_PORT: instance.port, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     version = getattr(response, "version") | ||||||
|  |     if version: | ||||||
|  |         metric_attributes[SpanAttributes.HTTP_FLAVOR] = ( | ||||||
|  |             "1.1" if version == 11 else "1.0" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     return metric_attributes | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @contextlib.contextmanager | @contextlib.contextmanager | ||||||
| def _suppress_further_instrumentation(): | def _suppress_further_instrumentation(): | ||||||
|     token = context.attach( |     token = context.attach( | ||||||
|  |  | ||||||
|  | @ -14,3 +14,5 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| _instruments = ("urllib3 >= 1.0.0, < 2.0.0",) | _instruments = ("urllib3 >= 1.0.0, < 2.0.0",) | ||||||
|  | 
 | ||||||
|  | _supports_metrics = True | ||||||
|  |  | ||||||
|  | @ -12,8 +12,11 @@ | ||||||
| # See the License for the specific language governing permissions and | # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | # limitations under the License. | ||||||
| 
 | 
 | ||||||
|  | from timeit import default_timer | ||||||
|  | 
 | ||||||
| import urllib3 | import urllib3 | ||||||
| import urllib3.exceptions | import urllib3.exceptions | ||||||
|  | from urllib3.request import encode_multipart_formdata | ||||||
| 
 | 
 | ||||||
| from opentelemetry import trace | from opentelemetry import trace | ||||||
| from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor | from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor | ||||||
|  | @ -84,3 +87,136 @@ class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase): | ||||||
|             "net.peer.ip": self.assert_ip, |             "net.peer.ip": self.assert_ip, | ||||||
|         } |         } | ||||||
|         self.assertGreaterEqual(span.attributes.items(), attributes.items()) |         self.assertGreaterEqual(span.attributes.items(), attributes.items()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase): | ||||||
|  |     def setUp(self): | ||||||
|  |         super().setUp() | ||||||
|  |         self.assert_ip = self.server.server_address[0] | ||||||
|  |         self.assert_port = self.server.server_address[1] | ||||||
|  |         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" | ||||||
|  |         URLLib3Instrumentor().instrument(meter_provider=self.meter_provider) | ||||||
|  | 
 | ||||||
|  |     def tearDown(self): | ||||||
|  |         super().tearDown() | ||||||
|  |         URLLib3Instrumentor().uninstrument() | ||||||
|  | 
 | ||||||
|  |     def test_metric_uninstrument(self): | ||||||
|  |         with urllib3.PoolManager() as pool: | ||||||
|  |             pool.request("GET", self.http_url) | ||||||
|  |             URLLib3Instrumentor().uninstrument() | ||||||
|  |             pool.request("GET", self.http_url) | ||||||
|  | 
 | ||||||
|  |             metrics_list = self.memory_metrics_reader.get_metrics_data() | ||||||
|  |             for resource_metric in metrics_list.resource_metrics: | ||||||
|  |                 for scope_metric in resource_metric.scope_metrics: | ||||||
|  |                     for metric in scope_metric.metrics: | ||||||
|  |                         for point in list(metric.data.data_points): | ||||||
|  |                             self.assertEqual(point.count, 1) | ||||||
|  | 
 | ||||||
|  |     def test_basic_metric_check_client_size_get(self): | ||||||
|  |         with urllib3.PoolManager() as pool: | ||||||
|  |             start_time = default_timer() | ||||||
|  |             response = pool.request("GET", self.http_url) | ||||||
|  |             client_duration_estimated = (default_timer() - start_time) * 1000 | ||||||
|  | 
 | ||||||
|  |             expected_attributes = { | ||||||
|  |                 "http.status_code": 200, | ||||||
|  |                 "http.host": self.assert_ip, | ||||||
|  |                 "http.method": "GET", | ||||||
|  |                 "http.flavor": "1.1", | ||||||
|  |                 "http.scheme": "http", | ||||||
|  |                 "net.peer.name": self.assert_ip, | ||||||
|  |                 "net.peer.port": self.assert_port, | ||||||
|  |             } | ||||||
|  |             expected_data = { | ||||||
|  |                 "http.client.request.size": 0, | ||||||
|  |                 "http.client.response.size": len(response.data), | ||||||
|  |             } | ||||||
|  |             expected_metrics = [ | ||||||
|  |                 "http.client.duration", | ||||||
|  |                 "http.client.request.size", | ||||||
|  |                 "http.client.response.size", | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |             resource_metrics = ( | ||||||
|  |                 self.memory_metrics_reader.get_metrics_data().resource_metrics | ||||||
|  |             ) | ||||||
|  |             for metrics in resource_metrics: | ||||||
|  |                 for scope_metrics in metrics.scope_metrics: | ||||||
|  |                     self.assertEqual(len(scope_metrics.metrics), 3) | ||||||
|  |                     for metric in scope_metrics.metrics: | ||||||
|  |                         for data_point in metric.data.data_points: | ||||||
|  |                             if metric.name in expected_data: | ||||||
|  |                                 self.assertEqual( | ||||||
|  |                                     data_point.sum, expected_data[metric.name] | ||||||
|  |                                 ) | ||||||
|  |                             if metric.name == "http.client.duration": | ||||||
|  |                                 self.assertAlmostEqual( | ||||||
|  |                                     data_point.sum, | ||||||
|  |                                     client_duration_estimated, | ||||||
|  |                                     delta=1000, | ||||||
|  |                                 ) | ||||||
|  |                             self.assertIn(metric.name, expected_metrics) | ||||||
|  |                             self.assertDictEqual( | ||||||
|  |                                 expected_attributes, | ||||||
|  |                                 dict(data_point.attributes), | ||||||
|  |                             ) | ||||||
|  |                             self.assertEqual(data_point.count, 1) | ||||||
|  | 
 | ||||||
|  |     def test_basic_metric_check_client_size_post(self): | ||||||
|  |         with urllib3.PoolManager() as pool: | ||||||
|  |             start_time = default_timer() | ||||||
|  |             data_fields = {"data": "test"} | ||||||
|  |             response = pool.request("POST", self.http_url, fields=data_fields) | ||||||
|  |             client_duration_estimated = (default_timer() - start_time) * 1000 | ||||||
|  | 
 | ||||||
|  |             expected_attributes = { | ||||||
|  |                 "http.status_code": 501, | ||||||
|  |                 "http.host": self.assert_ip, | ||||||
|  |                 "http.method": "POST", | ||||||
|  |                 "http.flavor": "1.1", | ||||||
|  |                 "http.scheme": "http", | ||||||
|  |                 "net.peer.name": self.assert_ip, | ||||||
|  |                 "net.peer.port": self.assert_port, | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             body = encode_multipart_formdata(data_fields)[0] | ||||||
|  | 
 | ||||||
|  |             expected_data = { | ||||||
|  |                 "http.client.request.size": len(body), | ||||||
|  |                 "http.client.response.size": len(response.data), | ||||||
|  |             } | ||||||
|  |             expected_metrics = [ | ||||||
|  |                 "http.client.duration", | ||||||
|  |                 "http.client.request.size", | ||||||
|  |                 "http.client.response.size", | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |             resource_metrics = ( | ||||||
|  |                 self.memory_metrics_reader.get_metrics_data().resource_metrics | ||||||
|  |             ) | ||||||
|  |             for metrics in resource_metrics: | ||||||
|  |                 for scope_metrics in metrics.scope_metrics: | ||||||
|  |                     self.assertEqual(len(scope_metrics.metrics), 3) | ||||||
|  |                     for metric in scope_metrics.metrics: | ||||||
|  |                         for data_point in metric.data.data_points: | ||||||
|  |                             if metric.name in expected_data: | ||||||
|  |                                 self.assertEqual( | ||||||
|  |                                     data_point.sum, expected_data[metric.name] | ||||||
|  |                                 ) | ||||||
|  |                             if metric.name == "http.client.duration": | ||||||
|  |                                 self.assertAlmostEqual( | ||||||
|  |                                     data_point.sum, | ||||||
|  |                                     client_duration_estimated, | ||||||
|  |                                     delta=1000, | ||||||
|  |                                 ) | ||||||
|  |                             self.assertIn(metric.name, expected_metrics) | ||||||
|  | 
 | ||||||
|  |                             self.assertDictEqual( | ||||||
|  |                                 expected_attributes, | ||||||
|  |                                 dict(data_point.attributes), | ||||||
|  |                             ) | ||||||
|  |                             self.assertEqual(data_point.count, 1) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue