diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d77f06e0..b6882cbe57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) - `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) +- `opentelemetry-instrumentation-fastapi` Implement new semantic convention opt-in with stable http semantic conventions + ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions ([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631)) - `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. @@ -32,9 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) -- Populate `{method}` as `HTTP` on `_OTHER` methods from scope +- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - + - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware + ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) ### Fixed diff --git a/instrumentation/README.md b/instrumentation/README.md index eb21717843..9add6d0228 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -19,7 +19,7 @@ | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental | [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental | [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental -| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental +| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | migration | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration | [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental | [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 4c673d214a..2f9bbe7314 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -177,6 +177,12 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A import fastapi from starlette.routing import Match +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, +) from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi.types import ( ClientRequestHook, @@ -189,7 +195,11 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A from opentelemetry.metrics import get_meter from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import get_tracer -from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls +from opentelemetry.util.http import ( + get_excluded_urls, + parse_excluded_urls, + sanitize_method, +) _excluded_urls_from_env = get_excluded_urls("FASTAPI") _logger = logging.getLogger(__name__) @@ -218,6 +228,11 @@ def instrument_app( app._is_instrumented_by_opentelemetry = False if not getattr(app, "_is_instrumented_by_opentelemetry", False): + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) if excluded_urls is None: excluded_urls = _excluded_urls_from_env else: @@ -226,13 +241,13 @@ def instrument_app( __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) meter = get_meter( __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) app.add_middleware( @@ -303,6 +318,7 @@ class _InstrumentedFastAPI(fastapi.FastAPI): _client_request_hook: ClientRequestHook = None _client_response_hook: ClientResponseHook = None _instrumented_fastapi_apps = set() + _sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -310,13 +326,17 @@ def __init__(self, *args, **kwargs): __name__, __version__, _InstrumentedFastAPI._tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url( + _InstrumentedFastAPI._sem_conv_opt_in_mode + ), ) meter = get_meter( __name__, __version__, _InstrumentedFastAPI._meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url( + _InstrumentedFastAPI._sem_conv_opt_in_mode + ), ) self.add_middleware( OpenTelemetryMiddleware, @@ -373,8 +393,10 @@ def _get_default_span_details(scope): A tuple of span name and attributes """ route = _get_route_details(scope) - method = scope.get("method", "") + method = sanitize_method(scope.get("method", "").strip()) attributes = {} + if method == "_OTHER": + method = "HTTP" if route: attributes[SpanAttributes.HTTP_ROUTE] = route if method and route: # http diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py index 8df84fc931..d95a2cf6d5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py @@ -16,3 +16,5 @@ _instruments = ("fastapi ~= 0.58",) _supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 0ad63164d5..69c5d312e5 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -11,6 +11,9 @@ # 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. + +# pylint: disable=too-many-lines + import unittest from timeit import default_timer from unittest.mock import patch @@ -20,39 +23,77 @@ from fastapi.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase -from opentelemetry.util.http import ( - _active_requests_count_attrs, - _duration_attrs, - get_excluded_urls, -) +from opentelemetry.util.http import get_excluded_urls -_expected_metric_names = [ +_expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", "http.server.response.size", "http.server.request.size", ] -_recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": {*_duration_attrs, SpanAttributes.HTTP_TARGET}, +_expected_metric_names_new = [ + "http.server.active_requests", + "http.server.request.duration", + "http.server.response.body.size", + "http.server.request.body.size", +] +_expected_metric_names_both = _expected_metric_names_old +_expected_metric_names_both.extend(_expected_metric_names_new) + +_recommended_attrs_old = { + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": { + *_server_duration_attrs_old, + SpanAttributes.HTTP_TARGET, + }, "http.server.response.size": { - *_duration_attrs, + *_server_duration_attrs_old, SpanAttributes.HTTP_TARGET, }, "http.server.request.size": { - *_duration_attrs, + *_server_duration_attrs_old, SpanAttributes.HTTP_TARGET, }, } +_recommended_attrs_new = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, + "http.server.response.body.size": _server_duration_attrs_new, + "http.server.request.body.size": _server_duration_attrs_new, +} + +_recommended_attrs_both = _recommended_attrs_old.copy() +_recommended_attrs_both.update(_recommended_attrs_new) +_recommended_attrs_both["http.server.active_requests"].extend( + _server_active_requests_count_attrs_old +) + class TestBaseFastAPI(TestBase): def _create_app(self): @@ -88,10 +129,23 @@ def setUpClass(cls): def setUp(self): super().setUp() + + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", - {"OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz"}, + { + "OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, + }, ) + _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.fastapi._excluded_urls_from_env", @@ -142,7 +196,6 @@ async def _(): class TestBaseManualFastAPI(TestBaseFastAPI): - @classmethod def setUpClass(cls): if cls is TestBaseManualFastAPI: @@ -196,7 +249,6 @@ def test_sub_app_fastapi_call(self): class TestBaseAutoFastAPI(TestBaseFastAPI): - @classmethod def setUpClass(cls): if cls is TestBaseAutoFastAPI: @@ -259,6 +311,7 @@ def test_sub_app_fastapi_call(self): ) +# pylint: disable=too-many-public-methods class TestFastAPIManualInstrumentation(TestBaseManualFastAPI): def test_instrument_app_with_instrument(self): if not isinstance(self, TestAutoInstrumentation): @@ -358,7 +411,71 @@ def test_fastapi_metrics(self): ) self.assertTrue(len(scope_metric.metrics) == 3) for metric in scope_metric.metrics: - self.assertIn(metric.name, _expected_metric_names) + self.assertIn(metric.name, _expected_metric_names_old) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs_old[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_fastapi_metrics_new_semconv(self): + self._client.get("/foobar") + self._client.get("/foobar") + self._client.get("/foobar") + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) == 1) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) == 1) + for scope_metric in resource_metric.scope_metrics: + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.fastapi", + ) + self.assertTrue(len(scope_metric.metrics) == 3) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_new) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs_new[metric.name] + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_fastapi_metrics_both_semconv(self): + self._client.get("/foobar") + self._client.get("/foobar") + self._client.get("/foobar") + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) == 1) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) == 1) + for scope_metric in resource_metric.scope_metrics: + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.fastapi", + ) + self.assertTrue(len(scope_metric.metrics) == 5) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_both) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: @@ -369,7 +486,7 @@ def test_fastapi_metrics(self): number_data_point_seen = True for attr in point.attributes: self.assertIn( - attr, _recommended_attrs[metric.name] + attr, _recommended_attrs_both[metric.name] ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) @@ -378,21 +495,216 @@ def test_basic_metric_success(self): self._client.get("/foobar") duration = max(round((default_timer() - start) * 1000), 0) expected_duration_attributes = { - "http.method": "GET", - "http.host": "testserver:443", - "http.scheme": "https", - "http.flavor": "1.1", - "http.server_name": "testserver", - "net.host.port": 443, - "http.status_code": 200, - "http.target": "/foobar", + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + SpanAttributes.NET_HOST_PORT: 443, + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_TARGET: "/foobar", + } + expected_requests_count_attributes = { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=40) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_success_new_semconv(self): + start = default_timer() + self._client.get("/foobar") + duration_s = max(default_timer() - start, 0) + expected_duration_attributes = { + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "https", + NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_ROUTE: "/foobar", + } + expected_requests_count_attributes = { + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "https", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + elif metric.name == "http.server.response.body.size": + self.assertEqual(25, point.sum) + elif metric.name == "http.server.request.body.size": + self.assertEqual(25, point.sum) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_success_both_semconv(self): + start = default_timer() + self._client.get("/foobar") + duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) + expected_duration_attributes_old = { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + SpanAttributes.NET_HOST_PORT: 443, + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_TARGET: "/foobar", + } + expected_duration_attributes_new = { + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "https", + NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_ROUTE: "/foobar", + } + expected_requests_count_attributes = { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "https", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=40) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.response.body.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.request.body.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.duration": + self.assertAlmostEqual(duration, point.sum, delta=40) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.response.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.request.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_nonstandard_http_method_success(self): + start = default_timer() + self._client.request("NONSTANDARD", "/foobar") + duration = max(round((default_timer() - start) * 1000), 0) + expected_duration_attributes = { + SpanAttributes.HTTP_METHOD: "_OTHER", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + SpanAttributes.NET_HOST_PORT: 443, + SpanAttributes.HTTP_STATUS_CODE: 405, + SpanAttributes.HTTP_TARGET: "/foobar", + } + expected_requests_count_attributes = { + SpanAttributes.HTTP_METHOD: "_OTHER", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertDictEqual( + expected_duration_attributes, + dict(point.attributes), + ) + self.assertEqual(point.count, 1) + self.assertAlmostEqual(duration, point.sum, delta=40) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_nonstandard_http_method_success_new_semconv(self): + start = default_timer() + self._client.request("NONSTANDARD", "/foobar") + duration_s = max(default_timer() - start, 0) + expected_duration_attributes = { + HTTP_REQUEST_METHOD: "_OTHER", + URL_SCHEME: "https", + NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 405, + HTTP_ROUTE: "/foobar", } expected_requests_count_attributes = { - "http.method": "GET", - "http.host": "testserver:443", - "http.scheme": "https", - "http.flavor": "1.1", - "http.server_name": "testserver", + HTTP_REQUEST_METHOD: "_OTHER", + URL_SCHEME: "https", } metrics_list = self.memory_metrics_reader.get_metrics_data() for metric in ( @@ -404,8 +716,95 @@ def test_basic_metric_success(self): expected_duration_attributes, dict(point.attributes), ) + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + elif metric.name == "http.server.response.body.size": + self.assertEqual(31, point.sum) + elif metric.name == "http.server.request.body.size": + self.assertEqual(25, point.sum) + if isinstance(point, NumberDataPoint): + self.assertDictEqual( + expected_requests_count_attributes, + dict(point.attributes), + ) + self.assertEqual(point.value, 0) + + def test_basic_metric_nonstandard_http_method_success_both_semconv(self): + start = default_timer() + self._client.request("NONSTANDARD", "/foobar") + duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) + expected_duration_attributes_old = { + SpanAttributes.HTTP_METHOD: "_OTHER", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + SpanAttributes.NET_HOST_PORT: 443, + SpanAttributes.HTTP_STATUS_CODE: 405, + SpanAttributes.HTTP_TARGET: "/foobar", + } + expected_duration_attributes_new = { + HTTP_REQUEST_METHOD: "_OTHER", + URL_SCHEME: "https", + NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 405, + HTTP_ROUTE: "/foobar", + } + expected_requests_count_attributes = { + SpanAttributes.HTTP_METHOD: "_OTHER", + SpanAttributes.HTTP_HOST: "testserver:443", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SERVER_NAME: "testserver", + HTTP_REQUEST_METHOD: "_OTHER", + URL_SCHEME: "https", + } + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=40) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.response.body.size": + self.assertEqual(31, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.request.body.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_new, + dict(point.attributes), + ) + elif metric.name == "http.server.duration": + self.assertAlmostEqual(duration, point.sum, delta=40) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.response.size": + self.assertEqual(31, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) + elif metric.name == "http.server.request.size": + self.assertEqual(25, point.sum) + self.assertDictEqual( + expected_duration_attributes_old, + dict(point.attributes), + ) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, @@ -438,6 +837,63 @@ def test_basic_post_request_metric_success(self): if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) + def test_basic_post_request_metric_success_new_semconv(self): + start = default_timer() + response = self._client.post( + "/foobar", + json={"foo": "bar"}, + ) + duration_s = max(default_timer() - start, 0) + response_size = int(response.headers.get("content-length")) + request_size = int(response.request.headers.get("content-length")) + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + elif metric.name == "http.server.response.body.size": + self.assertEqual(response_size, point.sum) + elif metric.name == "http.server.request.body.size": + self.assertEqual(request_size, point.sum) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + + def test_basic_post_request_metric_success_both_semconv(self): + start = default_timer() + response = self._client.post( + "/foobar", + json={"foo": "bar"}, + ) + duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) + response_size = int(response.headers.get("content-length")) + request_size = int(response.request.headers.get("content-length")) + metrics_list = self.memory_metrics_reader.get_metrics_data() + for metric in ( + metrics_list.resource_metrics[0].scope_metrics[0].metrics + ): + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if metric.name == "http.server.request.duration": + self.assertAlmostEqual(duration_s, point.sum, places=1) + elif metric.name == "http.server.response.body.size": + self.assertEqual(response_size, point.sum) + elif metric.name == "http.server.request.body.size": + self.assertEqual(request_size, point.sum) + elif metric.name == "http.server.duration": + self.assertAlmostEqual(duration, point.sum, delta=40) + elif metric.name == "http.server.response.size": + self.assertEqual(response_size, point.sum) + elif metric.name == "http.server.request.size": + self.assertEqual(request_size, point.sum) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + def test_metric_uninstrument_app(self): self._client.get("/foobar") self._instrumentor.uninstrument_app(self._app) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index 34e9b5ea50..eaf6c79506 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -543,7 +543,9 @@ def __init__(self, *args, **kwargs): __name__, __version__, _InstrumentedFlask._meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url( + _InstrumentedFlask._sem_conv_opt_in_mode + ), ) duration_histogram_old = None if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode): @@ -579,7 +581,9 @@ def __init__(self, *args, **kwargs): __name__, __version__, _InstrumentedFlask._tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url( + _InstrumentedFlask._sem_conv_opt_in_mode + ), ) _before_request = _wrapped_before_request(