Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package 
python-opentelemetry-instrumentation-asgi for openSUSE:Factory checked in at 
2026-03-31 15:23:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-opentelemetry-instrumentation-asgi 
(Old)
 and      
/work/SRC/openSUSE:Factory/.python-opentelemetry-instrumentation-asgi.new.1999 
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-opentelemetry-instrumentation-asgi"

Tue Mar 31 15:23:04 2026 rev:8 rq:1343794 version:0.61b0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-opentelemetry-instrumentation-asgi/python-opentelemetry-instrumentation-asgi.changes
      2026-01-06 17:43:58.896142421 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-opentelemetry-instrumentation-asgi.new.1999/python-opentelemetry-instrumentation-asgi.changes
    2026-03-31 15:24:09.424989098 +0200
@@ -1,0 +2,6 @@
+Mon Mar 23 22:12:01 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.61b0:
+  * Add Python 3.14 support
+
+-------------------------------------------------------------------

Old:
----
  opentelemetry_instrumentation_asgi-0.60b0.tar.gz

New:
----
  opentelemetry_instrumentation_asgi-0.61b0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-opentelemetry-instrumentation-asgi.spec ++++++
--- /var/tmp/diff_new_pack.GFPWnS/_old  2026-03-31 15:24:10.093016928 +0200
+++ /var/tmp/diff_new_pack.GFPWnS/_new  2026-03-31 15:24:10.093016928 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-opentelemetry-instrumentation-asgi
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-opentelemetry-instrumentation-asgi%{?psuffix}
-Version:        0.60b0
+Version:        0.61b0
 Release:        0
 Summary:        ASGI instrumentation for OpenTelemetry
 License:        Apache-2.0

++++++ opentelemetry_instrumentation_asgi-0.60b0.tar.gz -> 
opentelemetry_instrumentation_asgi-0.61b0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/opentelemetry_instrumentation_asgi-0.60b0/PKG-INFO 
new/opentelemetry_instrumentation_asgi-0.61b0/PKG-INFO
--- old/opentelemetry_instrumentation_asgi-0.60b0/PKG-INFO      2020-02-02 
01:00:00.000000000 +0100
+++ new/opentelemetry_instrumentation_asgi-0.61b0/PKG-INFO      2020-02-02 
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: opentelemetry-instrumentation-asgi
-Version: 0.60b0
+Version: 0.61b0
 Summary: ASGI instrumentation for OpenTelemetry
 Project-URL: Homepage, 
https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asgi
 Project-URL: Repository, 
https://github.com/open-telemetry/opentelemetry-python-contrib
@@ -17,12 +17,13 @@
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Requires-Python: >=3.9
 Requires-Dist: asgiref~=3.0
 Requires-Dist: opentelemetry-api~=1.12
-Requires-Dist: opentelemetry-instrumentation==0.60b0
-Requires-Dist: opentelemetry-semantic-conventions==0.60b0
-Requires-Dist: opentelemetry-util-http==0.60b0
+Requires-Dist: opentelemetry-instrumentation==0.61b0
+Requires-Dist: opentelemetry-semantic-conventions==0.61b0
+Requires-Dist: opentelemetry-util-http==0.61b0
 Provides-Extra: instruments
 Requires-Dist: asgiref~=3.0; extra == 'instruments'
 Description-Content-Type: text/x-rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_instrumentation_asgi-0.60b0/pyproject.toml 
new/opentelemetry_instrumentation_asgi-0.61b0/pyproject.toml
--- old/opentelemetry_instrumentation_asgi-0.60b0/pyproject.toml        
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_instrumentation_asgi-0.61b0/pyproject.toml        
2020-02-02 01:00:00.000000000 +0100
@@ -23,13 +23,14 @@
   "Programming Language :: Python :: 3.11",
   "Programming Language :: Python :: 3.12",
   "Programming Language :: Python :: 3.13",
+  "Programming Language :: Python :: 3.14",
 ]
 dependencies = [
   "asgiref ~= 3.0",
   "opentelemetry-api ~= 1.12",
-  "opentelemetry-instrumentation == 0.60b0",
-  "opentelemetry-semantic-conventions == 0.60b0",
-  "opentelemetry-util-http == 0.60b0",
+  "opentelemetry-instrumentation == 0.61b0",
+  "opentelemetry-semantic-conventions == 0.61b0",
+  "opentelemetry-util-http == 0.61b0",
 ]
 
 [project.optional-dependencies]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_instrumentation_asgi-0.60b0/src/opentelemetry/instrumentation/asgi/__init__.py
 
new/opentelemetry_instrumentation_asgi-0.61b0/src/opentelemetry/instrumentation/asgi/__init__.py
--- 
old/opentelemetry_instrumentation_asgi-0.60b0/src/opentelemetry/instrumentation/asgi/__init__.py
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/opentelemetry_instrumentation_asgi-0.61b0/src/opentelemetry/instrumentation/asgi/__init__.py
    2020-02-02 01:00:00.000000000 +0100
@@ -258,6 +258,10 @@
 from opentelemetry.instrumentation.utils import _start_internal_or_server_span
 from opentelemetry.metrics import get_meter
 from opentelemetry.propagators.textmap import Getter, Setter
+from opentelemetry.semconv._incubating.attributes.http_attributes import (
+    HTTP_SERVER_NAME,
+    HTTP_TARGET,
+)
 from opentelemetry.semconv._incubating.attributes.user_agent_attributes import 
(
     USER_AGENT_SYNTHETIC_TYPE,
 )
@@ -270,7 +274,6 @@
 from opentelemetry.semconv.metrics.http_metrics import (
     HTTP_SERVER_REQUEST_DURATION,
 )
-from opentelemetry.semconv.trace import SpanAttributes
 from opentelemetry.trace import Span, set_span_in_context
 from opentelemetry.util.http import (
     OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
@@ -283,6 +286,7 @@
     get_custom_headers,
     normalise_request_header_name,
     normalise_response_header_name,
+    normalize_user_agent,
     parse_excluded_urls,
     redact_url,
     sanitize_method,
@@ -396,13 +400,15 @@
     http_host_value_list = asgi_getter.get(scope, "host")
     if http_host_value_list:
         if _report_old(sem_conv_opt_in_mode):
-            result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
-                http_host_value_list
-            )
+            result[HTTP_SERVER_NAME] = ",".join(http_host_value_list)
     http_user_agent = asgi_getter.get(scope, "user-agent")
     if http_user_agent:
-        user_agent_value = http_user_agent[0]
-        _set_http_user_agent(result, user_agent_value, sem_conv_opt_in_mode)
+        user_agent_raw = http_user_agent[0]
+        user_agent_value = normalize_user_agent(user_agent_raw)
+        if user_agent_value:
+            _set_http_user_agent(
+                result, user_agent_value, sem_conv_opt_in_mode
+            )
 
         # Check for synthetic user agent type
         synthetic_type = detect_synthetic_user_agent(user_agent_value)
@@ -820,17 +826,22 @@
                     attributes, _StabilityMode.DEFAULT
                 )
                 if target:
-                    duration_attrs_old[SpanAttributes.HTTP_TARGET] = target
+                    duration_attrs_old[HTTP_TARGET] = target
                 duration_attrs_new = _parse_duration_attrs(
                     attributes, _StabilityMode.HTTP
                 )
+                span_ctx = set_span_in_context(span)
                 if self.duration_histogram_old:
                     self.duration_histogram_old.record(
-                        max(round(duration_s * 1000), 0), duration_attrs_old
+                        max(round(duration_s * 1000), 0),
+                        duration_attrs_old,
+                        context=span_ctx,
                     )
                 if self.duration_histogram_new:
                     self.duration_histogram_new.record(
-                        max(duration_s, 0), duration_attrs_new
+                        max(duration_s, 0),
+                        duration_attrs_new,
+                        context=span_ctx,
                     )
                 self.active_requests_counter.add(
                     -1, active_requests_count_attrs
@@ -838,11 +849,15 @@
                 if self.content_length_header:
                     if self.server_response_size_histogram:
                         self.server_response_size_histogram.record(
-                            self.content_length_header, duration_attrs_old
+                            self.content_length_header,
+                            duration_attrs_old,
+                            context=span_ctx,
                         )
                     if self.server_response_body_size_histogram:
                         self.server_response_body_size_histogram.record(
-                            self.content_length_header, duration_attrs_new
+                            self.content_length_header,
+                            duration_attrs_new,
+                            context=span_ctx,
                         )
 
                 request_size = asgi_getter.get(scope, "content-length")
@@ -854,11 +869,15 @@
                     else:
                         if self.server_request_size_histogram:
                             self.server_request_size_histogram.record(
-                                request_size_amount, duration_attrs_old
+                                request_size_amount,
+                                duration_attrs_old,
+                                context=span_ctx,
                             )
                         if self.server_request_body_size_histogram:
                             self.server_request_body_size_histogram.record(
-                                request_size_amount, duration_attrs_new
+                                request_size_amount,
+                                duration_attrs_new,
+                                context=span_ctx,
                             )
             if token:
                 context.detach(token)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_instrumentation_asgi-0.60b0/src/opentelemetry/instrumentation/asgi/version.py
 
new/opentelemetry_instrumentation_asgi-0.61b0/src/opentelemetry/instrumentation/asgi/version.py
--- 
old/opentelemetry_instrumentation_asgi-0.60b0/src/opentelemetry/instrumentation/asgi/version.py
     2020-02-02 01:00:00.000000000 +0100
+++ 
new/opentelemetry_instrumentation_asgi-0.61b0/src/opentelemetry/instrumentation/asgi/version.py
     2020-02-02 01:00:00.000000000 +0100
@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "0.60b0"
+__version__ = "0.61b0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_instrumentation_asgi-0.60b0/tests/test_asgi_middleware.py 
new/opentelemetry_instrumentation_asgi-0.61b0/tests/test_asgi_middleware.py
--- old/opentelemetry_instrumentation_asgi-0.60b0/tests/test_asgi_middleware.py 
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_instrumentation_asgi-0.61b0/tests/test_asgi_middleware.py 
2020-02-02 01:00:00.000000000 +0100
@@ -42,6 +42,22 @@
     HistogramDataPoint,
     NumberDataPoint,
 )
+from opentelemetry.semconv._incubating.attributes.http_attributes import (
+    HTTP_FLAVOR,
+    HTTP_HOST,
+    HTTP_METHOD,
+    HTTP_SCHEME,
+    HTTP_SERVER_NAME,
+    HTTP_STATUS_CODE,
+    HTTP_TARGET,
+    HTTP_URL,
+    HTTP_USER_AGENT,
+)
+from opentelemetry.semconv._incubating.attributes.net_attributes import (
+    NET_HOST_PORT,
+    NET_PEER_IP,
+    NET_PEER_PORT,
+)
 from opentelemetry.semconv._incubating.attributes.user_agent_attributes import 
(
     USER_AGENT_SYNTHETIC_TYPE,
 )
@@ -68,7 +84,6 @@
 from opentelemetry.semconv.attributes.user_agent_attributes import (
     USER_AGENT_ORIGINAL,
 )
-from opentelemetry.semconv.trace import SpanAttributes
 from opentelemetry.test.asgitestutil import (
     AsyncAsgiTestBase,
     setup_testing_defaults,
@@ -291,6 +306,9 @@
     return hook
 
 
+SCOPE = "opentelemetry.instrumentation.asgi"
+
+
 # pylint: disable=too-many-public-methods
 class TestAsgiApplication(AsyncAsgiTestBase):
     def setUp(self):
@@ -315,6 +333,46 @@
 
         self.env_patch.start()
 
+    def subTest(self, msg=..., **params):
+        sub = super().subTest(msg, **params)
+        # Reinitialize test state to avoid state pollution
+        self.setUp()
+        return sub
+
+    # Helper to assert exemplars presence across specified histogram metric 
names.
+    def _assert_exemplars_present(
+        self, metric_names: set[str], context: str = ""
+    ):
+        metrics = self.get_sorted_metrics(SCOPE)
+
+        found = {name: 0 for name in metric_names}
+        for metric in metrics:
+            if metric.name not in metric_names:
+                continue
+            for point in metric.data.data_points:
+                found[metric.name] += 1
+                exemplars = getattr(point, "exemplars", None)
+                self.assertIsNotNone(
+                    exemplars,
+                    msg=f"Expected exemplars list attribute on histogram data 
point for {metric.name} ({context})",
+                )
+                self.assertGreater(
+                    len(exemplars or []),
+                    0,
+                    msg=f"Expected at least one exemplar on histogram data 
point for {metric.name} ({context}) but none found.",
+                )
+                for ex in exemplars or []:
+                    if hasattr(ex, "span_id"):
+                        self.assertNotEqual(ex.span_id, 0)
+                    if hasattr(ex, "trace_id"):
+                        self.assertNotEqual(ex.trace_id, 0)
+        for name, count in found.items():
+            self.assertGreater(
+                count,
+                0,
+                msg=f"Did not encounter any data points for metric {name} 
while checking exemplars ({context}).",
+            )
+
     # pylint: disable=too-many-locals
     def validate_outputs(
         self,
@@ -357,7 +415,7 @@
             self.assertIsNone(exc_info)
 
         # Check spans
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         expected_old = [
             {
                 "name": "GET / http receive",
@@ -368,7 +426,7 @@
                 "name": "GET / http send",
                 "kind": trace_api.SpanKind.INTERNAL,
                 "attributes": {
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                     "asgi.event.type": "http.response.start",
                 },
             },
@@ -381,16 +439,16 @@
                 "name": "GET /",
                 "kind": trace_api.SpanKind.SERVER,
                 "attributes": {
-                    SpanAttributes.HTTP_METHOD: "GET",
-                    SpanAttributes.HTTP_SCHEME: "http",
-                    SpanAttributes.NET_HOST_PORT: 80,
-                    SpanAttributes.HTTP_HOST: "127.0.0.1",
-                    SpanAttributes.HTTP_FLAVOR: "1.0",
-                    SpanAttributes.HTTP_TARGET: "/",
-                    SpanAttributes.HTTP_URL: "http://127.0.0.1/";,
-                    SpanAttributes.NET_PEER_IP: "127.0.0.1",
-                    SpanAttributes.NET_PEER_PORT: 32767,
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_METHOD: "GET",
+                    HTTP_SCHEME: "http",
+                    NET_HOST_PORT: 80,
+                    HTTP_HOST: "127.0.0.1",
+                    HTTP_FLAVOR: "1.0",
+                    HTTP_TARGET: "/",
+                    HTTP_URL: "http://127.0.0.1/";,
+                    NET_PEER_IP: "127.0.0.1",
+                    NET_PEER_PORT: 32767,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
         ]
@@ -439,7 +497,7 @@
                 "name": "GET / http send",
                 "kind": trace_api.SpanKind.INTERNAL,
                 "attributes": {
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                     HTTP_RESPONSE_STATUS_CODE: 200,
                     "asgi.event.type": "http.response.start",
                 },
@@ -462,16 +520,16 @@
                     CLIENT_ADDRESS: "127.0.0.1",
                     CLIENT_PORT: 32767,
                     HTTP_RESPONSE_STATUS_CODE: 200,
-                    SpanAttributes.HTTP_METHOD: "GET",
-                    SpanAttributes.HTTP_SCHEME: "http",
-                    SpanAttributes.NET_HOST_PORT: 80,
-                    SpanAttributes.HTTP_HOST: "127.0.0.1",
-                    SpanAttributes.HTTP_FLAVOR: "1.0",
-                    SpanAttributes.HTTP_TARGET: "/",
-                    SpanAttributes.HTTP_URL: "http://127.0.0.1/";,
-                    SpanAttributes.NET_PEER_IP: "127.0.0.1",
-                    SpanAttributes.NET_PEER_PORT: 32767,
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_METHOD: "GET",
+                    HTTP_SCHEME: "http",
+                    NET_HOST_PORT: 80,
+                    HTTP_HOST: "127.0.0.1",
+                    HTTP_FLAVOR: "1.0",
+                    HTTP_TARGET: "/",
+                    HTTP_URL: "http://127.0.0.1/";,
+                    NET_PEER_IP: "127.0.0.1",
+                    NET_PEER_PORT: 32767,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
         ]
@@ -493,7 +551,7 @@
             self.assertDictEqual(dict(span.attributes), expected["attributes"])
             self.assertEqual(
                 span.instrumentation_scope.name,
-                "opentelemetry.instrumentation.asgi",
+                SCOPE,
             )
             if "events" in expected:
                 self.assertEqual(len(span.events), len(expected["events"]))
@@ -583,7 +641,7 @@
         await self.send_default_request()
         outputs = await self.get_all_output()
         self.validate_outputs(outputs)
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         server_span = span_list[-1]
         assert server_span.kind == SpanKind.SERVER
         span_duration_nanos = server_span.end_time - server_span.start_time
@@ -610,7 +668,7 @@
             self.seed_app(app)
             await self.send_default_request()
             await self.get_all_output()
-            span_list = self.memory_exporter.get_finished_spans()
+            span_list = self.get_finished_spans()
             self.assertTrue(span_list)
             for span in span_list:
                 for excluded_span in excluded_spans:
@@ -642,7 +700,7 @@
             return expected
 
         self.validate_outputs(outputs, modifiers=[add_body_and_trailer_span])
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         server_span = span_list[-1]
         assert server_span.kind == SpanKind.SERVER
         span_duration_nanos = server_span.end_time - server_span.start_time
@@ -703,7 +761,7 @@
         self.assertEqual(response_body["body"], b"*")
         self.assertEqual(response_start["status"], 200)
 
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         self.assertEqual(len(span_list), 0)
 
     async def test_behavior_with_scope_server_as_none(self):
@@ -712,9 +770,9 @@
         def update_expected_server(expected):
             expected[3]["attributes"].update(
                 {
-                    SpanAttributes.HTTP_HOST: "0.0.0.0",
-                    SpanAttributes.NET_HOST_PORT: 80,
-                    SpanAttributes.HTTP_URL: "http://0.0.0.0/";,
+                    HTTP_HOST: "0.0.0.0",
+                    NET_HOST_PORT: 80,
+                    HTTP_URL: "http://0.0.0.0/";,
                 }
             )
             return expected
@@ -756,9 +814,9 @@
         def update_expected_server(expected):
             expected[3]["attributes"].update(
                 {
-                    SpanAttributes.HTTP_HOST: "0.0.0.0",
-                    SpanAttributes.NET_HOST_PORT: 80,
-                    SpanAttributes.HTTP_URL: "http://0.0.0.0/";,
+                    HTTP_HOST: "0.0.0.0",
+                    NET_HOST_PORT: 80,
+                    HTTP_URL: "http://0.0.0.0/";,
                     SERVER_ADDRESS: "0.0.0.0",
                     SERVER_PORT: 80,
                 }
@@ -784,8 +842,8 @@
         def update_expected_server(expected):
             expected[3]["attributes"].update(
                 {
-                    SpanAttributes.HTTP_SERVER_NAME: hostname.decode("utf8"),
-                    SpanAttributes.HTTP_URL: 
f"http://{hostname.decode('utf8')}/",
+                    HTTP_SERVER_NAME: hostname.decode("utf8"),
+                    HTTP_URL: f"http://{hostname.decode('utf8')}/",
                 }
             )
             return expected
@@ -804,8 +862,8 @@
         def update_expected_server(expected):
             expected[3]["attributes"].update(
                 {
-                    SpanAttributes.HTTP_SERVER_NAME: hostname.decode("utf8"),
-                    SpanAttributes.HTTP_URL: 
f"http://{hostname.decode('utf8')}/",
+                    HTTP_SERVER_NAME: hostname.decode("utf8"),
+                    HTTP_URL: f"http://{hostname.decode('utf8')}/",
                 }
             )
             return expected
@@ -828,7 +886,7 @@
 
         def update_expected_user_agent(expected):
             expected[3]["attributes"].update(
-                {SpanAttributes.HTTP_USER_AGENT: user_agent.decode("utf8")}
+                {HTTP_USER_AGENT: user_agent.decode("utf8")}
             )
             return expected
 
@@ -868,7 +926,7 @@
         def update_expected_user_agent(expected):
             expected[3]["attributes"].update(
                 {
-                    SpanAttributes.HTTP_USER_AGENT: user_agent.decode("utf8"),
+                    HTTP_USER_AGENT: user_agent.decode("utf8"),
                     USER_AGENT_ORIGINAL: user_agent.decode("utf8"),
                 }
             )
@@ -906,7 +964,7 @@
                 ):
                     expected[3]["attributes"].update(
                         {
-                            SpanAttributes.HTTP_USER_AGENT: ua.decode("utf8"),
+                            HTTP_USER_AGENT: ua.decode("utf8"),
                             USER_AGENT_SYNTHETIC_TYPE: "bot",
                         }
                     )
@@ -921,9 +979,6 @@
                     outputs, modifiers=[update_expected_synthetic_bot]
                 )
 
-                # Clear spans after each test case to prevent accumulation
-                self.memory_exporter.clear()
-
     async def test_user_agent_synthetic_test_detection(self):
         """Test that test user agents are detected as synthetic with type 
'test'"""
         test_cases = [
@@ -943,7 +998,7 @@
                 ):
                     expected[3]["attributes"].update(
                         {
-                            SpanAttributes.HTTP_USER_AGENT: ua.decode("utf8"),
+                            HTTP_USER_AGENT: ua.decode("utf8"),
                             USER_AGENT_SYNTHETIC_TYPE: "test",
                         }
                     )
@@ -958,9 +1013,6 @@
                     outputs, modifiers=[update_expected_synthetic_test]
                 )
 
-                # Clear spans after each test case to prevent accumulation
-                self.memory_exporter.clear()
-
     async def test_user_agent_non_synthetic(self):
         """Test that normal user agents are not marked as synthetic"""
         test_cases = [
@@ -982,7 +1034,7 @@
                     # Should only have the user agent, not synthetic type
                     expected[3]["attributes"].update(
                         {
-                            SpanAttributes.HTTP_USER_AGENT: ua.decode("utf8"),
+                            HTTP_USER_AGENT: ua.decode("utf8"),
                         }
                     )
                     return expected
@@ -996,9 +1048,6 @@
                     outputs, modifiers=[update_expected_non_synthetic]
                 )
 
-                # Clear spans after each test case to prevent accumulation
-                self.memory_exporter.clear()
-
     async def test_user_agent_synthetic_new_semconv(self):
         """Test synthetic user agent detection with new semantic conventions"""
         user_agent = b"Mozilla/5.0 (compatible; Googlebot/2.1)"
@@ -1036,7 +1085,7 @@
         await self.send_default_request()
         response_start, response_body, *_ = await self.get_all_output()
 
-        span = self.memory_exporter.get_finished_spans()[-1]
+        span = self.get_finished_spans()[-1]
         self.assertEqual(trace_api.SpanKind.SERVER, span.kind)
 
         self.assertEqual(response_body["body"], b"*")
@@ -1076,7 +1125,7 @@
         await self.send_input({"type": "websocket.receive", "text": "ping"})
         await self.send_input({"type": "websocket.disconnect"})
         await self.get_all_output()
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         self.assertEqual(len(span_list), 6)
         expected = [
             {
@@ -1094,7 +1143,7 @@
                 "kind": trace_api.SpanKind.INTERNAL,
                 "attributes": {
                     "asgi.event.type": "websocket.receive",
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
             {
@@ -1102,7 +1151,7 @@
                 "kind": trace_api.SpanKind.INTERNAL,
                 "attributes": {
                     "asgi.event.type": "websocket.send",
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
             {
@@ -1114,16 +1163,16 @@
                 "name": "GET /",
                 "kind": trace_api.SpanKind.SERVER,
                 "attributes": {
-                    SpanAttributes.HTTP_SCHEME: self.scope["scheme"],
-                    SpanAttributes.NET_HOST_PORT: self.scope["server"][1],
-                    SpanAttributes.HTTP_HOST: self.scope["server"][0],
-                    SpanAttributes.HTTP_FLAVOR: self.scope["http_version"],
-                    SpanAttributes.HTTP_TARGET: self.scope["path"],
-                    SpanAttributes.HTTP_URL: 
f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}",
-                    SpanAttributes.NET_PEER_IP: self.scope["client"][0],
-                    SpanAttributes.NET_PEER_PORT: self.scope["client"][1],
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
-                    SpanAttributes.HTTP_METHOD: self.scope["method"],
+                    HTTP_SCHEME: self.scope["scheme"],
+                    NET_HOST_PORT: self.scope["server"][1],
+                    HTTP_HOST: self.scope["server"][0],
+                    HTTP_FLAVOR: self.scope["http_version"],
+                    HTTP_TARGET: self.scope["path"],
+                    HTTP_URL: 
f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}",
+                    NET_PEER_IP: self.scope["client"][0],
+                    NET_PEER_PORT: self.scope["client"][1],
+                    HTTP_STATUS_CODE: 200,
+                    HTTP_METHOD: self.scope["method"],
                 },
             },
         ]
@@ -1150,7 +1199,7 @@
         await self.send_input({"type": "websocket.receive", "text": "ping"})
         await self.send_input({"type": "websocket.disconnect"})
         await self.get_all_output()
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         self.assertEqual(len(span_list), 6)
         expected = [
             {
@@ -1223,7 +1272,7 @@
         await self.send_input({"type": "websocket.receive", "text": "ping"})
         await self.send_input({"type": "websocket.disconnect"})
         await self.get_all_output()
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         self.assertEqual(len(span_list), 6)
         expected = [
             {
@@ -1242,7 +1291,7 @@
                 "attributes": {
                     "asgi.event.type": "websocket.receive",
                     HTTP_RESPONSE_STATUS_CODE: 200,
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
             {
@@ -1251,7 +1300,7 @@
                 "attributes": {
                     "asgi.event.type": "websocket.send",
                     HTTP_RESPONSE_STATUS_CODE: 200,
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_STATUS_CODE: 200,
                 },
             },
             {
@@ -1263,16 +1312,16 @@
                 "name": "GET /",
                 "kind": trace_api.SpanKind.SERVER,
                 "attributes": {
-                    SpanAttributes.HTTP_SCHEME: self.scope["scheme"],
-                    SpanAttributes.NET_HOST_PORT: self.scope["server"][1],
-                    SpanAttributes.HTTP_HOST: self.scope["server"][0],
-                    SpanAttributes.HTTP_FLAVOR: self.scope["http_version"],
-                    SpanAttributes.HTTP_TARGET: self.scope["path"],
-                    SpanAttributes.HTTP_URL: 
f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}",
-                    SpanAttributes.NET_PEER_IP: self.scope["client"][0],
-                    SpanAttributes.NET_PEER_PORT: self.scope["client"][1],
-                    SpanAttributes.HTTP_STATUS_CODE: 200,
-                    SpanAttributes.HTTP_METHOD: self.scope["method"],
+                    HTTP_SCHEME: self.scope["scheme"],
+                    NET_HOST_PORT: self.scope["server"][1],
+                    HTTP_HOST: self.scope["server"][0],
+                    HTTP_FLAVOR: self.scope["http_version"],
+                    HTTP_TARGET: self.scope["path"],
+                    HTTP_URL: 
f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}",
+                    NET_PEER_IP: self.scope["client"][0],
+                    NET_PEER_PORT: self.scope["client"][1],
+                    HTTP_STATUS_CODE: 200,
+                    HTTP_METHOD: self.scope["method"],
                     URL_SCHEME: self.scope["scheme"],
                     SERVER_ADDRESS: self.scope["server"][0],
                     SERVER_PORT: self.scope["server"][1],
@@ -1313,7 +1362,7 @@
         await self.send_input({"type": "websocket.disconnect"})
         _, socket_send, *_ = await self.get_all_output()
 
-        span = self.memory_exporter.get_finished_spans()[-1]
+        span = self.get_finished_spans()[-1]
         self.assertEqual(trace_api.SpanKind.SERVER, span.kind)
 
         trace_id = format_trace_id(span.get_span_context().trace_id)
@@ -1335,7 +1384,7 @@
         app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
         self.seed_app(app)
         await self.send_default_request()
-        span_list = self.memory_exporter.get_finished_spans()
+        span_list = self.get_finished_spans()
         self.assertEqual(len(span_list), 0)
 
     async def test_hooks(self):
@@ -1416,32 +1465,22 @@
         self.seed_app(app)
         await self.send_default_request()
         await self.get_all_output()
-        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) != 0)
-        for resource_metric in metrics_list.resource_metrics:
-            self.assertTrue(len(resource_metric.scope_metrics) != 0)
-            for scope_metric in resource_metric.scope_metrics:
-                self.assertTrue(len(scope_metric.metrics) != 0)
-                self.assertEqual(
-                    scope_metric.scope.name,
-                    "opentelemetry.instrumentation.asgi",
-                )
-                for metric in scope_metric.metrics:
-                    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]
-                            )
+        metrics = self.get_sorted_metrics(SCOPE)
+        self.assertTrue(len(metrics) != 0)
+        for metric in metrics:
+            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)
 
     async def test_asgi_metrics_new_semconv(self):
@@ -1456,37 +1495,27 @@
         self.seed_app(app)
         await self.send_default_request()
         await self.get_all_output()
-        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) != 0)
-        for resource_metric in metrics_list.resource_metrics:
-            self.assertTrue(len(resource_metric.scope_metrics) != 0)
-            for scope_metric in resource_metric.scope_metrics:
-                self.assertTrue(len(scope_metric.metrics) != 0)
-                self.assertEqual(
-                    scope_metric.scope.name,
-                    "opentelemetry.instrumentation.asgi",
-                )
-                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)
-                            if metric.name == "http.server.request.duration":
-                                self.assertEqual(
-                                    point.explicit_bounds,
-                                    HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
-                                )
-                            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]
-                            )
+        metrics = self.get_sorted_metrics(SCOPE)
+        self.assertTrue(len(metrics) != 0)
+        for metric in 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)
+                    if metric.name == "http.server.request.duration":
+                        self.assertEqual(
+                            point.explicit_bounds,
+                            HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
+                        )
+                    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)
 
     async def test_asgi_metrics_both_semconv(self):
@@ -1501,39 +1530,63 @@
         self.seed_app(app)
         await self.send_default_request()
         await self.get_all_output()
-        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) != 0)
-        for resource_metric in metrics_list.resource_metrics:
-            self.assertTrue(len(resource_metric.scope_metrics) != 0)
-            for scope_metric in resource_metric.scope_metrics:
-                self.assertTrue(len(scope_metric.metrics) != 0)
-                self.assertEqual(
-                    scope_metric.scope.name,
-                    "opentelemetry.instrumentation.asgi",
-                )
-                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:
-                        if isinstance(point, HistogramDataPoint):
-                            self.assertEqual(point.count, 3)
-                            if metric.name == "http.server.request.duration":
-                                self.assertEqual(
-                                    point.explicit_bounds,
-                                    HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
-                                )
-                            histogram_data_point_seen = True
-                        if isinstance(point, NumberDataPoint):
-                            number_data_point_seen = True
-                        for attr in point.attributes:
-                            self.assertIn(
-                                attr, _recommended_attrs_both[metric.name]
-                            )
+        metrics = self.get_sorted_metrics(SCOPE)
+        self.assertTrue(len(metrics) != 0)
+        for metric in 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:
+                if isinstance(point, HistogramDataPoint):
+                    self.assertEqual(point.count, 3)
+                    if metric.name == "http.server.request.duration":
+                        self.assertEqual(
+                            point.explicit_bounds,
+                            HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
+                        )
+                    histogram_data_point_seen = True
+                if isinstance(point, NumberDataPoint):
+                    number_data_point_seen = True
+                for attr in point.attributes:
+                    self.assertIn(attr, _recommended_attrs_both[metric.name])
         self.assertTrue(number_data_point_seen and histogram_data_point_seen)
 
+    async def test_asgi_metrics_exemplars_expected_old_semconv(self):
+        """Failing test placeholder asserting exemplars should be present for 
duration histogram (old semconv)."""
+        app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
+        for _ in range(5):
+            self.seed_app(app)
+            await self.send_default_request()
+            await self.get_all_output()
+        self._assert_exemplars_present(
+            {"http.server.duration"}, context="old semconv"
+        )
+
+    async def test_asgi_metrics_exemplars_expected_new_semconv(self):
+        """Failing test placeholder asserting exemplars should be present for 
request duration histogram (new semconv)."""
+        app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
+        for _ in range(5):
+            self.seed_app(app)
+            await self.send_default_request()
+            await self.get_all_output()
+        self._assert_exemplars_present(
+            {"http.server.request.duration"}, context="new semconv"
+        )
+
+    async def test_asgi_metrics_exemplars_expected_both_semconv(self):
+        """Failing test placeholder asserting exemplars should be present for 
both duration histograms when both semconv modes enabled."""
+        app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
+        for _ in range(5):
+            self.seed_app(app)
+            await self.send_default_request()
+            await self.get_all_output()
+        self._assert_exemplars_present(
+            {"http.server.duration", "http.server.request.duration"},
+            context="both semconv",
+        )
+
     async def test_basic_metric_success(self):
         app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
         self.seed_app(app)
@@ -1555,32 +1608,28 @@
             "http.scheme": "http",
             "http.flavor": "1.0",
         }
-        metrics_list = self.memory_metrics_reader.get_metrics_data()
+        metrics = self.get_sorted_metrics(SCOPE)
         # pylint: disable=too-many-nested-blocks
-        for resource_metric in metrics_list.resource_metrics:
-            for scope_metrics in resource_metric.scope_metrics:
-                for metric in scope_metrics.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.duration":
-                                self.assertAlmostEqual(
-                                    duration, point.sum, delta=5
-                                )
-                            elif metric.name == "http.server.response.size":
-                                self.assertEqual(1024, point.sum)
-                            elif metric.name == "http.server.request.size":
-                                self.assertEqual(128, point.sum)
-                        elif isinstance(point, NumberDataPoint):
-                            self.assertDictEqual(
-                                expected_requests_count_attributes,
-                                dict(point.attributes),
-                            )
-                            self.assertEqual(point.value, 0)
+        for metric in 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.duration":
+                        self.assertAlmostEqual(duration, point.sum, delta=30)
+                    elif metric.name == "http.server.response.size":
+                        self.assertEqual(1024, point.sum)
+                    elif metric.name == "http.server.request.size":
+                        self.assertEqual(128, point.sum)
+                elif isinstance(point, NumberDataPoint):
+                    self.assertDictEqual(
+                        expected_requests_count_attributes,
+                        dict(point.attributes),
+                    )
+                    self.assertEqual(point.value, 0)
 
     async def test_basic_metric_success_nonrecording_span(self):
         mock_tracer = mock.Mock()
@@ -1613,34 +1662,30 @@
                 "http.scheme": "http",
                 "http.flavor": "1.0",
             }
-            metrics_list = self.memory_metrics_reader.get_metrics_data()
+            metrics = self.get_sorted_metrics(SCOPE)
             # pylint: disable=too-many-nested-blocks
-            for resource_metric in metrics_list.resource_metrics:
-                for scope_metrics in resource_metric.scope_metrics:
-                    for metric in scope_metrics.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.duration":
-                                    self.assertAlmostEqual(
-                                        duration, point.sum, delta=15
-                                    )
-                                elif (
-                                    metric.name == "http.server.response.size"
-                                ):
-                                    self.assertEqual(1024, point.sum)
-                                elif metric.name == "http.server.request.size":
-                                    self.assertEqual(128, point.sum)
-                            elif isinstance(point, NumberDataPoint):
-                                self.assertDictEqual(
-                                    expected_requests_count_attributes,
-                                    dict(point.attributes),
-                                )
-                                self.assertEqual(point.value, 0)
+            for metric in 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.duration":
+                            self.assertAlmostEqual(
+                                duration, point.sum, delta=15
+                            )
+                        elif metric.name == "http.server.response.size":
+                            self.assertEqual(1024, point.sum)
+                        elif metric.name == "http.server.request.size":
+                            self.assertEqual(128, point.sum)
+                    elif isinstance(point, NumberDataPoint):
+                        self.assertDictEqual(
+                            expected_requests_count_attributes,
+                            dict(point.attributes),
+                        )
+                        self.assertEqual(point.value, 0)
 
     async def test_basic_metric_success_new_semconv(self):
         app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
@@ -1659,36 +1704,28 @@
             "http.request.method": "GET",
             "url.scheme": "http",
         }
-        metrics_list = self.memory_metrics_reader.get_metrics_data()
+        metrics = self.get_sorted_metrics(SCOPE)
         # pylint: disable=too-many-nested-blocks
-        for resource_metric in metrics_list.resource_metrics:
-            for scope_metrics in resource_metric.scope_metrics:
-                for metric in scope_metrics.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=2
-                                )
-                            elif (
-                                metric.name == "http.server.response.body.size"
-                            ):
-                                self.assertEqual(1024, point.sum)
-                            elif (
-                                metric.name == "http.server.request.body.size"
-                            ):
-                                self.assertEqual(128, point.sum)
-                        elif isinstance(point, NumberDataPoint):
-                            self.assertDictEqual(
-                                expected_requests_count_attributes,
-                                dict(point.attributes),
-                            )
-                            self.assertEqual(point.value, 0)
+        for metric in 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=2)
+                    elif metric.name == "http.server.response.body.size":
+                        self.assertEqual(1024, point.sum)
+                    elif metric.name == "http.server.request.body.size":
+                        self.assertEqual(128, point.sum)
+                elif isinstance(point, NumberDataPoint):
+                    self.assertDictEqual(
+                        expected_requests_count_attributes,
+                        dict(point.attributes),
+                    )
+                    self.assertEqual(point.value, 0)
 
     async def test_basic_metric_success_both_semconv(self):
         app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
@@ -1720,64 +1757,54 @@
             "network.protocol.version": "1.0",
             "http.response.status_code": 200,
         }
-        metrics_list = self.memory_metrics_reader.get_metrics_data()
+        metrics = self.get_sorted_metrics(SCOPE)
         # pylint: disable=too-many-nested-blocks
-        for resource_metric in metrics_list.resource_metrics:
-            for scope_metrics in resource_metric.scope_metrics:
-                for metric in scope_metrics.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=2
-                                )
-                                self.assertDictEqual(
-                                    expected_duration_attributes_new,
-                                    dict(point.attributes),
-                                )
-                            elif (
-                                metric.name == "http.server.response.body.size"
-                            ):
-                                self.assertEqual(1024, point.sum)
-                                self.assertDictEqual(
-                                    expected_duration_attributes_new,
-                                    dict(point.attributes),
-                                )
-                            elif (
-                                metric.name == "http.server.request.body.size"
-                            ):
-                                self.assertEqual(128, point.sum)
-                                self.assertDictEqual(
-                                    expected_duration_attributes_new,
-                                    dict(point.attributes),
-                                )
-                            elif metric.name == "http.server.duration":
-                                self.assertAlmostEqual(
-                                    duration, point.sum, delta=5
-                                )
-                                self.assertDictEqual(
-                                    expected_duration_attributes_old,
-                                    dict(point.attributes),
-                                )
-                            elif metric.name == "http.server.response.size":
-                                self.assertEqual(1024, point.sum)
-                                self.assertDictEqual(
-                                    expected_duration_attributes_old,
-                                    dict(point.attributes),
-                                )
-                            elif metric.name == "http.server.request.size":
-                                self.assertEqual(128, point.sum)
-                                self.assertDictEqual(
-                                    expected_duration_attributes_old,
-                                    dict(point.attributes),
-                                )
-                        elif isinstance(point, NumberDataPoint):
-                            self.assertDictEqual(
-                                expected_requests_count_attributes,
-                                dict(point.attributes),
-                            )
-                            self.assertEqual(point.value, 0)
+        for metric in 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=2)
+                        self.assertDictEqual(
+                            expected_duration_attributes_new,
+                            dict(point.attributes),
+                        )
+                    elif metric.name == "http.server.response.body.size":
+                        self.assertEqual(1024, point.sum)
+                        self.assertDictEqual(
+                            expected_duration_attributes_new,
+                            dict(point.attributes),
+                        )
+                    elif metric.name == "http.server.request.body.size":
+                        self.assertEqual(128, point.sum)
+                        self.assertDictEqual(
+                            expected_duration_attributes_new,
+                            dict(point.attributes),
+                        )
+                    elif metric.name == "http.server.duration":
+                        self.assertAlmostEqual(duration, point.sum, delta=30)
+                        self.assertDictEqual(
+                            expected_duration_attributes_old,
+                            dict(point.attributes),
+                        )
+                    elif metric.name == "http.server.response.size":
+                        self.assertEqual(1024, point.sum)
+                        self.assertDictEqual(
+                            expected_duration_attributes_old,
+                            dict(point.attributes),
+                        )
+                    elif metric.name == "http.server.request.size":
+                        self.assertEqual(128, point.sum)
+                        self.assertDictEqual(
+                            expected_duration_attributes_old,
+                            dict(point.attributes),
+                        )
+                elif isinstance(point, NumberDataPoint):
+                    self.assertDictEqual(
+                        expected_requests_count_attributes,
+                        dict(point.attributes),
+                    )
+                    self.assertEqual(point.value, 0)
 
     async def test_metric_target_attribute(self):
         expected_target = "/api/user/{id}"
@@ -1797,20 +1824,18 @@
         self.seed_app(app)
         await self.send_default_request()
         await self.get_all_output()
-        metrics_list = self.memory_metrics_reader.get_metrics_data()
+        metrics = self.get_sorted_metrics(SCOPE)
         assertions = 0
-        for resource_metric in metrics_list.resource_metrics:
-            for scope_metrics in resource_metric.scope_metrics:
-                for metric in scope_metrics.metrics:
-                    if metric.name == "http.server.active_requests":
-                        continue
-                    for point in metric.data.data_points:
-                        if isinstance(point, HistogramDataPoint):
-                            self.assertEqual(
-                                point.attributes["http.target"],
-                                expected_target,
-                            )
-                            assertions += 1
+        for metric in metrics:
+            if metric.name == "http.server.active_requests":
+                continue
+            for point in metric.data.data_points:
+                if isinstance(point, HistogramDataPoint):
+                    self.assertEqual(
+                        point.attributes["http.target"],
+                        expected_target,
+                    )
+                    assertions += 1
         self.assertEqual(assertions, 3)
 
     async def test_no_metric_for_websockets(self):
@@ -1830,7 +1855,7 @@
         await self.send_input({"type": "websocket.receive", "text": "ping"})
         await self.send_input({"type": "websocket.disconnect"})
         await self.get_all_output()
-        self.assertIsNone(self.memory_metrics_reader.get_metrics_data())
+        self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0)
 
     async def test_excluded_urls(self):
         self.scope["path"] = "/test_excluded_urls"
@@ -1840,7 +1865,7 @@
         self.seed_app(app)
         await self.send_default_request()
         await self.get_all_output()
-        spans = self.memory_exporter.get_finished_spans()
+        spans = self.get_finished_spans()
         self.assertEqual(len(spans), 0)
 
     async def test_no_excluded_urls(self):
@@ -1852,7 +1877,7 @@
         self.scope["path"] = "/test_no_excluded_urls"
         await self.send_default_request()
         await self.get_all_output()
-        spans = self.memory_exporter.get_finished_spans()
+        spans = self.get_finished_spans()
         self.assertGreater(len(spans), 0)
 
 
@@ -1873,16 +1898,16 @@
         self.assertDictEqual(
             attrs,
             {
-                SpanAttributes.HTTP_METHOD: "GET",
-                SpanAttributes.HTTP_HOST: "127.0.0.1",
-                SpanAttributes.HTTP_TARGET: "/",
-                SpanAttributes.HTTP_URL: "http://test/?foo=bar";,
-                SpanAttributes.NET_HOST_PORT: 80,
-                SpanAttributes.HTTP_SCHEME: "http",
-                SpanAttributes.HTTP_SERVER_NAME: "test",
-                SpanAttributes.HTTP_FLAVOR: "1.0",
-                SpanAttributes.NET_PEER_IP: "127.0.0.1",
-                SpanAttributes.NET_PEER_PORT: 32767,
+                HTTP_METHOD: "GET",
+                HTTP_HOST: "127.0.0.1",
+                HTTP_TARGET: "/",
+                HTTP_URL: "http://test/?foo=bar";,
+                NET_HOST_PORT: 80,
+                HTTP_SCHEME: "http",
+                HTTP_SERVER_NAME: "test",
+                HTTP_FLAVOR: "1.0",
+                NET_PEER_IP: "127.0.0.1",
+                NET_PEER_PORT: 32767,
             },
         )
 
@@ -1926,16 +1951,16 @@
         self.assertDictEqual(
             attrs,
             {
-                SpanAttributes.HTTP_METHOD: "GET",
-                SpanAttributes.HTTP_HOST: "127.0.0.1",
-                SpanAttributes.HTTP_TARGET: "/",
-                SpanAttributes.HTTP_URL: "http://test/?foo=bar";,
-                SpanAttributes.NET_HOST_PORT: 80,
-                SpanAttributes.HTTP_SCHEME: "http",
-                SpanAttributes.HTTP_SERVER_NAME: "test",
-                SpanAttributes.HTTP_FLAVOR: "1.0",
-                SpanAttributes.NET_PEER_IP: "127.0.0.1",
-                SpanAttributes.NET_PEER_PORT: 32767,
+                HTTP_METHOD: "GET",
+                HTTP_HOST: "127.0.0.1",
+                HTTP_TARGET: "/",
+                HTTP_URL: "http://test/?foo=bar";,
+                NET_HOST_PORT: 80,
+                HTTP_SCHEME: "http",
+                HTTP_SERVER_NAME: "test",
+                HTTP_FLAVOR: "1.0",
+                NET_PEER_IP: "127.0.0.1",
+                NET_PEER_PORT: 32767,
                 HTTP_REQUEST_METHOD: "GET",
                 URL_PATH: "/",
                 URL_QUERY: "foo=bar",
@@ -1951,9 +1976,7 @@
     def test_query_string(self):
         self.scope["query_string"] = b"foo=bar"
         attrs = otel_asgi.collect_request_attributes(self.scope)
-        self.assertEqual(
-            attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar";
-        )
+        self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar";)
 
     def test_query_string_new_semconv(self):
         self.scope["query_string"] = b"foo=bar"
@@ -1972,9 +1995,7 @@
             self.scope,
             _StabilityMode.HTTP_DUP,
         )
-        self.assertEqual(
-            attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar";
-        )
+        self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar";)
         self.assertEqual(attrs[URL_SCHEME], "http")
         self.assertEqual(attrs[CLIENT_ADDRESS], "127.0.0.1")
         self.assertEqual(attrs[URL_PATH], "/")
@@ -1983,20 +2004,16 @@
     def test_query_string_percent_bytes(self):
         self.scope["query_string"] = b"foo%3Dbar"
         attrs = otel_asgi.collect_request_attributes(self.scope)
-        self.assertEqual(
-            attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar";
-        )
+        self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar";)
 
     def test_query_string_percent_str(self):
         self.scope["query_string"] = "foo%3Dbar"
         attrs = otel_asgi.collect_request_attributes(self.scope)
-        self.assertEqual(
-            attrs[SpanAttributes.HTTP_URL], "http://127.0.0.1/?foo=bar";
-        )
+        self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar";)
 
     def test_response_attributes(self):
         otel_asgi.set_status_code(self.span, 404)
-        expected = (mock.call(SpanAttributes.HTTP_STATUS_CODE, 404),)
+        expected = (mock.call(HTTP_STATUS_CODE, 404),)
         self.assertEqual(self.span.set_attribute.call_count, 1)
         self.assertEqual(self.span.set_attribute.call_count, 1)
         self.span.set_attribute.assert_has_calls(expected, any_order=True)
@@ -2020,7 +2037,7 @@
             None,
             _StabilityMode.HTTP_DUP,
         )
-        expected = (mock.call(SpanAttributes.HTTP_STATUS_CODE, 404),)
+        expected = (mock.call(HTTP_STATUS_CODE, 404),)
         expected2 = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),)
         self.assertEqual(self.span.set_attribute.call_count, 2)
         self.assertEqual(self.span.set_attribute.call_count, 2)
@@ -2037,7 +2054,7 @@
         self.scope["query_string"] = b"X-Goog-Signature=1234567890"
         attrs = otel_asgi.collect_request_attributes(self.scope)
         self.assertEqual(
-            attrs[SpanAttributes.HTTP_URL],
+            attrs[HTTP_URL],
             
"http://REDACTED:REDACTED@mock/status/200?X-Goog-Signature=REDACTED";,
         )
 

Reply via email to