From f4762063f1d6e8f9e7e6b33da64b1983e398e453 Mon Sep 17 00:00:00 2001 From: rite7sh Date: Wed, 17 Dec 2025 16:44:37 +0530 Subject: [PATCH 1/6] refactor(asgi): replace HTTP_SERVER_NAME SpanAttribute with semconv attribute Refs #3475 --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 60c43f6db0..edb2300f01 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -219,6 +219,7 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): from asgiref.compatibility import guarantee_single_callable + from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, @@ -288,7 +289,9 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): redact_url, sanitize_method, ) - +from opentelemetry.semconv._incubating.attributes.http_attributes import ( + HTTP_SERVER_NAME, +) class ASGIGetter(Getter[dict]): def get( @@ -397,7 +400,7 @@ def collect_request_attributes( 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( + result[HTTP_SERVER_NAME] = ",".join( http_host_value_list ) http_user_agent = asgi_getter.get(scope, "user-agent") From 9789577bfee08b258f564fbf44482aefff62e038 Mon Sep 17 00:00:00 2001 From: Ritesh Traipathi Date: Fri, 19 Dec 2025 15:40:21 +0530 Subject: [PATCH 2/6] Update instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py Co-authored-by: Riccardo Magliocchetti --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index edb2300f01..8e1c1df799 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -219,7 +219,6 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): from asgiref.compatibility import guarantee_single_callable - from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, From a6bd905a54e40a9c1df20d6232b076a784313340 Mon Sep 17 00:00:00 2001 From: Ritesh Traipathi Date: Fri, 19 Dec 2025 15:46:51 +0530 Subject: [PATCH 3/6] Update instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py Co-authored-by: Riccardo Magliocchetti --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 8e1c1df799..541c28d1ab 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -399,7 +399,7 @@ def collect_request_attributes( http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: if _report_old(sem_conv_opt_in_mode): - result[HTTP_SERVER_NAME] = ",".join( + result[HTTP_SERVER_NAME] = ",".join( http_host_value_list ) http_user_agent = asgi_getter.get(scope, "user-agent") From 8f45599d499a9a843647af7b3aa32b3f09d6cf60 Mon Sep 17 00:00:00 2001 From: rite7sh Date: Tue, 23 Dec 2025 01:32:28 +0530 Subject: [PATCH 4/6] tests(asgi): remove duplicate expected attribute keys --- .../instrumentation/asgi/__init__.py | 11 ++- .../tests/test_asgi_middleware.py | 72 ++++++++----------- 2 files changed, 35 insertions(+), 48 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 541c28d1ab..20b0ac31e5 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -258,6 +258,9 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): 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, +) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) @@ -288,9 +291,7 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): redact_url, sanitize_method, ) -from opentelemetry.semconv._incubating.attributes.http_attributes import ( - HTTP_SERVER_NAME, -) + class ASGIGetter(Getter[dict]): def get( @@ -399,9 +400,7 @@ def collect_request_attributes( http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: if _report_old(sem_conv_opt_in_mode): - result[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_raw = http_user_agent[0] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index fdf328498b..ca65e70472 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -42,6 +42,9 @@ HistogramDataPoint, NumberDataPoint, ) +from opentelemetry.semconv._incubating.attributes.http_attributes import ( + HTTP_SERVER_NAME, +) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) @@ -61,6 +64,7 @@ SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME, @@ -368,7 +372,7 @@ def validate_outputs( "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { - SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, }, @@ -381,16 +385,16 @@ def validate_outputs( "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { - SpanAttributes.HTTP_METHOD: "GET", + HTTP_REQUEST_METHOD: "GET", SpanAttributes.HTTP_SCHEME: "http", - SpanAttributes.NET_HOST_PORT: 80, - SpanAttributes.HTTP_HOST: "127.0.0.1", + SERVER_PORT: 80, + SERVER_ADDRESS: "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, + URL_FULL: "http://127.0.0.1/", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, + HTTP_RESPONSE_STATUS_CODE: 200, }, }, ] @@ -439,7 +443,6 @@ def validate_outputs( "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { - SpanAttributes.HTTP_STATUS_CODE: 200, HTTP_RESPONSE_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, @@ -462,16 +465,10 @@ def validate_outputs( 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, + URL_FULL: "http://127.0.0.1/", }, }, ] @@ -713,7 +710,7 @@ def update_expected_server(expected): expected[3]["attributes"].update( { SpanAttributes.HTTP_HOST: "0.0.0.0", - SpanAttributes.NET_HOST_PORT: 80, + SERVER_PORT: 80, SpanAttributes.HTTP_URL: "http://0.0.0.0/", } ) @@ -757,10 +754,9 @@ def update_expected_server(expected): expected[3]["attributes"].update( { SpanAttributes.HTTP_HOST: "0.0.0.0", - SpanAttributes.NET_HOST_PORT: 80, + SERVER_PORT: 80, SpanAttributes.HTTP_URL: "http://0.0.0.0/", SERVER_ADDRESS: "0.0.0.0", - SERVER_PORT: 80, } ) return expected @@ -784,7 +780,7 @@ async def test_host_header(self): def update_expected_server(expected): expected[3]["attributes"].update( { - SpanAttributes.HTTP_SERVER_NAME: hostname.decode("utf8"), + HTTP_SERVER_NAME: hostname.decode("utf8"), SpanAttributes.HTTP_URL: f"http://{hostname.decode('utf8')}/", } ) @@ -1094,7 +1090,7 @@ async def test_websocket(self): "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", - SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, }, }, { @@ -1102,7 +1098,7 @@ async def test_websocket(self): "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", - SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, }, }, { @@ -1122,7 +1118,7 @@ async def test_websocket(self): 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, + HTTP_RESPONSE_STATUS_CODE: 200, SpanAttributes.HTTP_METHOD: self.scope["method"], }, }, @@ -1242,7 +1238,6 @@ async def test_websocket_both_semconv(self): "attributes": { "asgi.event.type": "websocket.receive", HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1251,7 +1246,6 @@ async def test_websocket_both_semconv(self): "attributes": { "asgi.event.type": "websocket.send", HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1271,7 +1265,7 @@ async def test_websocket_both_semconv(self): 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, + HTTP_RESPONSE_STATUS_CODE: 200, SpanAttributes.HTTP_METHOD: self.scope["method"], URL_SCHEME: self.scope["scheme"], SERVER_ADDRESS: self.scope["server"][0], @@ -1280,7 +1274,6 @@ async def test_websocket_both_semconv(self): URL_PATH: self.scope["path"], CLIENT_ADDRESS: self.scope["client"][0], CLIENT_PORT: self.scope["client"][1], - HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: self.scope["method"], }, }, @@ -1873,16 +1866,16 @@ def test_request_attributes(self): self.assertDictEqual( attrs, { - SpanAttributes.HTTP_METHOD: "GET", - SpanAttributes.HTTP_HOST: "127.0.0.1", + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_URL: "http://test/?foo=bar", - SpanAttributes.NET_HOST_PORT: 80, + SERVER_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, + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, }, ) @@ -1926,25 +1919,20 @@ def test_request_attributes_both_semconv(self): self.assertDictEqual( attrs, { - SpanAttributes.HTTP_METHOD: "GET", - SpanAttributes.HTTP_HOST: "127.0.0.1", + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_URL: "http://test/?foo=bar", - SpanAttributes.NET_HOST_PORT: 80, + SERVER_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_REQUEST_METHOD: "GET", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, URL_PATH: "/", URL_QUERY: "foo=bar", - SERVER_ADDRESS: "127.0.0.1", - SERVER_PORT: 80, URL_SCHEME: "http", NETWORK_PROTOCOL_VERSION: "1.0", - CLIENT_ADDRESS: "127.0.0.1", - CLIENT_PORT: 32767, }, ) From 9a2c2d86bd466bff8bddc2289ab86d383b602c62 Mon Sep 17 00:00:00 2001 From: rite7sh Date: Fri, 26 Dec 2025 17:29:42 +0530 Subject: [PATCH 5/6] refactor(asgi): replace HTTP_SERVER_NAME SpanAttribute with semconv attribute Refs #3475 --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 20b0ac31e5..b485227bad 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -258,9 +258,6 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): 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, -) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) @@ -400,7 +397,11 @@ def collect_request_attributes( http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: if _report_old(sem_conv_opt_in_mode): - result[HTTP_SERVER_NAME] = ",".join(http_host_value_list) + _set_http_host_server( + result, + ",".join(http_host_value_list), + sem_conv_opt_in_mode, + ) http_user_agent = asgi_getter.get(scope, "user-agent") if http_user_agent: user_agent_raw = http_user_agent[0] From 9572b84c776b974beeb505ab6308a882f3ca536c Mon Sep 17 00:00:00 2001 From: rite7sh Date: Sun, 4 Jan 2026 03:04:01 +0530 Subject: [PATCH 6/6] revert(asgi): restore original implementation and tests --- .../instrumentation/asgi/__init__.py | 31 ++-- .../tests/test_asgi_middleware.py | 168 +++++++++++++----- 2 files changed, 148 insertions(+), 51 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index b485227bad..40ac1e2983 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -397,10 +397,8 @@ def collect_request_attributes( http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: if _report_old(sem_conv_opt_in_mode): - _set_http_host_server( - result, - ",".join(http_host_value_list), - sem_conv_opt_in_mode, + result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( + http_host_value_list ) http_user_agent = asgi_getter.get(scope, "user-agent") if http_user_agent: @@ -831,13 +829,18 @@ async def __call__( 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 @@ -845,11 +848,15 @@ async def __call__( 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") @@ -861,11 +868,15 @@ async def __call__( 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 --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index ca65e70472..d94ee9137a 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -42,9 +42,6 @@ HistogramDataPoint, NumberDataPoint, ) -from opentelemetry.semconv._incubating.attributes.http_attributes import ( - HTTP_SERVER_NAME, -) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) @@ -64,7 +61,6 @@ SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( - URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME, @@ -319,6 +315,55 @@ def setUp(self): 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_list = self.memory_metrics_reader.get_metrics_data() + print(metrics_list) + metrics = [] + for resource_metric in ( + getattr(metrics_list, "resource_metrics", []) or [] + ): + for scope_metric in ( + getattr(resource_metric, "scope_metrics", []) or [] + ): + metrics.extend(getattr(scope_metric, "metrics", []) or []) + + 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, @@ -372,7 +417,7 @@ def validate_outputs( "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { - HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, }, @@ -385,16 +430,16 @@ def validate_outputs( "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { - HTTP_REQUEST_METHOD: "GET", + SpanAttributes.HTTP_METHOD: "GET", SpanAttributes.HTTP_SCHEME: "http", - SERVER_PORT: 80, - SERVER_ADDRESS: "127.0.0.1", + SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.HTTP_HOST: "127.0.0.1", SpanAttributes.HTTP_FLAVOR: "1.0", SpanAttributes.HTTP_TARGET: "/", - URL_FULL: "http://127.0.0.1/", - CLIENT_ADDRESS: "127.0.0.1", - CLIENT_PORT: 32767, - HTTP_RESPONSE_STATUS_CODE: 200, + 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, }, }, ] @@ -443,6 +488,7 @@ def validate_outputs( "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { + SpanAttributes.HTTP_STATUS_CODE: 200, HTTP_RESPONSE_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, @@ -465,10 +511,16 @@ def validate_outputs( 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: "/", - URL_FULL: "http://127.0.0.1/", + 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, }, }, ] @@ -710,7 +762,7 @@ def update_expected_server(expected): expected[3]["attributes"].update( { SpanAttributes.HTTP_HOST: "0.0.0.0", - SERVER_PORT: 80, + SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_URL: "http://0.0.0.0/", } ) @@ -754,9 +806,10 @@ def update_expected_server(expected): expected[3]["attributes"].update( { SpanAttributes.HTTP_HOST: "0.0.0.0", - SERVER_PORT: 80, + SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_URL: "http://0.0.0.0/", SERVER_ADDRESS: "0.0.0.0", + SERVER_PORT: 80, } ) return expected @@ -780,7 +833,7 @@ async def test_host_header(self): def update_expected_server(expected): expected[3]["attributes"].update( { - HTTP_SERVER_NAME: hostname.decode("utf8"), + SpanAttributes.HTTP_SERVER_NAME: hostname.decode("utf8"), SpanAttributes.HTTP_URL: f"http://{hostname.decode('utf8')}/", } ) @@ -917,9 +970,6 @@ def update_expected_synthetic_bot( 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 = [ @@ -954,9 +1004,6 @@ def update_expected_synthetic_test( 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 = [ @@ -992,9 +1039,6 @@ def update_expected_non_synthetic( 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)" @@ -1090,7 +1134,7 @@ async def test_websocket(self): "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", - HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1098,7 +1142,7 @@ async def test_websocket(self): "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", - HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1118,7 +1162,7 @@ async def test_websocket(self): 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], - HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, SpanAttributes.HTTP_METHOD: self.scope["method"], }, }, @@ -1238,6 +1282,7 @@ async def test_websocket_both_semconv(self): "attributes": { "asgi.event.type": "websocket.receive", HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1246,6 +1291,7 @@ async def test_websocket_both_semconv(self): "attributes": { "asgi.event.type": "websocket.send", HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { @@ -1265,7 +1311,7 @@ async def test_websocket_both_semconv(self): 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], - HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_STATUS_CODE: 200, SpanAttributes.HTTP_METHOD: self.scope["method"], URL_SCHEME: self.scope["scheme"], SERVER_ADDRESS: self.scope["server"][0], @@ -1274,6 +1320,7 @@ async def test_websocket_both_semconv(self): URL_PATH: self.scope["path"], CLIENT_ADDRESS: self.scope["client"][0], CLIENT_PORT: self.scope["client"][1], + HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: self.scope["method"], }, }, @@ -1527,6 +1574,40 @@ async def test_asgi_metrics_both_semconv(self): ) 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) @@ -1562,7 +1643,7 @@ async def test_basic_metric_success(self): self.assertEqual(point.count, 1) if metric.name == "http.server.duration": self.assertAlmostEqual( - duration, point.sum, delta=5 + duration, point.sum, delta=30 ) elif metric.name == "http.server.response.size": self.assertEqual(1024, point.sum) @@ -1747,7 +1828,7 @@ async def test_basic_metric_success_both_semconv(self): ) elif metric.name == "http.server.duration": self.assertAlmostEqual( - duration, point.sum, delta=5 + duration, point.sum, delta=30 ) self.assertDictEqual( expected_duration_attributes_old, @@ -1866,16 +1947,16 @@ def test_request_attributes(self): self.assertDictEqual( attrs, { - HTTP_REQUEST_METHOD: "GET", - SERVER_ADDRESS: "127.0.0.1", + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "127.0.0.1", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_URL: "http://test/?foo=bar", - SERVER_PORT: 80, + SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.HTTP_SERVER_NAME: "test", SpanAttributes.HTTP_FLAVOR: "1.0", - CLIENT_ADDRESS: "127.0.0.1", - CLIENT_PORT: 32767, + SpanAttributes.NET_PEER_IP: "127.0.0.1", + SpanAttributes.NET_PEER_PORT: 32767, }, ) @@ -1919,20 +2000,25 @@ def test_request_attributes_both_semconv(self): self.assertDictEqual( attrs, { - HTTP_REQUEST_METHOD: "GET", - SERVER_ADDRESS: "127.0.0.1", + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_HOST: "127.0.0.1", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_URL: "http://test/?foo=bar", - SERVER_PORT: 80, + SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.HTTP_SERVER_NAME: "test", SpanAttributes.HTTP_FLAVOR: "1.0", - CLIENT_ADDRESS: "127.0.0.1", - CLIENT_PORT: 32767, + SpanAttributes.NET_PEER_IP: "127.0.0.1", + SpanAttributes.NET_PEER_PORT: 32767, + HTTP_REQUEST_METHOD: "GET", URL_PATH: "/", URL_QUERY: "foo=bar", + SERVER_ADDRESS: "127.0.0.1", + SERVER_PORT: 80, URL_SCHEME: "http", NETWORK_PROTOCOL_VERSION: "1.0", + CLIENT_ADDRESS: "127.0.0.1", + CLIENT_PORT: 32767, }, )