From c6a74f9331ae1ef0f7df889a9b1da7061768e276 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sat, 24 Oct 2026 16:29:33 +0530 Subject: [PATCH 1/2] fix(http): enclose proceed in handler span lifecycle and propagate span in UserAgent and UrlReplace handlers --- .../http/middleware/UrlReplaceHandler.java | 5 +++- .../http/middleware/UserAgentHandler.java | 5 +++- .../middleware/UrlReplaceHandlerTest.java | 23 +++++++++++++++++++ .../http/middleware/UserAgentHandlerTest.java | 18 +++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UrlReplaceHandler.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UrlReplaceHandler.java index cf2d2bbaa..75d881b87 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UrlReplaceHandler.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UrlReplaceHandler.java @@ -61,6 +61,10 @@ public Response intercept(@Nonnull Chain chain) throws IOException { } try { request = replaceRequestUrl(request, replaceOption.getReplacementPairs()); + if (span != null) { + request = request.newBuilder().tag(Span.class, span).build(); + } + return chain.proceed(request); } finally { if (scope != null) { scope.close(); @@ -69,7 +73,6 @@ public Response intercept(@Nonnull Chain chain) throws IOException { span.end(); } } - return chain.proceed(request); } /** diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UserAgentHandler.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UserAgentHandler.java index 8e6eb7500..ec2831815 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UserAgentHandler.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/UserAgentHandler.java @@ -73,6 +73,10 @@ public Response intercept(final Chain chain) throws IOException { } else if (!currentValue.contains(valueToAppend)) { builder.header(USER_AGENT_HEADER_NAME, currentValue + " " + valueToAppend); } + if (span != null) { + builder.tag(Span.class, span); + } + return chain.proceed(builder.build()); } finally { if (scope != null) { scope.close(); @@ -81,6 +85,5 @@ public Response intercept(final Chain chain) throws IOException { span.end(); } } - return chain.proceed(builder.build()); } } diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java index 0c04f014e..be29bd025 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; import com.microsoft.kiota.http.KiotaClientFactory; import com.microsoft.kiota.http.middleware.options.UrlReplaceHandlerOption; @@ -96,4 +97,26 @@ void testItDoesNotReplaceTokensInThePath() throws IOException { "https://graph.microsoft.com/v1.0/me/calendar/events/AAMkADM4Njk4YzMzLTc3YTktNGQwZi05ZjRjLTJlNTUzYWMxZjAxNABGAAAAAABzQLabgoJzT6ewNe8DeydoBwBoFwNVrXtLQ5PleemFoZ87AAAAAAENAABoFwNVrXtLQ5PleemFoZ87AAA33T-eAAA%3D", response.request().url().toString()); } + + @Test + void testUrlReplaceHandler_propagatesSpan() throws IOException { + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final Interceptor.Chain mockChain = mock(Interceptor.Chain.class); + final Response mockResponse = mock(Response.class); + defaultReplacementPairs.put("/users/TokenToReplace", "/me"); + final UrlReplaceHandler handler = + new UrlReplaceHandler(new UrlReplaceHandlerOption(defaultReplacementPairs)); + final Request request = + new Request.Builder() + .url(DEFAULT_URL_WITH_TOKEN) + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + when(mockChain.proceed(any(Request.class))).thenReturn(mockResponse); + + final Response response = handler.intercept(mockChain); + assertNotNull(response); + verify(mockChain).proceed(argThat(r -> r.tag(io.opentelemetry.api.trace.Span.class) != null)); + } } diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java index 1b8fe4498..cd3291994 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java @@ -84,4 +84,22 @@ void doesNotAddTheProductWhenDisabled() throws IOException { assertNotNull(result); assertNull(result.header("User-Agent")); } + + @Test + void enclosesProceedAndPropagatesSpan() throws IOException { + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final UserAgentHandler handler = new UserAgentHandler(); + final Request request = + new Request.Builder() + .url("http://localhost") + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + + final Response response = handler.intercept(mockChain); + final Request result = response.request(); + assertNotNull(result); + assertNotNull(result.tag(io.opentelemetry.api.trace.Span.class)); + } } From 02c59b52de703954f0ad6a4543b97f8430bbe03c Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Sun, 25 Oct 2026 22:38:08 +0530 Subject: [PATCH 2/2] test(http): add span and scope lifecycle assertions during and after proceed --- .../middleware/UrlReplaceHandlerTest.java | 110 +++++++++++++++--- .../http/middleware/UserAgentHandlerTest.java | 98 +++++++++++++--- 2 files changed, 177 insertions(+), 31 deletions(-) diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java index be29bd025..4a6e88210 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UrlReplaceHandlerTest.java @@ -2,17 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; import com.microsoft.kiota.http.KiotaClientFactory; import com.microsoft.kiota.http.middleware.options.UrlReplaceHandlerOption; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; + import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import java.io.IOException; import java.util.HashMap; @@ -100,23 +108,89 @@ void testItDoesNotReplaceTokensInThePath() throws IOException { @Test void testUrlReplaceHandler_propagatesSpan() throws IOException { - final com.microsoft.kiota.http.ObservabilityOptions obsOptions = - new com.microsoft.kiota.http.ObservabilityOptions(); - final Interceptor.Chain mockChain = mock(Interceptor.Chain.class); - final Response mockResponse = mock(Response.class); - defaultReplacementPairs.put("/users/TokenToReplace", "/me"); - final UrlReplaceHandler handler = - new UrlReplaceHandler(new UrlReplaceHandlerOption(defaultReplacementPairs)); - final Request request = - new Request.Builder() - .url(DEFAULT_URL_WITH_TOKEN) - .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) - .build(); - when(mockChain.request()).thenReturn(request); - when(mockChain.proceed(any(Request.class))).thenReturn(mockResponse); - - final Response response = handler.intercept(mockChain); - assertNotNull(response); - verify(mockChain).proceed(argThat(r -> r.tag(io.opentelemetry.api.trace.Span.class) != null)); + try (MockedStatic otelMock = mockStatic(GlobalOpenTelemetry.class)) { + final Tracer mockTracer = mock(Tracer.class); + final SpanBuilder mockSpanBuilder = mock(SpanBuilder.class); + final Span mockSpan = mock(Span.class); + final io.opentelemetry.context.Scope mockScope = + mock(io.opentelemetry.context.Scope.class); + + otelMock.when(() -> GlobalOpenTelemetry.getTracer(anyString())).thenReturn(mockTracer); + when(mockTracer.spanBuilder(anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.startSpan()).thenReturn(mockSpan); + when(mockSpan.makeCurrent()).thenReturn(mockScope); + + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final Interceptor.Chain mockChain = mock(Interceptor.Chain.class); + final Response mockResponse = mock(Response.class); + defaultReplacementPairs.put("/users/TokenToReplace", "/me"); + final UrlReplaceHandler handler = + new UrlReplaceHandler(new UrlReplaceHandlerOption(defaultReplacementPairs)); + final Request request = + new Request.Builder() + .url(DEFAULT_URL_WITH_TOKEN) + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + when(mockChain.proceed(any(Request.class))) + .thenAnswer( + invocation -> { + final Request req = invocation.getArgument(0); + assertSame(mockSpan, req.tag(Span.class)); + verify(mockSpan, never()).end(); + verify(mockScope, never()).close(); + return mockResponse; + }); + + final Response response = handler.intercept(mockChain); + assertNotNull(response); + verify(mockScope).close(); + verify(mockSpan).end(); + } + } + + @Test + void testUrlReplaceHandler_cleansUpSpanOnException() throws IOException { + try (MockedStatic otelMock = mockStatic(GlobalOpenTelemetry.class)) { + final Tracer mockTracer = mock(Tracer.class); + final SpanBuilder mockSpanBuilder = mock(SpanBuilder.class); + final Span mockSpan = mock(Span.class); + final io.opentelemetry.context.Scope mockScope = + mock(io.opentelemetry.context.Scope.class); + + otelMock.when(() -> GlobalOpenTelemetry.getTracer(anyString())).thenReturn(mockTracer); + when(mockTracer.spanBuilder(anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.startSpan()).thenReturn(mockSpan); + when(mockSpan.makeCurrent()).thenReturn(mockScope); + + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final Interceptor.Chain mockChain = mock(Interceptor.Chain.class); + defaultReplacementPairs.put("/users/TokenToReplace", "/me"); + final UrlReplaceHandler handler = + new UrlReplaceHandler(new UrlReplaceHandlerOption(defaultReplacementPairs)); + final Request request = + new Request.Builder() + .url(DEFAULT_URL_WITH_TOKEN) + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + when(mockChain.proceed(any(Request.class))) + .thenAnswer( + invocation -> { + final Request req = invocation.getArgument(0); + assertSame(mockSpan, req.tag(Span.class)); + verify(mockSpan, never()).end(); + verify(mockScope, never()).close(); + throw new IOException("network failure"); + }); + + assertThrows(IOException.class, () -> handler.intercept(mockChain)); + verify(mockScope).close(); + verify(mockSpan).end(); + } } } diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java index cd3291994..61421675b 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/middleware/UserAgentHandlerTest.java @@ -3,10 +3,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; import com.microsoft.kiota.http.middleware.options.UserAgentHandlerOption; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; + import okhttp3.Headers; import okhttp3.Interceptor.Chain; import okhttp3.Request; @@ -14,6 +21,7 @@ import okhttp3.ResponseBody; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -87,19 +95,83 @@ void doesNotAddTheProductWhenDisabled() throws IOException { @Test void enclosesProceedAndPropagatesSpan() throws IOException { - final com.microsoft.kiota.http.ObservabilityOptions obsOptions = - new com.microsoft.kiota.http.ObservabilityOptions(); - final UserAgentHandler handler = new UserAgentHandler(); - final Request request = - new Request.Builder() - .url("http://localhost") - .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) - .build(); - when(mockChain.request()).thenReturn(request); + try (MockedStatic otelMock = mockStatic(GlobalOpenTelemetry.class)) { + final Tracer mockTracer = mock(Tracer.class); + final SpanBuilder mockSpanBuilder = mock(SpanBuilder.class); + final Span mockSpan = mock(Span.class); + final io.opentelemetry.context.Scope mockScope = + mock(io.opentelemetry.context.Scope.class); - final Response response = handler.intercept(mockChain); - final Request result = response.request(); - assertNotNull(result); - assertNotNull(result.tag(io.opentelemetry.api.trace.Span.class)); + otelMock.when(() -> GlobalOpenTelemetry.getTracer(anyString())).thenReturn(mockTracer); + when(mockTracer.spanBuilder(anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.startSpan()).thenReturn(mockSpan); + when(mockSpan.makeCurrent()).thenReturn(mockScope); + + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final UserAgentHandler handler = new UserAgentHandler(); + final Request request = + new Request.Builder() + .url("http://localhost") + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + when(mockChain.proceed(any(Request.class))) + .thenAnswer( + invocation -> { + final Request req = invocation.getArgument(0); + assertSame(mockSpan, req.tag(Span.class)); + verify(mockSpan, never()).end(); + verify(mockScope, never()).close(); + when(mockResponse.request()).thenReturn(req); + return mockResponse; + }); + + final Response response = handler.intercept(mockChain); + assertNotNull(response); + verify(mockScope).close(); + verify(mockSpan).end(); + } + } + + @Test + void enclosesProceedAndCleansUpOnException() throws IOException { + try (MockedStatic otelMock = mockStatic(GlobalOpenTelemetry.class)) { + final Tracer mockTracer = mock(Tracer.class); + final SpanBuilder mockSpanBuilder = mock(SpanBuilder.class); + final Span mockSpan = mock(Span.class); + final io.opentelemetry.context.Scope mockScope = + mock(io.opentelemetry.context.Scope.class); + + otelMock.when(() -> GlobalOpenTelemetry.getTracer(anyString())).thenReturn(mockTracer); + when(mockTracer.spanBuilder(anyString())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); + when(mockSpanBuilder.startSpan()).thenReturn(mockSpan); + when(mockSpan.makeCurrent()).thenReturn(mockScope); + + final com.microsoft.kiota.http.ObservabilityOptions obsOptions = + new com.microsoft.kiota.http.ObservabilityOptions(); + final UserAgentHandler handler = new UserAgentHandler(); + final Request request = + new Request.Builder() + .url("http://localhost") + .tag(com.microsoft.kiota.http.ObservabilityOptions.class, obsOptions) + .build(); + when(mockChain.request()).thenReturn(request); + when(mockChain.proceed(any(Request.class))) + .thenAnswer( + invocation -> { + final Request req = invocation.getArgument(0); + assertSame(mockSpan, req.tag(Span.class)); + verify(mockSpan, never()).end(); + verify(mockScope, never()).close(); + throw new IOException("network failure"); + }); + + assertThrows(IOException.class, () -> handler.intercept(mockChain)); + verify(mockScope).close(); + verify(mockSpan).end(); + } } }