2222
2323from opentelemetry import metrics as metrics_api
2424from opentelemetry import trace as trace_api
25+ from opentelemetry .instrumentation ._semconv import (
26+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
27+ OTEL_SEMCONV_STABILITY_OPT_IN ,
28+ _OpenTelemetrySemanticConventionStability ,
29+ _server_active_requests_count_attrs_new ,
30+ _server_active_requests_count_attrs_old ,
31+ _server_duration_attrs_new ,
32+ _server_duration_attrs_old ,
33+ _StabilityMode ,
34+ )
2535from opentelemetry .instrumentation .aiohttp_server import (
2636 AioHttpServerInstrumentor ,
2737)
2838from opentelemetry .instrumentation .utils import suppress_http_instrumentation
39+ from opentelemetry .sdk .metrics .export import (
40+ HistogramDataPoint ,
41+ )
2942from opentelemetry .semconv ._incubating .attributes .http_attributes import (
43+ HTTP_FLAVOR ,
3044 HTTP_METHOD ,
45+ HTTP_SCHEME ,
3146 HTTP_STATUS_CODE ,
47+ HTTP_TARGET ,
3248 HTTP_URL ,
49+ HTTP_USER_AGENT ,
50+ )
51+ from opentelemetry .semconv ._incubating .attributes .net_attributes import (
52+ NET_HOST_NAME ,
53+ NET_HOST_PORT ,
54+ )
55+ from opentelemetry .semconv .attributes .http_attributes import (
56+ HTTP_REQUEST_METHOD ,
57+ HTTP_RESPONSE_STATUS_CODE ,
58+ )
59+ from opentelemetry .semconv .attributes .network_attributes import (
60+ NETWORK_PROTOCOL_VERSION ,
61+ )
62+ from opentelemetry .semconv .attributes .server_attributes import (
63+ SERVER_ADDRESS ,
64+ SERVER_PORT ,
65+ )
66+ from opentelemetry .semconv .attributes .url_attributes import (
67+ URL_PATH ,
68+ URL_QUERY ,
69+ URL_SCHEME ,
70+ )
71+ from opentelemetry .semconv .attributes .user_agent_attributes import (
72+ USER_AGENT_ORIGINAL ,
3373)
3474from opentelemetry .test .globals_test import (
3575 reset_metrics_globals ,
@@ -164,10 +204,7 @@ async def test_status_code_instrumentation(
164204
165205 assert expected_method .value == span .attributes [HTTP_METHOD ]
166206 assert expected_status_code == span .attributes [HTTP_STATUS_CODE ]
167-
168- assert (
169- f"http://{ server .host } :{ server .port } { url } " == span .attributes [HTTP_URL ]
170- )
207+ assert url == span .attributes [HTTP_TARGET ]
171208
172209
173210@pytest .mark .asyncio
@@ -186,10 +223,16 @@ async def test_suppress_instrumentation(
186223
187224
188225@pytest .mark .asyncio
189- async def test_remove_sensitive_params (tracer , aiohttp_server ):
226+ async def test_remove_sensitive_params (tracer , aiohttp_server , monkeypatch ):
190227 """Test that sensitive information in URLs is properly redacted."""
191228 _ , memory_exporter = tracer
192229
230+ # Use old semconv to test HTTP_URL redaction
231+ monkeypatch .setenv (
232+ OTEL_SEMCONV_STABILITY_OPT_IN , _StabilityMode .DEFAULT .value
233+ )
234+ _OpenTelemetrySemanticConventionStability ._initialized = False
235+
193236 # Set up instrumentation
194237 AioHttpServerInstrumentor ().instrument ()
195238
@@ -228,6 +271,56 @@ async def handler(request):
228271 memory_exporter .clear ()
229272
230273
274+ @pytest .mark .asyncio
275+ async def test_remove_sensitive_params_new (
276+ tracer , aiohttp_server , monkeypatch
277+ ):
278+ """Test URL handling with new semantic conventions (no redaction for URL_PATH/URL_QUERY)."""
279+ _ , memory_exporter = tracer
280+
281+ # Use new semconv
282+ monkeypatch .setenv (
283+ OTEL_SEMCONV_STABILITY_OPT_IN , _StabilityMode .HTTP .value
284+ )
285+ _OpenTelemetrySemanticConventionStability ._initialized = False
286+
287+ # Set up instrumentation
288+ AioHttpServerInstrumentor ().instrument ()
289+
290+ # Create app with test route
291+ app = aiohttp .web .Application ()
292+
293+ async def handler (request ):
294+ return aiohttp .web .Response (text = "hello" )
295+
296+ app .router .add_get ("/status/200" , handler )
297+
298+ # Start the server
299+ server = await aiohttp_server (app )
300+
301+ # Make request with sensitive data in URL
302+ url = f"http://username:password@{ server .host } :{ server .port } /status/200?Signature=secret"
303+ async with aiohttp .ClientSession () as session :
304+ async with session .get (url ) as response :
305+ assert response .status == 200
306+ assert await response .text () == "hello"
307+
308+ # Verify span attributes with new semconv
309+ spans = memory_exporter .get_finished_spans ()
310+ assert len (spans ) == 1
311+
312+ span = spans [0 ]
313+ assert span .attributes [HTTP_REQUEST_METHOD ] == "GET"
314+ assert span .attributes [HTTP_RESPONSE_STATUS_CODE ] == 200
315+ assert span .attributes [URL_PATH ] == "/status/200"
316+ assert span .attributes [URL_QUERY ] == "Signature=REDACTED"
317+ assert HTTP_URL not in span .attributes
318+
319+ # Clean up
320+ AioHttpServerInstrumentor ().uninstrument ()
321+ memory_exporter .clear ()
322+
323+
231324def _get_sorted_metrics (metrics_data ):
232325 resource_metrics = metrics_data .resource_metrics if metrics_data else []
233326
@@ -400,3 +493,230 @@ async def handler(request):
400493 assert "http.response.header.custom_test_header_3" not in span .attributes
401494
402495 AioHttpServerInstrumentor ().uninstrument ()
496+
497+
498+ @pytest .mark .asyncio
499+ async def test_semantic_conventions_metrics_old_default (
500+ tracer , meter , aiohttp_server , monkeypatch
501+ ):
502+ _ , memory_exporter = tracer
503+ _ , metrics_reader = meter
504+ monkeypatch .setenv (
505+ OTEL_SEMCONV_STABILITY_OPT_IN , _StabilityMode .DEFAULT .value
506+ )
507+ _OpenTelemetrySemanticConventionStability ._initialized = False
508+
509+ AioHttpServerInstrumentor ().instrument ()
510+ app = aiohttp .web .Application ()
511+ app .router .add_get ("/test-path" , default_handler )
512+ server = await aiohttp_server (app )
513+ client_session = aiohttp .ClientSession ()
514+ try :
515+ url = f"http://{ server .host } :{ server .port } /test-path?query=test"
516+ async with client_session .get (
517+ url , headers = {"User-Agent" : "test-agent" }
518+ ) as response :
519+ assert response .status == 200
520+ spans = memory_exporter .get_finished_spans ()
521+ assert len (spans ) == 1
522+ span = spans [0 ]
523+
524+ # Old semconv span attributes present
525+ assert span .attributes .get (HTTP_METHOD ) == "GET"
526+ assert span .attributes .get (HTTP_SCHEME ) == "http"
527+ assert span .attributes .get (NET_HOST_NAME ) == server .host
528+ assert span .attributes .get (NET_HOST_PORT ) == server .port
529+ assert span .attributes .get (HTTP_TARGET ) == "/test-path?query=test"
530+ assert span .attributes .get (HTTP_USER_AGENT ) == "test-agent"
531+ assert span .attributes .get (HTTP_FLAVOR ) == "1.1"
532+ assert span .attributes .get (HTTP_STATUS_CODE ) == 200
533+ # New semconv span attributes NOT present
534+ assert HTTP_REQUEST_METHOD not in span .attributes
535+ assert URL_SCHEME not in span .attributes
536+ assert SERVER_ADDRESS not in span .attributes
537+ assert SERVER_PORT not in span .attributes
538+ assert URL_PATH not in span .attributes
539+ assert URL_QUERY not in span .attributes
540+ assert USER_AGENT_ORIGINAL not in span .attributes
541+ assert NETWORK_PROTOCOL_VERSION not in span .attributes
542+ assert HTTP_RESPONSE_STATUS_CODE not in span .attributes
543+
544+ metrics = _get_sorted_metrics (metrics_reader .get_metrics_data ())
545+ expected_metric_names = [
546+ "http.server.active_requests" ,
547+ "http.server.duration" ,
548+ ]
549+ recommended_metrics_attrs = {
550+ "http.server.active_requests" : _server_active_requests_count_attrs_old ,
551+ "http.server.duration" : _server_duration_attrs_old ,
552+ }
553+ for metric in metrics :
554+ assert metric .name in expected_metric_names
555+ if metric .name == "http.server.duration" :
556+ assert metric .unit == "ms"
557+ for point in metric .data .data_points :
558+ for attr in point .attributes :
559+ assert attr in recommended_metrics_attrs [metric .name ]
560+
561+ finally :
562+ await client_session .close ()
563+ AioHttpServerInstrumentor ().uninstrument ()
564+
565+
566+ @pytest .mark .asyncio
567+ async def test_semantic_conventions_metrics_new (
568+ tracer , meter , aiohttp_server , monkeypatch
569+ ):
570+ _ , memory_exporter = tracer
571+ _ , metrics_reader = meter
572+ monkeypatch .setenv (
573+ OTEL_SEMCONV_STABILITY_OPT_IN , _StabilityMode .HTTP .value
574+ )
575+ _OpenTelemetrySemanticConventionStability ._initialized = False
576+
577+ AioHttpServerInstrumentor ().instrument ()
578+ app = aiohttp .web .Application ()
579+ app .router .add_get ("/test-path" , default_handler )
580+ server = await aiohttp_server (app )
581+ client_session = aiohttp .ClientSession ()
582+ try :
583+ url = f"http://{ server .host } :{ server .port } /test-path?query=test"
584+ async with client_session .get (
585+ url , headers = {"User-Agent" : "test-agent" }
586+ ) as response :
587+ assert response .status == 200
588+ spans = memory_exporter .get_finished_spans ()
589+ assert len (spans ) == 1
590+ span = spans [0 ]
591+
592+ # New semconv span attributes present
593+ assert span .attributes .get (HTTP_REQUEST_METHOD ) == "GET"
594+ assert span .attributes .get (URL_SCHEME ) == "http"
595+ assert span .attributes .get (SERVER_ADDRESS ) == server .host
596+ assert span .attributes .get (SERVER_PORT ) == server .port
597+ assert span .attributes .get (URL_PATH ) == "/test-path"
598+ assert span .attributes .get (URL_QUERY ) == "query=test"
599+ assert span .attributes .get (USER_AGENT_ORIGINAL ) == "test-agent"
600+ assert span .attributes .get (NETWORK_PROTOCOL_VERSION ) == "1.1"
601+ assert span .attributes .get (HTTP_RESPONSE_STATUS_CODE ) == 200
602+ # Old semconv span attributes NOT present
603+ assert HTTP_METHOD not in span .attributes
604+ assert HTTP_SCHEME not in span .attributes
605+ assert NET_HOST_NAME not in span .attributes
606+ assert NET_HOST_PORT not in span .attributes
607+ assert HTTP_TARGET not in span .attributes
608+ assert HTTP_USER_AGENT not in span .attributes
609+ assert HTTP_FLAVOR not in span .attributes
610+ assert HTTP_STATUS_CODE not in span .attributes
611+
612+ metrics = _get_sorted_metrics (metrics_reader .get_metrics_data ())
613+ expected_metric_names = [
614+ "http.server.active_requests" ,
615+ "http.server.request.duration" ,
616+ ]
617+ recommended_metrics_attrs = {
618+ "http.server.active_requests" : _server_active_requests_count_attrs_new ,
619+ "http.server.request.duration" : _server_duration_attrs_new ,
620+ }
621+ for metric in metrics :
622+ assert metric .name in expected_metric_names
623+ if metric .name == "http.server.request.duration" :
624+ assert metric .unit == "s"
625+ for point in metric .data .data_points :
626+ if (
627+ isinstance (point , HistogramDataPoint )
628+ and metric .name == "http.server.request.duration"
629+ ):
630+ assert (
631+ point .explicit_bounds
632+ == HTTP_DURATION_HISTOGRAM_BUCKETS_NEW
633+ )
634+ for attr in point .attributes :
635+ assert attr in recommended_metrics_attrs [metric .name ]
636+
637+ finally :
638+ await client_session .close ()
639+ AioHttpServerInstrumentor ().uninstrument ()
640+
641+
642+ @pytest .mark .asyncio
643+ async def test_semantic_conventions_metrics_both (
644+ tracer , meter , aiohttp_server , monkeypatch
645+ ):
646+ _ , memory_exporter = tracer
647+ _ , metrics_reader = meter
648+ monkeypatch .setenv (
649+ OTEL_SEMCONV_STABILITY_OPT_IN , _StabilityMode .HTTP_DUP .value
650+ )
651+ _OpenTelemetrySemanticConventionStability ._initialized = False
652+
653+ AioHttpServerInstrumentor ().instrument ()
654+ app = aiohttp .web .Application ()
655+ app .router .add_get ("/test-path" , default_handler )
656+ server = await aiohttp_server (app )
657+ client_session = aiohttp .ClientSession ()
658+ try :
659+ url = f"http://{ server .host } :{ server .port } /test-path?query=test"
660+ async with client_session .get (
661+ url , headers = {"User-Agent" : "test-agent" }
662+ ) as response :
663+ assert response .status == 200
664+ spans = memory_exporter .get_finished_spans ()
665+ assert len (spans ) == 1
666+ span = spans [0 ]
667+
668+ # Both old and new semconv span attributes present
669+ assert span .attributes .get (HTTP_METHOD ) == "GET"
670+ assert span .attributes .get (HTTP_REQUEST_METHOD ) == "GET"
671+ assert span .attributes .get (HTTP_SCHEME ) == "http"
672+ assert span .attributes .get (URL_SCHEME ) == "http"
673+ assert span .attributes .get (NET_HOST_NAME ) == server .host
674+ assert span .attributes .get (SERVER_ADDRESS ) == server .host
675+ assert span .attributes .get (NET_HOST_PORT ) == server .port
676+ assert span .attributes .get (SERVER_PORT ) == server .port
677+ assert span .attributes .get (HTTP_TARGET ) == "/test-path?query=test"
678+ assert span .attributes .get (URL_PATH ) == "/test-path"
679+ assert span .attributes .get (URL_QUERY ) == "query=test"
680+ assert span .attributes .get (HTTP_USER_AGENT ) == "test-agent"
681+ assert span .attributes .get (USER_AGENT_ORIGINAL ) == "test-agent"
682+ assert span .attributes .get (HTTP_FLAVOR ) == "1.1"
683+ assert span .attributes .get (NETWORK_PROTOCOL_VERSION ) == "1.1"
684+ assert span .attributes .get (HTTP_STATUS_CODE ) == 200
685+ assert span .attributes .get (HTTP_RESPONSE_STATUS_CODE ) == 200
686+
687+ metrics = _get_sorted_metrics (metrics_reader .get_metrics_data ())
688+ assert len (metrics ) == 3 # Both duration metrics + active requests
689+ server_active_requests_count_attrs_both = list (
690+ _server_active_requests_count_attrs_old
691+ )
692+ server_active_requests_count_attrs_both .extend (
693+ _server_active_requests_count_attrs_new
694+ )
695+ recommended_metrics_attrs = {
696+ "http.server.active_requests" : server_active_requests_count_attrs_both ,
697+ "http.server.duration" : _server_duration_attrs_old ,
698+ "http.server.request.duration" : _server_duration_attrs_new ,
699+ }
700+ for metric in metrics :
701+ if metric .unit == "ms" :
702+ assert metric .name == "http.server.duration"
703+ elif metric .unit == "s" :
704+ assert metric .name == "http.server.request.duration"
705+ else :
706+ assert metric .name == "http.server.active_requests"
707+
708+ for point in metric .data .data_points :
709+ if (
710+ isinstance (point , HistogramDataPoint )
711+ and metric .name == "http.server.request.duration"
712+ ):
713+ assert (
714+ point .explicit_bounds
715+ == HTTP_DURATION_HISTOGRAM_BUCKETS_NEW
716+ )
717+ for attr in point .attributes :
718+ assert attr in recommended_metrics_attrs [metric .name ]
719+
720+ finally :
721+ await client_session .close ()
722+ AioHttpServerInstrumentor ().uninstrument ()
0 commit comments