diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c4e76aaf8..0848cf9f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3133](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3133)) - `opentelemetry-instrumentation-falcon` add support version to v4 ([#3086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3086)) +- `opentelemetry-instrumentation-falcon` Implement new HTTP semantic convention opt-in for Falcon + ([#2790](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2790)) - `opentelemetry-instrumentation-wsgi` always record span status code to have it available in metrics ([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148)) - add support to Python 3.13 diff --git a/instrumentation/README.md b/instrumentation/README.md index d1b383d5a6..75341dad9a 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -20,7 +20,7 @@ | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental | [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, < 5.0.0 | Yes | experimental +| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | migration | [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.42.0 | No | experimental diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 1037f98f5f..2b26c55cb1 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -193,6 +193,14 @@ def response_hook(span, req, resp): import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, + _StabilityMode, +) from opentelemetry.instrumentation.falcon.package import _instruments from opentelemetry.instrumentation.falcon.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -203,18 +211,22 @@ def response_hook(span, req, resp): from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, extract_attributes_from_object, - http_status_to_status_code, ) from opentelemetry.metrics import get_meter +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_ROUTE, +) from opentelemetry.semconv.metrics import MetricInstruments -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs _logger = getLogger(__name__) _ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key" +_ENVIRON_REQ_ATTRS = "opentelemetry-falcon.req_attrs" _ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key" _ENVIRON_TOKEN = "opentelemetry-falcon.token" _ENVIRON_EXC = "opentelemetry-falcon.exc" @@ -243,6 +255,10 @@ class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)): def __init__(self, *args, **kwargs): otel_opts = kwargs.pop("_otel_opts", {}) + self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + # inject trace middleware self._middlewares_list = kwargs.pop("middleware", []) if self._middlewares_list is None: @@ -257,19 +273,30 @@ def __init__(self, *args, **kwargs): __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) self._otel_meter = get_meter( __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", - ) - self.duration_histogram = self._otel_meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="Measures the duration of inbound HTTP requests.", + schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) + + self.duration_histogram_old = None + if _report_old(self._sem_conv_opt_in_mode): + self.duration_histogram_old = self._otel_meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="Measures the duration of inbound HTTP requests.", + ) + self.duration_histogram_new = None + if _report_new(self._sem_conv_opt_in_mode): + self.duration_histogram_new = self._otel_meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + description="Duration of HTTP server requests.", + unit="s", + ) + self.active_requests_counter = self._otel_meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", @@ -283,6 +310,7 @@ def __init__(self, *args, **kwargs): ), otel_opts.pop("request_hook", None), otel_opts.pop("response_hook", None), + self._sem_conv_opt_in_mode, ) self._middlewares_list.insert(0, trace_middleware) kwargs["middleware"] = self._middlewares_list @@ -343,11 +371,14 @@ def __call__(self, env, start_response): context_carrier=env, context_getter=otel_wsgi.wsgi_getter, ) - attributes = otel_wsgi.collect_request_attributes(env) + attributes = otel_wsgi.collect_request_attributes( + env, self._sem_conv_opt_in_mode + ) active_requests_count_attrs = ( - otel_wsgi._parse_active_request_count_attrs(attributes) + otel_wsgi._parse_active_request_count_attrs( + attributes, self._sem_conv_opt_in_mode + ) ) - duration_attrs = otel_wsgi._parse_duration_attrs(attributes) self.active_requests_counter.add(1, active_requests_count_attrs) if span.is_recording(): @@ -364,6 +395,7 @@ def __call__(self, env, start_response): activation.__enter__() env[_ENVIRON_SPAN_KEY] = span env[_ENVIRON_ACTIVATION_KEY] = activation + env[_ENVIRON_REQ_ATTRS] = attributes exception = None def _start_response(status, response_headers, *args, **kwargs): @@ -379,12 +411,22 @@ def _start_response(status, response_headers, *args, **kwargs): exception = exc raise finally: - if span.is_recording(): - duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( - span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + duration_s = default_timer() - start + if self.duration_histogram_old: + duration_attrs = otel_wsgi._parse_duration_attrs( + attributes, _StabilityMode.DEFAULT + ) + self.duration_histogram_old.record( + max(round(duration_s * 1000), 0), duration_attrs + ) + if self.duration_histogram_new: + duration_attrs = otel_wsgi._parse_duration_attrs( + attributes, _StabilityMode.HTTP + ) + self.duration_histogram_new.record( + max(duration_s, 0), duration_attrs ) - duration = max(round((default_timer() - start) * 1000), 0) - self.duration_histogram.record(duration, duration_attrs) + self.active_requests_counter.add(-1, active_requests_count_attrs) if exception is None: activation.__exit__(None, None, None) @@ -407,11 +449,13 @@ def __init__( traced_request_attrs=None, request_hook=None, response_hook=None, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): self.tracer = tracer self._traced_request_attrs = traced_request_attrs self._request_hook = request_hook self._response_hook = response_hook + self._sem_conv_opt_in_mode = sem_conv_opt_in_mode def process_request(self, req, resp): span = req.env.get(_ENVIRON_SPAN_KEY) @@ -437,58 +481,60 @@ def process_resource(self, req, resp, resource, params): def process_response(self, req, resp, resource, req_succeeded=None): # pylint:disable=R0201,R0912 span = req.env.get(_ENVIRON_SPAN_KEY) + req_attrs = req.env.get(_ENVIRON_REQ_ATTRS) - if not span or not span.is_recording(): + if not span: return status = resp.status - reason = None if resource is None: - status = "404" - reason = "NotFound" + status = falcon.HTTP_404 else: + exc_type, exc = None, None if _ENVIRON_EXC in req.env: exc = req.env[_ENVIRON_EXC] exc_type = type(exc) - else: - exc_type, exc = None, None + if exc_type and not req_succeeded: if "HTTPNotFound" in exc_type.__name__: - status = "404" - reason = "NotFound" + status = falcon.HTTP_404 + elif isinstance(exc, (falcon.HTTPError, falcon.HTTPStatus)): + try: + if _falcon_version > 2: + status = falcon.code_to_http_status(exc.status) + else: + status = exc.status + except ValueError: + status = falcon.HTTP_500 else: - status = "500" - reason = f"{exc_type.__name__}: {exc}" + status = falcon.HTTP_500 + + # Falcon 1 does not support response headers. So + # send an empty dict. + response_headers = {} + if _falcon_version > 1: + response_headers = resp.headers + + otel_wsgi.add_response_attributes( + span, + status, + response_headers, + req_attrs, + self._sem_conv_opt_in_mode, + ) - status = status.split(" ")[0] + if ( + _report_new(self._sem_conv_opt_in_mode) + and req.uri_template + and req_attrs is not None + ): + req_attrs[HTTP_ROUTE] = req.uri_template try: - status_code = int(status) - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) - otel_status_code = http_status_to_status_code( - status_code, server_span=True - ) - - # set the description only when the status code is ERROR - if otel_status_code is not StatusCode.ERROR: - reason = None - - span.set_status( - Status( - status_code=otel_status_code, - description=reason, - ) - ) - - # Falcon 1 does not support response headers. So - # send an empty dict. - response_headers = {} - if _falcon_version > 1: - response_headers = resp.headers - if span.is_recording() and span.kind == trace.SpanKind.SERVER: # Check if low-cardinality route is available as per semantic-conventions if req.uri_template: span.update_name(f"{req.method} {req.uri_template}") + span.set_attribute(HTTP_ROUTE, req.uri_template) else: span.update_name(f"{req.method}") diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/package.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/package.py index 440a6e25f2..74651ddc42 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/package.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/package.py @@ -16,3 +16,5 @@ _instruments = ("falcon >= 1.4.1, < 5.0.0",) _supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index f940deb34e..48cbdbe3f8 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -22,7 +22,11 @@ from opentelemetry import trace 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.falcon import FalconInstrumentor @@ -36,6 +40,24 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.client_attributes import ( + CLIENT_PORT, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_PATH, + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -52,10 +74,33 @@ "http.server.active_requests", "http.server.duration", ] + _recommended_attrs = { + "http.server.active_requests": _server_active_requests_count_attrs_new + + _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_new + + _server_duration_attrs_old, +} + +_recommended_metrics_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old, } +_recommended_metrics_attrs_new = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, +} +_server_active_requests_count_attrs_both = ( + _server_active_requests_count_attrs_old +) +_server_active_requests_count_attrs_both.extend( + _server_active_requests_count_attrs_new +) +_recommended_metrics_attrs_both = { + "http.server.active_requests": _server_active_requests_count_attrs_both, + "http.server.duration": _server_duration_attrs_old, + "http.server.request.duration": _server_duration_attrs_new, +} _parsed_falcon_version = package_version.parse(_falcon_version) @@ -63,13 +108,26 @@ class TestFalconBase(TestBase): 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_FALCON_EXCLUDED_URLS": "ping", "OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS": "query_string", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) + + _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() FalconInstrumentor().instrument( @@ -95,26 +153,63 @@ def tearDown(self): self.env_patch.stop() +# pylint: disable=too-many-public-methods class TestFalconInstrumentation(TestFalconBase, WsgiTestBase): def test_get(self): self._test_method("GET") + def test_get_new_semconv(self): + self._test_method("GET", old_semconv=False, new_semconv=True) + + def test_get_both_semconv(self): + self._test_method("GET", old_semconv=True, new_semconv=True) + def test_post(self): self._test_method("POST") + def test_post_new_semconv(self): + self._test_method("POST", old_semconv=False, new_semconv=True) + + def test_post_both_semconv(self): + self._test_method("POST", old_semconv=True, new_semconv=True) + def test_patch(self): self._test_method("PATCH") + def test_patch_new_semconv(self): + self._test_method("PATCH", old_semconv=False, new_semconv=True) + + def test_patch_both_semconv(self): + self._test_method("PATCH", old_semconv=True, new_semconv=True) + def test_put(self): self._test_method("PUT") + def test_put_new_semconv(self): + self._test_method("PUT", old_semconv=False, new_semconv=True) + + def test_put_both_semconv(self): + self._test_method("PUT", old_semconv=True, new_semconv=True) + def test_delete(self): self._test_method("DELETE") + def test_delete_new_semconv(self): + self._test_method("DELETE", old_semconv=False, new_semconv=True) + + def test_delete_both_semconv(self): + self._test_method("DELETE", old_semconv=True, new_semconv=True) + def test_head(self): self._test_method("HEAD") - def _test_method(self, method): + def test_head_new_semconv(self): + self._test_method("HEAD", old_semconv=False, new_semconv=True) + + def test_head_both_semconv(self): + self._test_method("HEAD", old_semconv=True, new_semconv=True) + + def _test_method(self, method, old_semconv=True, new_semconv=False): self.client().simulate_request(method=method, path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -125,23 +220,42 @@ def _test_method(self, method): span.status.description, None, ) - self.assertSpanHasAttributes( - span, - { - SpanAttributes.HTTP_METHOD: method, - SpanAttributes.HTTP_SERVER_NAME: "falconframework.org", - SpanAttributes.HTTP_SCHEME: "http", - SpanAttributes.NET_HOST_PORT: 80, - SpanAttributes.HTTP_HOST: "falconframework.org", - SpanAttributes.HTTP_TARGET: "/" - if self._has_fixed_http_target - else "/hello", - SpanAttributes.NET_PEER_PORT: 65133, - SpanAttributes.HTTP_FLAVOR: "1.1", - "falcon.resource": "HelloWorldResource", - SpanAttributes.HTTP_STATUS_CODE: 201, - }, - ) + + expected_attributes = {} + expected_attributes_old = { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_SERVER_NAME: "falconframework.org", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.HTTP_HOST: "falconframework.org", + SpanAttributes.HTTP_TARGET: "/" + if self._has_fixed_http_target + else "/hello", + SpanAttributes.NET_PEER_PORT: 65133, + SpanAttributes.HTTP_FLAVOR: "1.1", + "falcon.resource": "HelloWorldResource", + SpanAttributes.HTTP_STATUS_CODE: 201, + SpanAttributes.HTTP_ROUTE: "/hello", + } + expected_attributes_new = { + HTTP_REQUEST_METHOD: method, + SERVER_ADDRESS: "falconframework.org", + URL_SCHEME: "http", + SERVER_PORT: 80, + URL_PATH: "/" if self._has_fixed_http_target else "/hello", + CLIENT_PORT: 65133, + NETWORK_PROTOCOL_VERSION: "1.1", + "falcon.resource": "HelloWorldResource", + HTTP_RESPONSE_STATUS_CODE: 201, + SpanAttributes.HTTP_ROUTE: "/hello", + } + + if old_semconv: + expected_attributes.update(expected_attributes_old) + if new_semconv: + expected_attributes.update(expected_attributes_new) + + self.assertSpanHasAttributes(span, expected_attributes) # In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1 # In falcon>=3, NET_PEER_IP is not set to anything by default # https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 # noqa @@ -193,10 +307,16 @@ def test_500(self): self.assertEqual(span.name, "GET /error") self.assertFalse(span.status.is_ok) self.assertEqual(span.status.status_code, StatusCode.ERROR) - self.assertEqual( - span.status.description, - "NameError: name 'non_existent_var' is not defined", - ) + + _parsed_falcon_version = package_version.parse(_falcon_version) + if _parsed_falcon_version < package_version.parse("3.0.0"): + self.assertEqual( + span.status.description, + "NameError: name 'non_existent_var' is not defined", + ) + else: + self.assertEqual(span.status.description, None) + self.assertSpanHasAttributes( span, { @@ -211,6 +331,7 @@ def test_500(self): SpanAttributes.NET_PEER_PORT: 65133, SpanAttributes.HTTP_FLAVOR: "1.1", SpanAttributes.HTTP_STATUS_CODE: 500, + SpanAttributes.HTTP_ROUTE: "/error", }, ) # In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1 @@ -221,6 +342,47 @@ def test_500(self): span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1" ) + def test_url_template_new_semconv(self): + self.client().simulate_get("/user/123") + spans = self.memory_exporter.get_finished_spans() + metrics_list = self.memory_metrics_reader.get_metrics_data() + + self.assertEqual(len(spans), 1) + self.assertTrue(len(metrics_list.resource_metrics) != 0) + span = spans[0] + self.assertEqual(span.name, "GET /user/{user_id}") + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "falconframework.org", + URL_SCHEME: "http", + SERVER_PORT: 80, + URL_PATH: "/" if self._has_fixed_http_target else "/user/123", + CLIENT_PORT: 65133, + NETWORK_PROTOCOL_VERSION: "1.1", + "falcon.resource": "UserResource", + HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_ROUTE: "/user/{user_id}", + }, + ) + + for resource_metric in metrics_list.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + if metric.name == "http.server.request.duration": + data_points = list(metric.data.data_points) + for point in data_points: + self.assertIn( + "http.route", + point.attributes, + ) + def test_url_template(self): self.client().simulate_get("/user/123") spans = self.memory_exporter.get_finished_spans() @@ -247,6 +409,7 @@ def test_url_template(self): SpanAttributes.HTTP_FLAVOR: "1.1", "falcon.resource": "UserResource", SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_ROUTE: "/user/{user_id}", }, ) @@ -305,6 +468,27 @@ def test_traced_not_recording(self): self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) + metrics_list = self.memory_metrics_reader.get_metrics_data() + self.assertTrue(len(metrics_list.resource_metrics) != 0) + + for resource_metric in metrics_list.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_old[ + metric.name + ], + ) + def test_uninstrument_after_instrument(self): self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() @@ -345,47 +529,119 @@ def test_falcon_metrics(self): ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) + def test_falcon_metric_values_new_semconv(self): + number_data_point_seen = False + histogram_data_point_seen = False + + start = default_timer() + self.client().simulate_get("/hello/756") + duration = max(default_timer() - start, 0) + + 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: + 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, 1) + histogram_data_point_seen = True + self.assertAlmostEqual( + duration, point.sum, delta=10 + ) + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_new[metric.name], + ) + + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_falcon_metric_values_both_semconv(self): + number_data_point_seen = False + histogram_data_point_seen = False + + start = default_timer() + self.client().simulate_get("/hello/756") + duration_s = default_timer() - start + + metrics_list = self.memory_metrics_reader.get_metrics_data() + + # pylint: disable=too-many-nested-blocks + for resource_metric in metrics_list.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + if metric.unit == "ms": + self.assertEqual(metric.name, "http.server.duration") + elif metric.unit == "s": + self.assertEqual( + metric.name, "http.server.request.duration" + ) + else: + self.assertEqual( + metric.name, "http.server.active_requests" + ) + 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, 1) + if metric.unit == "ms": + self.assertAlmostEqual( + max(round(duration_s * 1000), 0), + point.sum, + delta=10, + ) + elif metric.unit == "s": + self.assertAlmostEqual( + max(duration_s, 0), point.sum, delta=10 + ) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + self.assertEqual(point.value, 0) + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_both[metric.name], + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + def test_falcon_metric_values(self): - expected_duration_attributes = { - "http.method": "GET", - "http.host": "falconframework.org", - "http.scheme": "http", - "http.flavor": "1.1", - "http.server_name": "falconframework.org", - "net.host.port": 80, - "net.host.name": "falconframework.org", - "http.status_code": 404, - } - expected_requests_count_attributes = { - "http.method": "GET", - "http.host": "falconframework.org", - "http.scheme": "http", - "http.flavor": "1.1", - "http.server_name": "falconframework.org", - } + number_data_point_seen = False + histogram_data_point_seen = False + start = default_timer() self.client().simulate_get("/hello/756") duration = max(round((default_timer() - start) * 1000), 0) + 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: + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) 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) + histogram_data_point_seen = True self.assertAlmostEqual( duration, point.sum, delta=10 ) if isinstance(point, NumberDataPoint): - self.assertDictEqual( - expected_requests_count_attributes, - dict(point.attributes), - ) self.assertEqual(point.value, 0) + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_old[metric.name], + ) + + self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_metric_uninstrument(self): self.client().simulate_request(method="POST", path="/hello/756")