diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c983ac6f..5680b7aa0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `opentelemetry-instrumentation-requests` Support explicit_bucket_boundaries_advisory in duration metrics + ([#3464](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3464)) - `opentelemetry-instrumentation-redis` Add support for redis client-specific instrumentation. ([#3143](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3143)) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 1940e2f64b..9557805f9f 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -57,6 +57,22 @@ def response_hook(span, request_obj, response): request_hook=request_hook, response_hook=response_hook ) +Custom Duration Histogram Boundaries +************************************ +To customize the duration histogram bucket boundaries used for HTTP client request duration metrics, +you can provide a list of values when instrumenting: + +.. code:: python + + import requests + from opentelemetry.instrumentation.requests import RequestsInstrumentor + + custom_boundaries = [0.0, 5.0, 10.0, 25.0, 50.0, 100.0] + + RequestsInstrumentor().instrument( + duration_histogram_boundaries=custom_boundaries + ) + Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_REQUESTS_EXCLUDED_URLS`` @@ -87,6 +103,8 @@ def response_hook(span, request_obj, response): from requests.structures import CaseInsensitiveDict from opentelemetry.instrumentation._semconv import ( + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, @@ -410,8 +428,8 @@ def _instrument(self, **kwargs: Any): ``tracer_provider``: a TracerProvider, defaults to global ``request_hook``: An optional callback 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. - ``excluded_urls``: A string containing a comma-delimited - list of regexes used to exclude URLs from tracking + ``excluded_urls``: A string containing a comma-delimited list of regexes used to exclude URLs from tracking + ``duration_histogram_boundaries``: A list of float values representing the explicit bucket boundaries for the duration histogram. """ semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, @@ -426,6 +444,9 @@ def _instrument(self, **kwargs: Any): ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") + duration_histogram_boundaries = kwargs.get( + "duration_histogram_boundaries" + ) meter = get_meter( __name__, __version__, @@ -438,6 +459,8 @@ def _instrument(self, **kwargs: Any): name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration of the outbound HTTP request", + explicit_bucket_boundaries_advisory=duration_histogram_boundaries + or HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) duration_histogram_new = None if _report_new(semconv_opt_in_mode): @@ -445,6 +468,8 @@ def _instrument(self, **kwargs: Any): name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", + explicit_bucket_boundaries_advisory=duration_histogram_boundaries + or HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) _instrument( tracer, diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index efe9f75f56..f44e81383d 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -23,6 +23,8 @@ import opentelemetry.instrumentation.requests from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) @@ -123,6 +125,7 @@ def setUp(self): def tearDown(self): super().tearDown() self.env_patch.stop() + _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().uninstrument() httpretty.disable() @@ -730,6 +733,7 @@ def setUp(self): def tearDown(self): super().tearDown() self.env_patch.stop() + _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().uninstrument() httpretty.disable() @@ -762,6 +766,10 @@ def test_basic_metric_success(self): "measures the duration of the outbound HTTP request", ) for data_point in metric.data.data_points: + self.assertEqual( + data_point.explicit_bounds, + HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, + ) self.assertDictEqual( expected_attributes, dict(data_point.attributes) ) @@ -788,6 +796,10 @@ def test_basic_metric_new_semconv(self): metric.description, "Duration of HTTP client requests." ) for data_point in metric.data.data_points: + self.assertEqual( + data_point.explicit_bounds, + HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, + ) self.assertDictEqual( expected_attributes, dict(data_point.attributes) ) @@ -833,6 +845,38 @@ def test_basic_metric_both_semconv(self): ) self.assertEqual(data_point.count, 1) + def test_custom_histogram_boundaries(self): + RequestsInstrumentor().uninstrument() + custom_boundaries = (0.0, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0) + meter_provider, memory_reader = self.create_meter_provider() + RequestsInstrumentor().instrument( + meter_provider=meter_provider, + duration_histogram_boundaries=custom_boundaries, + ) + + self.perform_request(self.URL) + metrics = memory_reader.get_metrics_data().resource_metrics[0] + self.assertEqual(len(metrics.scope_metrics), 1) + data_point = metrics.scope_metrics[0].metrics[0].data.data_points[0] + self.assertEqual(data_point.explicit_bounds, custom_boundaries) + self.assertEqual(data_point.count, 1) + + def test_custom_histogram_boundaries_new_semconv(self): + RequestsInstrumentor().uninstrument() + custom_boundaries = (0.0, 5.0, 10.0, 25.0, 50.0, 100.0) + meter_provider, memory_reader = self.create_meter_provider() + RequestsInstrumentor().instrument( + meter_provider=meter_provider, + duration_histogram_boundaries=custom_boundaries, + ) + + self.perform_request(self.URL) + metrics = memory_reader.get_metrics_data().resource_metrics[0] + self.assertEqual(len(metrics.scope_metrics), 1) + data_point = metrics.scope_metrics[0].metrics[0].data.data_points[0] + self.assertEqual(data_point.explicit_bounds, custom_boundaries) + self.assertEqual(data_point.count, 1) + def test_basic_metric_non_recording_span(self): expected_attributes = { SpanAttributes.HTTP_STATUS_CODE: 200, diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index f75ca114a4..f3f3fb6201 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -47,6 +47,43 @@ from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode +# Values defined in milliseconds +HTTP_DURATION_HISTOGRAM_BUCKETS_OLD = ( + 0.0, + 5.0, + 10.0, + 25.0, + 50.0, + 75.0, + 100.0, + 250.0, + 500.0, + 750.0, + 1000.0, + 2500.0, + 5000.0, + 7500.0, + 10000.0, +) + +# Values defined in seconds +HTTP_DURATION_HISTOGRAM_BUCKETS_NEW = ( + 0.005, + 0.01, + 0.025, + 0.05, + 0.075, + 0.1, + 0.25, + 0.5, + 0.75, + 1, + 2.5, + 5, + 7.5, + 10, +) + # These lists represent attributes for metrics that are currently supported _client_duration_attrs_old = [