diff --git a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java index e56b297349..3c4222e986 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java @@ -118,13 +118,13 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter { throw ex; } finally { - // If async is started, register a listener for completion notification. - if (request.isAsyncStarted()) { + // If async is started during the first dispatch, register a listener for completion notification. + if (request.isAsyncStarted() && request.getDispatcherType() == DispatcherType.REQUEST) { request.getAsyncContext().addListener(new ObservationAsyncListener(observation)); } // scope is opened for ASYNC dispatches, but the observation will be closed // by the async listener. - else if (request.getDispatcherType() != DispatcherType.ASYNC){ + else if (!isAsyncDispatch(request)) { Throwable error = fetchException(request); if (error != null) { observation.error(error); @@ -168,6 +168,7 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter { @Override public void onStartAsync(AsyncEvent event) { + event.getAsyncContext().addListener(this); } @Override diff --git a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java index 1edc589b13..ddbbad37cf 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java @@ -21,11 +21,16 @@ import java.io.IOException; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; +import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncEvent; import jakarta.servlet.AsyncListener; import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -139,6 +144,21 @@ class ServerHttpObservationFilterTests { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); } + @Test + void shouldRegisterListenerForAsyncStarts() throws Exception { + CustomAsyncFilter customAsyncFilter = new CustomAsyncFilter(); + this.mockFilterChain = new MockFilterChain(new NoOpServlet(), customAsyncFilter); + this.request.setAsyncSupported(true); + this.request.setDispatcherType(DispatcherType.REQUEST); + this.filter.doFilter(this.request, this.response, this.mockFilterChain); + customAsyncFilter.asyncContext.dispatch(); + this.request.setDispatcherType(DispatcherType.ASYNC); + AsyncContext newAsyncContext = this.request.startAsync(); + newAsyncContext.complete(); + + assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); + } + @Test void shouldCloseObservationAfterAsyncError() throws Exception { this.request.setAsyncSupported(true); @@ -187,4 +207,26 @@ class ServerHttpObservationFilterTests { } } + @SuppressWarnings("serial") + static class NoOpServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + } + + } + + static class CustomAsyncFilter implements Filter { + + AsyncContext asyncContext; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + this.asyncContext = servletRequest.startAsync(); + filterChain.doFilter(servletRequest, servletResponse); + } + + } + }