From dbe90c5fcf3872ae99771de2c4299c5179ed871a Mon Sep 17 00:00:00 2001
From: Mateusz Rzeszutek <mrzeszutek@splunk.com>
Date: Mon, 6 Nov 2023 11:48:42 +0100
Subject: [PATCH] Change `network.protocol.name` from opt-in to conditionally
 required (#9797)

---
 .../http/HttpClientAttributesExtractor.java   |  1 +
 .../HttpClientAttributesExtractorBuilder.java |  3 ++-
 .../http/HttpCommonAttributesExtractor.java   | 23 ++++++++++++++++
 .../http/HttpServerAttributesExtractor.java   |  1 +
 .../HttpServerAttributesExtractorBuilder.java |  4 +--
 .../net/NetClientAttributesExtractor.java     |  2 +-
 .../net/NetServerAttributesExtractor.java     |  2 +-
 .../network/NetworkAttributesExtractor.java   |  2 +-
 .../InternalNetworkAttributesExtractor.java   | 20 +++++++-------
 ...entAttributesExtractorBothSemconvTest.java |  1 -
 ...verAttributesExtractorBothSemconvTest.java |  1 -
 ...tAttributesExtractorStableSemconvTest.java | 26 ++++++++++++++++++-
 ...rAttributesExtractorStableSemconvTest.java | 26 ++++++++++++++++++-
 .../HttpUrlConnectionTest.java                | 12 +++++----
 .../src/test/groovy/UndertowServerTest.groovy |  2 --
 .../junit/http/AbstractHttpClientTest.java    |  6 ++++-
 .../junit/http/AbstractHttpServerTest.java    |  5 +++-
 .../junit/http/HttpClientTestOptions.java     |  1 -
 18 files changed, 107 insertions(+), 31 deletions(-)

diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java
index 81e91b1be205..396e26d341cb 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java
@@ -91,6 +91,7 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
   HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder<REQUEST, RESPONSE> builder) {
     super(
         builder.httpAttributesGetter,
+        builder.netAttributesGetter,
         HttpStatusCodeConverter.CLIENT,
         builder.capturedRequestHeaders,
         builder.capturedResponseHeaders,
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java
index db5c12823fc2..dfb3f3f7774c 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBuilder.java
@@ -136,7 +136,8 @@ InternalNetworkAttributesExtractor<REQUEST, RESPONSE> buildNetworkExtractor() {
     return new InternalNetworkAttributesExtractor<>(
         netAttributesGetter,
         serverAddressAndPortExtractor,
-        /* captureNetworkTransportAndType= */ false,
+        // network.{transport,type} are opt-in, network.protocol.* have HTTP-specific logic
+        /* captureProtocolAttributes= */ false,
         /* captureLocalSocketAttributes= */ false,
         /* captureOldPeerDomainAttribute= */ true,
         SemconvStability.emitStableHttpSemconv(),
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java
index 9d93a87b5faf..bf52534da6c2 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpCommonAttributesExtractor.java
@@ -17,10 +17,12 @@
 import io.opentelemetry.context.Context;
 import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
 import io.opentelemetry.instrumentation.api.instrumenter.http.internal.HttpAttributes;
+import io.opentelemetry.instrumentation.api.instrumenter.network.NetworkAttributesGetter;
 import io.opentelemetry.instrumentation.api.internal.SemconvStability;
 import io.opentelemetry.semconv.SemanticAttributes;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -34,6 +36,7 @@ abstract class HttpCommonAttributesExtractor<
     implements AttributesExtractor<REQUEST, RESPONSE> {
 
   final GETTER getter;
+  final NetworkAttributesGetter<REQUEST, RESPONSE> networkGetter;
   private final HttpStatusCodeConverter statusCodeConverter;
   private final List<String> capturedRequestHeaders;
   private final List<String> capturedResponseHeaders;
@@ -41,11 +44,13 @@ abstract class HttpCommonAttributesExtractor<
 
   HttpCommonAttributesExtractor(
       GETTER getter,
+      NetworkAttributesGetter<REQUEST, RESPONSE> networkGetter,
       HttpStatusCodeConverter statusCodeConverter,
       List<String> capturedRequestHeaders,
       List<String> capturedResponseHeaders,
       Set<String> knownMethods) {
     this.getter = getter;
+    this.networkGetter = networkGetter;
     this.statusCodeConverter = statusCodeConverter;
     this.capturedRequestHeaders = lowercase(capturedRequestHeaders);
     this.capturedResponseHeaders = lowercase(capturedResponseHeaders);
@@ -143,6 +148,19 @@ public void onEnd(
       }
       internalSet(attributes, HttpAttributes.ERROR_TYPE, errorType);
     }
+
+    if (SemconvStability.emitStableHttpSemconv()) {
+      String protocolName = lowercaseStr(networkGetter.getNetworkProtocolName(request, response));
+      String protocolVersion =
+          lowercaseStr(networkGetter.getNetworkProtocolVersion(request, response));
+
+      if (protocolVersion != null) {
+        if (!"http".equals(protocolName)) {
+          internalSet(attributes, SemanticAttributes.NETWORK_PROTOCOL_NAME, protocolName);
+        }
+        internalSet(attributes, SemanticAttributes.NETWORK_PROTOCOL_VERSION, protocolVersion);
+      }
+    }
   }
 
   @Nullable
@@ -173,4 +191,9 @@ private static Long parseNumber(@Nullable String number) {
       return null;
     }
   }
+
+  @Nullable
+  private static String lowercaseStr(@Nullable String str) {
+    return str == null ? null : str.toLowerCase(Locale.ROOT);
+  }
 }
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java
index ce2120e8113c..7450a973c376 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractor.java
@@ -93,6 +93,7 @@ public static <REQUEST, RESPONSE> HttpServerAttributesExtractorBuilder<REQUEST,
   HttpServerAttributesExtractor(HttpServerAttributesExtractorBuilder<REQUEST, RESPONSE> builder) {
     super(
         builder.httpAttributesGetter,
+        builder.netAttributesGetter,
         HttpStatusCodeConverter.SERVER,
         builder.capturedRequestHeaders,
         builder.capturedResponseHeaders,
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java
index f524f48e06de..9370e0c8c44a 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBuilder.java
@@ -149,8 +149,8 @@ InternalNetworkAttributesExtractor<REQUEST, RESPONSE> buildNetworkExtractor() {
     return new InternalNetworkAttributesExtractor<>(
         netAttributesGetter,
         clientAddressPortExtractor,
-        // network.type and network.transport are opt-in
-        /* captureNetworkTransportAndType= */ false,
+        // network.{transport,type} are opt-in, network.protocol.* have HTTP-specific logic
+        /* captureProtocolAttributes= */ false,
         // network.local.* are opt-in
         /* captureLocalSocketAttributes= */ false,
         /* captureOldPeerDomainAttribute= */ false,
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java
index 5f028aec09eb..91a93979f3ed 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetClientAttributesExtractor.java
@@ -51,7 +51,7 @@ private NetClientAttributesExtractor(NetClientAttributesGetter<REQUEST, RESPONSE
         new InternalNetworkAttributesExtractor<>(
             getter,
             serverAddressAndPortExtractor,
-            /* captureNetworkTransportAndType= */ true,
+            /* captureProtocolAttributes= */ true,
             /* captureLocalSocketAttributes= */ false,
             /* captureOldPeerDomainAttribute= */ true,
             SemconvStability.emitStableHttpSemconv(),
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java
index b62bad5c29e2..aa6a92169d9c 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/NetServerAttributesExtractor.java
@@ -53,7 +53,7 @@ private NetServerAttributesExtractor(NetServerAttributesGetter<REQUEST, RESPONSE
         new InternalNetworkAttributesExtractor<>(
             getter,
             clientAddressAndPortExtractor,
-            /* captureNetworkTransportAndType= */ true,
+            /* captureProtocolAttributes= */ true,
             /* captureLocalSocketAttributes= */ true,
             /* captureOldPeerDomainAttribute= */ false,
             SemconvStability.emitStableHttpSemconv(),
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java
index e232a8db1067..55e8da02bd9e 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/NetworkAttributesExtractor.java
@@ -37,7 +37,7 @@ public static <REQUEST, RESPONSE> NetworkAttributesExtractor<REQUEST, RESPONSE>
         new InternalNetworkAttributesExtractor<>(
             getter,
             AddressAndPortExtractor.noop(),
-            /* captureNetworkTransportAndType= */ true,
+            /* captureProtocolAttributes= */ true,
             /* captureLocalSocketAttributes= */ true,
             // capture the old net.sock.peer.name attr for backwards compatibility
             /* captureOldPeerDomainAttribute= */ true,
diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java
index 6ebadd2657d5..f725d0b490f6 100644
--- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java
+++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/network/internal/InternalNetworkAttributesExtractor.java
@@ -23,7 +23,7 @@ public final class InternalNetworkAttributesExtractor<REQUEST, RESPONSE> {
 
   private final NetworkAttributesGetter<REQUEST, RESPONSE> getter;
   private final AddressAndPortExtractor<REQUEST> logicalPeerAddressAndPortExtractor;
-  private final boolean captureNetworkTransportAndType;
+  private final boolean captureProtocolAttributes;
   private final boolean captureLocalSocketAttributes;
   private final boolean captureOldPeerDomainAttribute;
   private final boolean emitStableUrlAttributes;
@@ -32,14 +32,14 @@ public final class InternalNetworkAttributesExtractor<REQUEST, RESPONSE> {
   public InternalNetworkAttributesExtractor(
       NetworkAttributesGetter<REQUEST, RESPONSE> getter,
       AddressAndPortExtractor<REQUEST> logicalPeerAddressAndPortExtractor,
-      boolean captureNetworkTransportAndType,
+      boolean captureProtocolAttributes,
       boolean captureLocalSocketAttributes,
       boolean captureOldPeerDomainAttribute,
       boolean emitStableUrlAttributes,
       boolean emitOldHttpAttributes) {
     this.getter = getter;
     this.logicalPeerAddressAndPortExtractor = logicalPeerAddressAndPortExtractor;
-    this.captureNetworkTransportAndType = captureNetworkTransportAndType;
+    this.captureProtocolAttributes = captureProtocolAttributes;
     this.captureLocalSocketAttributes = captureLocalSocketAttributes;
     this.captureOldPeerDomainAttribute = captureOldPeerDomainAttribute;
     this.emitStableUrlAttributes = emitStableUrlAttributes;
@@ -51,15 +51,13 @@ public void onEnd(AttributesBuilder attributes, REQUEST request, @Nullable RESPO
     String protocolName = lowercase(getter.getNetworkProtocolName(request, response));
     String protocolVersion = lowercase(getter.getNetworkProtocolVersion(request, response));
 
-    if (emitStableUrlAttributes) {
+    if (emitStableUrlAttributes && captureProtocolAttributes) {
       String transport = lowercase(getter.getNetworkTransport(request, response));
-      if (captureNetworkTransportAndType) {
-        internalSet(attributes, SemanticAttributes.NETWORK_TRANSPORT, transport);
-        internalSet(
-            attributes,
-            SemanticAttributes.NETWORK_TYPE,
-            lowercase(getter.getNetworkType(request, response)));
-      }
+      internalSet(attributes, SemanticAttributes.NETWORK_TRANSPORT, transport);
+      internalSet(
+          attributes,
+          SemanticAttributes.NETWORK_TYPE,
+          lowercase(getter.getNetworkType(request, response)));
       internalSet(attributes, SemanticAttributes.NETWORK_PROTOCOL_NAME, protocolName);
       internalSet(attributes, SemanticAttributes.NETWORK_PROTOCOL_VERSION, protocolVersion);
     }
diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java
index 9209f740353c..59f9e43d6a4e 100644
--- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java
+++ b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorBothSemconvTest.java
@@ -167,7 +167,6 @@ void normal() {
                 asList("654", "321")),
             entry(SemanticAttributes.NET_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NET_PROTOCOL_VERSION, "1.1"),
-            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "1.1"));
   }
 }
diff --git a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java
index 79676ef97486..55cc917cc854 100644
--- a/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java
+++ b/instrumentation-api-semconv/src/testBothHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorBothSemconvTest.java
@@ -169,7 +169,6 @@ void normal() {
         .containsOnly(
             entry(SemanticAttributes.NET_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NET_PROTOCOL_VERSION, "2.0"),
-            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "2.0"),
             entry(SemanticAttributes.HTTP_ROUTE, "/repositories/{repoId}"),
             entry(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, 10L),
diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java
index 9015b8dbe223..62952e3e27c5 100644
--- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java
+++ b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorStableSemconvTest.java
@@ -186,7 +186,6 @@ void normal() {
             entry(
                 AttributeKey.stringArrayKey("http.response.header.custom-response-header"),
                 asList("654", "321")),
-            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "1.1"),
             entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"),
             entry(NetworkAttributes.NETWORK_PEER_PORT, 456L));
@@ -397,4 +396,29 @@ void shouldExtractPeerAddressEvenIfItDuplicatesServerAddress() {
             entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4"),
             entry(NetworkAttributes.NETWORK_PEER_PORT, 456L));
   }
+
+  @Test
+  void shouldExtractProtocolNameDifferentFromHttp() {
+    Map<String, String> request = new HashMap<>();
+    request.put("networkProtocolName", "spdy");
+    request.put("networkProtocolVersion", "3.1");
+
+    Map<String, String> response = new HashMap<>();
+    response.put("statusCode", "200");
+
+    AttributesExtractor<Map<String, String>, Map<String, String>> extractor =
+        HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter());
+
+    AttributesBuilder startAttributes = Attributes.builder();
+    extractor.onStart(startAttributes, Context.root(), request);
+    assertThat(startAttributes.build()).isEmpty();
+
+    AttributesBuilder endAttributes = Attributes.builder();
+    extractor.onEnd(endAttributes, Context.root(), request, response, null);
+    assertThat(endAttributes.build())
+        .containsOnly(
+            entry(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 200L),
+            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "spdy"),
+            entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "3.1"));
+  }
 }
diff --git a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java
index 045d1e7185ad..d61ad91a7f74 100644
--- a/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java
+++ b/instrumentation-api-semconv/src/testStableHttpSemconv/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpServerAttributesExtractorStableSemconvTest.java
@@ -211,7 +211,6 @@ void normal() {
     extractor.onEnd(endAttributes, Context.root(), request, response, null);
     assertThat(endAttributes.build())
         .containsOnly(
-            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "http"),
             entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "2.0"),
             entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "4.3.2.1"),
             entry(NetworkAttributes.NETWORK_PEER_PORT, 456L),
@@ -528,4 +527,29 @@ void shouldExtractPeerAddressEvenIfItDuplicatesClientAddress() {
             entry(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4"),
             entry(NetworkAttributes.NETWORK_PEER_PORT, 456L));
   }
+
+  @Test
+  void shouldExtractProtocolNameDifferentFromHttp() {
+    Map<String, String> request = new HashMap<>();
+    request.put("networkProtocolName", "spdy");
+    request.put("networkProtocolVersion", "3.1");
+
+    Map<String, String> response = new HashMap<>();
+    response.put("statusCode", "200");
+
+    AttributesExtractor<Map<String, String>, Map<String, String>> extractor =
+        HttpServerAttributesExtractor.create(new TestHttpServerAttributesGetter());
+
+    AttributesBuilder startAttributes = Attributes.builder();
+    extractor.onStart(startAttributes, Context.root(), request);
+    assertThat(startAttributes.build()).isEmpty();
+
+    AttributesBuilder endAttributes = Attributes.builder();
+    extractor.onEnd(endAttributes, Context.root(), request, response, null);
+    assertThat(endAttributes.build())
+        .containsOnly(
+            entry(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 200L),
+            entry(SemanticAttributes.NETWORK_PROTOCOL_NAME, "spdy"),
+            entry(SemanticAttributes.NETWORK_PROTOCOL_VERSION, "3.1"));
+  }
 }
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java
index 96a7067f07b4..ea8df5e87086 100644
--- a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java
@@ -124,7 +124,6 @@ public void traceRequest(boolean useCache) throws IOException {
     List<AttributeAssertion> attributes =
         new ArrayList<>(
             Arrays.asList(
-                equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME), "http"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION), "1.1"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_NAME), "localhost"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_PORT), url.getPort()),
@@ -132,6 +131,7 @@ public void traceRequest(boolean useCache) throws IOException {
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_METHOD), "GET"),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_STATUS_CODE), STATUS)));
     if (SemconvStability.emitOldHttpSemconv()) {
+      attributes.add(equalTo(SemanticAttributes.NET_PROTOCOL_NAME, "http"));
       attributes.add(
           satisfies(
               SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, AbstractLongAssert::isNotNegative));
@@ -176,7 +176,6 @@ public void testBrokenApiUsage() throws IOException {
     List<AttributeAssertion> attributes =
         new ArrayList<>(
             Arrays.asList(
-                equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME), "http"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION), "1.1"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_NAME), "localhost"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_PORT), url.getPort()),
@@ -184,6 +183,7 @@ public void testBrokenApiUsage() throws IOException {
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_METHOD), "GET"),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_STATUS_CODE), STATUS)));
     if (SemconvStability.emitOldHttpSemconv()) {
+      attributes.add(equalTo(SemanticAttributes.NET_PROTOCOL_NAME, "http"));
       attributes.add(
           satisfies(
               SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, AbstractLongAssert::isNotNegative));
@@ -234,7 +234,6 @@ public void testPostRequest() throws IOException {
     List<AttributeAssertion> attributes =
         new ArrayList<>(
             Arrays.asList(
-                equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME), "http"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION), "1.1"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_NAME), "localhost"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_PORT), url.getPort()),
@@ -242,6 +241,7 @@ public void testPostRequest() throws IOException {
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_METHOD), "POST"),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_STATUS_CODE), STATUS)));
     if (SemconvStability.emitOldHttpSemconv()) {
+      attributes.add(equalTo(SemanticAttributes.NET_PROTOCOL_NAME, "http"));
       attributes.add(
           satisfies(
               SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, AbstractLongAssert::isNotNegative));
@@ -297,7 +297,6 @@ public void getOutputStreamShouldTransformGetIntoPost() throws IOException {
     List<AttributeAssertion> attributes =
         new ArrayList<>(
             Arrays.asList(
-                equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME), "http"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION), "1.1"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_NAME), "localhost"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_PORT), url.getPort()),
@@ -305,6 +304,7 @@ public void getOutputStreamShouldTransformGetIntoPost() throws IOException {
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_METHOD), "POST"),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_STATUS_CODE), STATUS)));
     if (SemconvStability.emitOldHttpSemconv()) {
+      attributes.add(equalTo(SemanticAttributes.NET_PROTOCOL_NAME, "http"));
       attributes.add(
           satisfies(
               SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, AbstractLongAssert::isNotNegative));
@@ -349,12 +349,14 @@ public void traceRequestWithConnectionFailure(String scheme) {
     List<AttributeAssertion> attributes =
         new ArrayList<>(
             Arrays.asList(
-                equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME), "http"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION), "1.1"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_NAME), "localhost"),
                 equalTo(getAttributeKey(SemanticAttributes.NET_PEER_PORT), PortUtils.UNUSABLE_PORT),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_URL), uri),
                 equalTo(getAttributeKey(SemanticAttributes.HTTP_METHOD), "GET")));
+    if (SemconvStability.emitOldHttpSemconv()) {
+      attributes.add(equalTo(SemanticAttributes.NET_PROTOCOL_NAME, "http"));
+    }
     if (SemconvStability.emitStableHttpSemconv()) {
       attributes.add(equalTo(HttpAttributes.ERROR_TYPE, "java.net.ConnectException"));
     }
diff --git a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy
index 17d0a421b801..7b67bd3ba17b 100644
--- a/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy
+++ b/instrumentation/undertow-1.4/javaagent/src/test/groovy/UndertowServerTest.groovy
@@ -171,7 +171,6 @@ class UndertowServerTest extends HttpServerTest<Undertow> implements AgentTestTr
               "$SemanticAttributes.HTTP_REQUEST_METHOD" "GET"
               "$SemanticAttributes.HTTP_RESPONSE_STATUS_CODE" 200
               "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT
-              "$SemanticAttributes.NETWORK_PROTOCOL_NAME" "http"
               "$SemanticAttributes.NETWORK_PROTOCOL_VERSION" "1.1"
               "$SemanticAttributes.SERVER_ADDRESS" uri.host
               "$SemanticAttributes.SERVER_PORT" uri.port
@@ -242,7 +241,6 @@ class UndertowServerTest extends HttpServerTest<Undertow> implements AgentTestTr
               "$SemanticAttributes.HTTP_REQUEST_METHOD" "GET"
               "$SemanticAttributes.HTTP_RESPONSE_STATUS_CODE" 200
               "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT
-              "$SemanticAttributes.NETWORK_PROTOCOL_NAME" "http"
               "$SemanticAttributes.NETWORK_PROTOCOL_VERSION" "1.1"
               "$SemanticAttributes.SERVER_ADDRESS" uri.host
               "$SemanticAttributes.SERVER_PORT" uri.port
diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java
index 6af2a2b768c5..afaed5020870 100644
--- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java
+++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java
@@ -1024,9 +1024,13 @@ SpanDataAssert assertClientSpan(
                     .doesNotContainKey(SemanticAttributes.NETWORK_TRANSPORT)
                     .doesNotContainKey(SemanticAttributes.NETWORK_TYPE);
               }
+
               AttributeKey<String> netProtocolKey =
                   getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME);
-              if (httpClientAttributes.contains(netProtocolKey)) {
+              if (SemconvStability.emitStableHttpSemconv()) {
+                // only protocol names different from "http" are emitted
+                assertThat(attrs).doesNotContainKey(netProtocolKey);
+              } else if (attrs.get(netProtocolKey) != null) {
                 assertThat(attrs).containsEntry(netProtocolKey, "http");
               }
               AttributeKey<String> netProtocolVersionKey =
diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java
index 4205f331b481..852146ddfcd6 100644
--- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java
+++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpServerTest.java
@@ -798,7 +798,10 @@ protected SpanDataAssert assertServerSpan(
 
           AttributeKey<String> netProtocolKey =
               getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME);
-          if (attrs.get(netProtocolKey) != null) {
+          if (SemconvStability.emitStableHttpSemconv()) {
+            // only protocol names different from "http" are emitted
+            assertThat(attrs).doesNotContainKey(netProtocolKey);
+          } else if (attrs.get(netProtocolKey) != null) {
             assertThat(attrs).containsEntry(netProtocolKey, "http");
           }
           AttributeKey<String> netProtocolVersionKey =
diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java
index 6567b3e0a78c..e78c2c63e600 100644
--- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java
+++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java
@@ -28,7 +28,6 @@ public abstract class HttpClientTestOptions {
       Collections.unmodifiableSet(
           new HashSet<>(
               Arrays.asList(
-                  SemconvStabilityUtil.getAttributeKey(SemanticAttributes.NET_PROTOCOL_NAME),
                   SemconvStabilityUtil.getAttributeKey(SemanticAttributes.NET_PROTOCOL_VERSION),
                   SemconvStabilityUtil.getAttributeKey(SemanticAttributes.NET_PEER_NAME),
                   SemconvStabilityUtil.getAttributeKey(SemanticAttributes.NET_PEER_PORT),