From 49f3c4c8aa77e014165b9ab12327a1ee25ce3f4a Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Mon, 1 Dec 2025 13:54:10 -0500 Subject: [PATCH 1/3] Add test and header fallback --- .../webflux/v5_3/WebfluxTextMapGetter.java | 2 +- .../webflux/v5_3/internal/HeaderUtil.java | 56 +++++++ .../v5_3/WebfluxTextMapGetterTest.java | 153 ++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetter.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetter.java index fe973696de77..e66cd5084c4d 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetter.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetter.java @@ -18,7 +18,7 @@ enum WebfluxTextMapGetter implements TextMapGetter { @Override public Iterable keys(ServerWebExchange exchange) { - return exchange.getRequest().getHeaders().keySet(); + return HeaderUtil.getKeys(exchange.getRequest().getHeaders()); } @Nullable diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java index 33464ea260e6..e2a8ec5d9cd5 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java @@ -6,11 +6,16 @@ package io.opentelemetry.instrumentation.spring.webflux.v5_3.internal; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Supplier; import javax.annotation.Nullable; import org.springframework.http.HttpHeaders; @@ -23,6 +28,8 @@ public final class HeaderUtil { @Nullable private static final MethodHandle GET_HEADERS; + @Nullable private static final MethodHandle FOR_EACH; + @Nullable private static final MethodHandle KEY_SET; static { GET_HEADERS = @@ -32,6 +39,33 @@ public final class HeaderUtil { () -> findGetHeadersMethod( MethodType.methodType(List.class, Object.class))); // before spring web 7.0 + + // Spring Web 7+ + MethodHandle forEach = null; + try { + forEach = + MethodHandles.lookup() + .findVirtual( + HttpHeaders.class, + "forEach", + MethodType.methodType(void.class, BiConsumer.class)); + } catch (Throwable t) { + // ignore - will fall back to keySet + } + FOR_EACH = forEach; + + // Spring Web 6 and earlier + MethodHandle keySet = null; + if (FOR_EACH == null) { + try { + keySet = + MethodHandles.lookup() + .findVirtual(Map.class, "keySet", MethodType.methodType(Set.class)); + } catch (Throwable t) { + // ignore + } + } + KEY_SET = keySet; } @Nullable @@ -64,5 +98,27 @@ public static List getHeader(HttpHeaders headers, String name) { return emptyList(); } + @SuppressWarnings("unchecked") // HttpHeaders is a Map in Spring Web 6 and earlier + public static Set getKeys(HttpHeaders headers) { + if (FOR_EACH != null) { + // Spring Web 7: HttpHeaders has forEach(BiConsumer) method + try { + Set keys = new HashSet<>(); + FOR_EACH.invoke(headers, (BiConsumer) (key, value) -> keys.add(key)); + return keys; + } catch (Throwable t) { + // ignore + } + } else if (KEY_SET != null) { + // Spring Web 6 and earlier: HttpHeaders extends Map + try { + return (Set) KEY_SET.invoke(headers); + } catch (Throwable t) { + // ignore + } + } + return emptySet(); + } + private HeaderUtil() {} } diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java new file mode 100644 index 000000000000..09867ab6b868 --- /dev/null +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.webflux.v5_3; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +class WebfluxTextMapGetterTest { + + @Test + void testKeysWithMultipleHeaders() { + MockServerHttpRequest request = + MockServerHttpRequest.get("/test") + .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + .header("tracestate", "congo=t61rcWkgMzE") + .header("custom-header", "custom-value") + .header("x-forwarded-for", "192.168.1.1") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); + assertThat(keys).contains("traceparent", "tracestate", "custom-header", "x-forwarded-for"); + } + + @Test + void testGet() { + MockServerHttpRequest request = + MockServerHttpRequest.get("/test") + .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + .header("custom-header", "custom-value") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + String traceparent = WebfluxTextMapGetter.INSTANCE.get(exchange, "traceparent"); + assertThat(traceparent).isEqualTo("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + + String customHeader = WebfluxTextMapGetter.INSTANCE.get(exchange, "custom-header"); + assertThat(customHeader).isEqualTo("custom-value"); + } + + @Test + void testGetAll() { + MockServerHttpRequest request = + MockServerHttpRequest.get("/test") + .header("accept", "application/json") + .header("accept", "text/html") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + List acceptHeaders = new ArrayList<>(); + WebfluxTextMapGetter.INSTANCE.getAll(exchange, "accept").forEachRemaining(acceptHeaders::add); + + assertThat(acceptHeaders).containsExactly("application/json", "text/html"); + } + + @Test + void testGetAllSingleValue() { + MockServerHttpRequest request = + MockServerHttpRequest.get("/test").header("content-type", "application/json").build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + List contentTypes = new ArrayList<>(); + WebfluxTextMapGetter.INSTANCE + .getAll(exchange, "content-type") + .forEachRemaining(contentTypes::add); + + assertThat(contentTypes).containsExactly("application/json"); + } + + @Test + void testGetNullExchange() { + String result = WebfluxTextMapGetter.INSTANCE.get(null, "any-header"); + assertThat(result).isNull(); + } + + @Test + void testKeysDirectlyOnHttpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + headers.add("tracestate", "congo=t61rcWkgMzE"); + headers.add("custom-header", "custom-value"); + + MockServerHttpRequest request = MockServerHttpRequest.get("/test").headers(headers).build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + // The keys() method internally calls HttpHeaders.keySet() + // This will throw NoSuchMethodError with Spring Web 7 if not properly handled + Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); + assertThat(keys).hasSize(3).contains("traceparent", "tracestate", "custom-header"); + } + + @Test + void testGetAllNullExchange() { + assertThat(WebfluxTextMapGetter.INSTANCE.getAll(null, "any-header")).isExhausted(); + } + + @Test + void testKeysWithBaggageHeader() { + // Test that baggage headers are properly returned by keys() + MockServerHttpRequest request = + MockServerHttpRequest.get("/test") + .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + .header("baggage", "test-baggage-key-1=test-baggage-value-1") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); + assertThat(keys).contains("traceparent", "baggage"); + + String baggageValue = WebfluxTextMapGetter.INSTANCE.get(exchange, "baggage"); + assertThat(baggageValue).isEqualTo("test-baggage-key-1=test-baggage-value-1"); + } + + @Test + void testKeysWithMultipleBaggageHeaders() { + // Test that multiple baggage headers are properly returned by keys() + // The W3C Baggage propagator needs to iterate through all headers to find baggage entries + MockServerHttpRequest request = + MockServerHttpRequest.get("/test") + .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + .header("baggage", "test-baggage-key-1=test-baggage-value-1") + .header("baggage", "test-baggage-key-2=test-baggage-value-2") + .header("x-custom", "custom-value") + .build(); + + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); + assertThat(keys).contains("traceparent", "baggage", "x-custom"); + + List baggageValues = new ArrayList<>(); + WebfluxTextMapGetter.INSTANCE.getAll(exchange, "baggage").forEachRemaining(baggageValues::add); + + assertThat(baggageValues) + .containsExactly( + "test-baggage-key-1=test-baggage-value-1", "test-baggage-key-2=test-baggage-value-2"); + } +} From 59392a4bf9a2e1f6aaa19b17d6c5808ce68fd699 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Tue, 2 Dec 2025 07:24:09 -0500 Subject: [PATCH 2/3] use different approach to getting headers in spring 7, cast to map for spring 6 --- .../webflux/v5_3/internal/HeaderUtil.java | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java index e2a8ec5d9cd5..323d9b25bba7 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/main/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/internal/HeaderUtil.java @@ -11,11 +11,9 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.BiConsumer; import java.util.function.Supplier; import javax.annotation.Nullable; import org.springframework.http.HttpHeaders; @@ -28,8 +26,7 @@ public final class HeaderUtil { @Nullable private static final MethodHandle GET_HEADERS; - @Nullable private static final MethodHandle FOR_EACH; - @Nullable private static final MethodHandle KEY_SET; + @Nullable private static final MethodHandle HEADER_NAMES; static { GET_HEADERS = @@ -41,31 +38,15 @@ public final class HeaderUtil { MethodType.methodType(List.class, Object.class))); // before spring web 7.0 // Spring Web 7+ - MethodHandle forEach = null; + MethodHandle headerNames = null; try { - forEach = + headerNames = MethodHandles.lookup() - .findVirtual( - HttpHeaders.class, - "forEach", - MethodType.methodType(void.class, BiConsumer.class)); + .findVirtual(HttpHeaders.class, "headerNames", MethodType.methodType(Set.class)); } catch (Throwable t) { - // ignore - will fall back to keySet + // ignore - will fall back to casting to Map } - FOR_EACH = forEach; - - // Spring Web 6 and earlier - MethodHandle keySet = null; - if (FOR_EACH == null) { - try { - keySet = - MethodHandles.lookup() - .findVirtual(Map.class, "keySet", MethodType.methodType(Set.class)); - } catch (Throwable t) { - // ignore - } - } - KEY_SET = keySet; + HEADER_NAMES = headerNames; } @Nullable @@ -100,22 +81,19 @@ public static List getHeader(HttpHeaders headers, String name) { @SuppressWarnings("unchecked") // HttpHeaders is a Map in Spring Web 6 and earlier public static Set getKeys(HttpHeaders headers) { - if (FOR_EACH != null) { - // Spring Web 7: HttpHeaders has forEach(BiConsumer) method + if (HEADER_NAMES != null) { + // Spring Web 7: HttpHeaders has headerNames() method try { - Set keys = new HashSet<>(); - FOR_EACH.invoke(headers, (BiConsumer) (key, value) -> keys.add(key)); - return keys; + Set result = (Set) HEADER_NAMES.invoke(headers); + if (result != null) { + return result; + } } catch (Throwable t) { // ignore } - } else if (KEY_SET != null) { + } else { // Spring Web 6 and earlier: HttpHeaders extends Map - try { - return (Set) KEY_SET.invoke(headers); - } catch (Throwable t) { - // ignore - } + return ((Map>) headers).keySet(); } return emptySet(); } From 3536cc5ca8c43579a3ec10a9c0703e1163adce45 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 3 Dec 2025 11:25:56 -0500 Subject: [PATCH 3/3] slim down tests --- .../v5_3/WebfluxTextMapGetterTest.java | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java index 09867ab6b868..e0bbb1cc79ba 100644 --- a/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java +++ b/instrumentation/spring/spring-webflux/spring-webflux-5.3/library/src/test/java/io/opentelemetry/instrumentation/spring/webflux/v5_3/WebfluxTextMapGetterTest.java @@ -10,29 +10,11 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; -import org.springframework.web.server.ServerWebExchange; class WebfluxTextMapGetterTest { - @Test - void testKeysWithMultipleHeaders() { - MockServerHttpRequest request = - MockServerHttpRequest.get("/test") - .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") - .header("tracestate", "congo=t61rcWkgMzE") - .header("custom-header", "custom-value") - .header("x-forwarded-for", "192.168.1.1") - .build(); - - MockServerWebExchange exchange = MockServerWebExchange.from(request); - - Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); - assertThat(keys).contains("traceparent", "tracestate", "custom-header", "x-forwarded-for"); - } - @Test void testGet() { MockServerHttpRequest request = @@ -66,51 +48,8 @@ void testGetAll() { assertThat(acceptHeaders).containsExactly("application/json", "text/html"); } - @Test - void testGetAllSingleValue() { - MockServerHttpRequest request = - MockServerHttpRequest.get("/test").header("content-type", "application/json").build(); - - MockServerWebExchange exchange = MockServerWebExchange.from(request); - - List contentTypes = new ArrayList<>(); - WebfluxTextMapGetter.INSTANCE - .getAll(exchange, "content-type") - .forEachRemaining(contentTypes::add); - - assertThat(contentTypes).containsExactly("application/json"); - } - - @Test - void testGetNullExchange() { - String result = WebfluxTextMapGetter.INSTANCE.get(null, "any-header"); - assertThat(result).isNull(); - } - - @Test - void testKeysDirectlyOnHttpHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.add("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); - headers.add("tracestate", "congo=t61rcWkgMzE"); - headers.add("custom-header", "custom-value"); - - MockServerHttpRequest request = MockServerHttpRequest.get("/test").headers(headers).build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - // The keys() method internally calls HttpHeaders.keySet() - // This will throw NoSuchMethodError with Spring Web 7 if not properly handled - Iterable keys = WebfluxTextMapGetter.INSTANCE.keys(exchange); - assertThat(keys).hasSize(3).contains("traceparent", "tracestate", "custom-header"); - } - - @Test - void testGetAllNullExchange() { - assertThat(WebfluxTextMapGetter.INSTANCE.getAll(null, "any-header")).isExhausted(); - } - @Test void testKeysWithBaggageHeader() { - // Test that baggage headers are properly returned by keys() MockServerHttpRequest request = MockServerHttpRequest.get("/test") .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") @@ -128,8 +67,6 @@ void testKeysWithBaggageHeader() { @Test void testKeysWithMultipleBaggageHeaders() { - // Test that multiple baggage headers are properly returned by keys() - // The W3C Baggage propagator needs to iterate through all headers to find baggage entries MockServerHttpRequest request = MockServerHttpRequest.get("/test") .header("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")