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)) | ||||
| - Add metric instrumentation for WSGI | ||||
|   ([#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. | ||||
|   ([#1095](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1095)) | ||||
| - `opentelemetry-instrumentation-requests` Restoring metrics in requests | ||||
|  |  | |||
|  | @ -40,5 +40,5 @@ | |||
| | [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | ||||
| | [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | 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 | ||||
|  | @ -66,6 +66,7 @@ API | |||
| 
 | ||||
| import contextlib | ||||
| import typing | ||||
| from timeit import default_timer | ||||
| from typing import Collection | ||||
| 
 | ||||
| import urllib3.connectionpool | ||||
|  | @ -83,9 +84,10 @@ from opentelemetry.instrumentation.utils import ( | |||
|     http_status_to_status_code, | ||||
|     unwrap, | ||||
| ) | ||||
| from opentelemetry.metrics import Histogram, get_meter | ||||
| from opentelemetry.propagate import inject | ||||
| 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.util.http.httplib import set_ip_on_next_http_connection | ||||
| 
 | ||||
|  | @ -135,8 +137,31 @@ class URLLib3Instrumentor(BaseInstrumentor): | |||
|         """ | ||||
|         tracer_provider = kwargs.get("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( | ||||
|             tracer, | ||||
|             duration_histogram, | ||||
|             request_size_histogram, | ||||
|             response_size_histogram, | ||||
|             request_hook=kwargs.get("request_hook"), | ||||
|             response_hook=kwargs.get("response_hook"), | ||||
|             url_filter=kwargs.get("url_filter"), | ||||
|  | @ -147,7 +172,10 @@ class URLLib3Instrumentor(BaseInstrumentor): | |||
| 
 | ||||
| 
 | ||||
| def _instrument( | ||||
|     tracer, | ||||
|     tracer: Tracer, | ||||
|     duration_histogram: Histogram, | ||||
|     request_size_histogram: Histogram, | ||||
|     response_size_histogram: Histogram, | ||||
|     request_hook: _RequestHookT = None, | ||||
|     response_hook: _ResponseHookT = None, | ||||
|     url_filter: _UrlFilterT = None, | ||||
|  | @ -175,11 +203,30 @@ def _instrument( | |||
|             inject(headers) | ||||
| 
 | ||||
|             with _suppress_further_instrumentation(): | ||||
|                 start_time = default_timer() | ||||
|                 response = wrapped(*args, **kwargs) | ||||
|                 elapsed_time = round((default_timer() - start_time) * 1000) | ||||
| 
 | ||||
|             _apply_response(span, response) | ||||
|             if callable(response_hook): | ||||
|                 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 | ||||
| 
 | ||||
|     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 | ||||
| def _suppress_further_instrumentation(): | ||||
|     token = context.attach( | ||||
|  |  | |||
|  | @ -14,3 +14,5 @@ | |||
| 
 | ||||
| 
 | ||||
| _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 | ||||
| # limitations under the License. | ||||
| 
 | ||||
| from timeit import default_timer | ||||
| 
 | ||||
| import urllib3 | ||||
| import urllib3.exceptions | ||||
| from urllib3.request import encode_multipart_formdata | ||||
| 
 | ||||
| from opentelemetry import trace | ||||
| from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor | ||||
|  | @ -84,3 +87,136 @@ class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase): | |||
|             "net.peer.ip": self.assert_ip, | ||||
|         } | ||||
|         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