Skip to content

Commit ab236c7

Browse files
committed
Re-enable async dispatches in Observation Filter
Prior to this commit, the fix for gh-32730 disabled the involvment of the osbervation filter for async dispatches. Instead of relying on ASYNC dispatches to close the observation for async requests, this is now using an async listener instead: async dispatches are not guaranteed to happen once the async request is handled. This change caused another side-effect: because async dispatches are not considered anymore by this filter, the observation scope is not reinstated for async dispatches. For example, `ResponseBodyAdvice` implementations do not have the observation scope opened during their execution. This commit re-enables async dispatches for this filter, but ensures that observations are not closed during such dispatches as this will be done by the async listener. Fixes gh-33091
1 parent 61adf2d commit ab236c7

File tree

2 files changed

+74
-10
lines changed

2 files changed

+74
-10
lines changed

Diff for: spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.micrometer.observation.ObservationRegistry;
2424
import jakarta.servlet.AsyncEvent;
2525
import jakarta.servlet.AsyncListener;
26+
import jakarta.servlet.DispatcherType;
2627
import jakarta.servlet.FilterChain;
2728
import jakarta.servlet.RequestDispatcher;
2829
import jakarta.servlet.ServletException;
@@ -97,6 +98,11 @@ public static Optional<ServerRequestObservationContext> findObservationContext(H
9798
return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
9899
}
99100

101+
@Override
102+
protected boolean shouldNotFilterAsyncDispatch() {
103+
return false;
104+
}
105+
100106
@Override
101107
@SuppressWarnings("try")
102108
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -116,8 +122,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
116122
if (request.isAsyncStarted()) {
117123
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
118124
}
119-
// Stop Observation right now if async processing has not been started.
120-
else {
125+
// scope is opened for ASYNC dispatches, but the observation will be closed
126+
// by the async listener.
127+
else if (request.getDispatcherType() != DispatcherType.ASYNC){
121128
Throwable error = fetchException(request);
122129
if (error != null) {
123130
observation.error(error);
@@ -176,7 +183,6 @@ public void onComplete(AsyncEvent event) {
176183
@Override
177184
public void onError(AsyncEvent event) {
178185
this.currentObservation.error(unwrapServletException(event.getThrowable()));
179-
this.currentObservation.stop();
180186
}
181187

182188
}

Diff for: spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java

+65-7
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@
1616

1717
package org.springframework.web.filter;
1818

19+
import java.io.IOException;
20+
1921
import io.micrometer.observation.ObservationRegistry;
2022
import io.micrometer.observation.tck.TestObservationRegistry;
2123
import io.micrometer.observation.tck.TestObservationRegistryAssert;
24+
import jakarta.servlet.AsyncEvent;
25+
import jakarta.servlet.AsyncListener;
26+
import jakarta.servlet.DispatcherType;
2227
import jakarta.servlet.RequestDispatcher;
2328
import jakarta.servlet.ServletException;
29+
import jakarta.servlet.http.HttpServlet;
30+
import jakarta.servlet.http.HttpServletRequest;
31+
import jakarta.servlet.http.HttpServletResponse;
2432
import org.junit.jupiter.api.Test;
2533

2634
import org.springframework.http.HttpMethod;
2735
import org.springframework.http.server.observation.ServerRequestObservationContext;
36+
import org.springframework.web.testfixture.servlet.MockAsyncContext;
2837
import org.springframework.web.testfixture.servlet.MockFilterChain;
2938
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
3039
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
@@ -41,18 +50,18 @@ class ServerHttpObservationFilterTests {
4150

4251
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
4352

44-
private final ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry);
45-
46-
private final MockFilterChain mockFilterChain = new MockFilterChain();
47-
4853
private final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/resource/test");
4954

5055
private final MockHttpServletResponse response = new MockHttpServletResponse();
5156

57+
private MockFilterChain mockFilterChain = new MockFilterChain();
58+
59+
private ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry);
60+
5261

5362
@Test
54-
void filterShouldNotProcessAsyncDispatch() {
55-
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
63+
void filterShouldProcessAsyncDispatch() {
64+
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isFalse();
5665
}
5766

5867
@Test
@@ -68,6 +77,12 @@ void filterShouldFillObservationContext() throws Exception {
6877
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
6978
}
7079

80+
@Test
81+
void filterShouldOpenScope() throws Exception {
82+
this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry));
83+
filter.doFilter(this.request, this.response, this.mockFilterChain);
84+
}
85+
7186
@Test
7287
void filterShouldAcceptNoOpObservationContext() throws Exception {
7388
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(ObservationRegistry.NOOP);
@@ -124,9 +139,52 @@ void shouldCloseObservationAfterAsyncCompletion() throws Exception {
124139
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
125140
}
126141

142+
@Test
143+
void shouldCloseObservationAfterAsyncError() throws Exception {
144+
this.request.setAsyncSupported(true);
145+
this.request.startAsync();
146+
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
147+
MockAsyncContext asyncContext = (MockAsyncContext) this.request.getAsyncContext();
148+
for (AsyncListener listener : asyncContext.getListeners()) {
149+
listener.onError(new AsyncEvent(this.request.getAsyncContext(), new IllegalStateException("test error")));
150+
}
151+
asyncContext.complete();
152+
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException").hasBeenStopped();
153+
}
154+
155+
@Test
156+
void shouldNotCloseObservationDuringAsyncDispatch() throws Exception {
157+
this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry));
158+
this.request.setDispatcherType(DispatcherType.ASYNC);
159+
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
160+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
161+
.hasObservationWithNameEqualTo("http.server.requests")
162+
.that().isNotStopped();
163+
}
164+
127165
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
166+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
167+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1);
168+
128169
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
129-
.hasObservationWithNameEqualTo("http.server.requests").that();
170+
.hasObservationWithNameEqualTo("http.server.requests")
171+
.that()
172+
.hasBeenStopped();
173+
}
174+
175+
@SuppressWarnings("serial")
176+
static class ScopeCheckingServlet extends HttpServlet {
177+
178+
private final ObservationRegistry observationRegistry;
179+
180+
public ScopeCheckingServlet(ObservationRegistry observationRegistry) {
181+
this.observationRegistry = observationRegistry;
182+
}
183+
184+
@Override
185+
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
186+
assertThat(this.observationRegistry.getCurrentObservation()).isNotNull();
187+
}
130188
}
131189

132190
}

0 commit comments

Comments
 (0)