Skip to content

Commit 6681394

Browse files
committed
Stop observations for async requests in Servlet filter
Prior to this commit, the `ServerHttpObservationFilter` would support async dispatches and would do the following: 1. start the observation 2. call the filter chain 3. if async has started, do nothing 4. if not in async mode, stop the observation This behavior would effectively rely on Async implementations to complete and dispatch the request back to the container for an async dispatch. This is what Spring web frameworks do and guarantee. Some implementations complete the async request but do not dispatch back; as a result, observations could leak as they are never stopped. This commit changes the support of async requests. The filter now opts-out of async dispatches - the filter will not be called for those anymore. Instead, if the application started async mode during the initial container dispatch, the filter will register an AsyncListener to be notified of the outcome of the async handling. Fixes gh-32730
1 parent 172987c commit 6681394

File tree

2 files changed

+58
-11
lines changed

2 files changed

+58
-11
lines changed

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

+42-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,9 +21,12 @@
2121

2222
import io.micrometer.observation.Observation;
2323
import io.micrometer.observation.ObservationRegistry;
24+
import jakarta.servlet.AsyncEvent;
25+
import jakarta.servlet.AsyncListener;
2426
import jakarta.servlet.FilterChain;
2527
import jakarta.servlet.RequestDispatcher;
2628
import jakarta.servlet.ServletException;
29+
import jakarta.servlet.ServletRequest;
2730
import jakarta.servlet.http.HttpServletRequest;
2831
import jakarta.servlet.http.HttpServletResponse;
2932

@@ -94,11 +97,6 @@ public static Optional<ServerRequestObservationContext> findObservationContext(H
9497
return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
9598
}
9699

97-
@Override
98-
protected boolean shouldNotFilterAsyncDispatch() {
99-
return false;
100-
}
101-
102100
@Override
103101
@SuppressWarnings("try")
104102
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -114,8 +112,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
114112
throw ex;
115113
}
116114
finally {
117-
// Only stop Observation if async processing is done or has never been started.
118-
if (!request.isAsyncStarted()) {
115+
// If async is started, register a listener for completion notification.
116+
if (request.isAsyncStarted()) {
117+
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
118+
}
119+
// Stop Observation right now if async processing has not been started.
120+
else {
119121
Throwable error = fetchException(request);
120122
if (error != null) {
121123
observation.error(error);
@@ -140,13 +142,43 @@ private Observation createOrFetchObservation(HttpServletRequest request, HttpSer
140142
}
141143

142144
@Nullable
143-
private Throwable unwrapServletException(Throwable ex) {
145+
static Throwable unwrapServletException(Throwable ex) {
144146
return (ex instanceof ServletException) ? ex.getCause() : ex;
145147
}
146148

147149
@Nullable
148-
private Throwable fetchException(HttpServletRequest request) {
150+
static Throwable fetchException(ServletRequest request) {
149151
return (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
150152
}
151153

154+
private static class ObservationAsyncListener implements AsyncListener {
155+
156+
private final Observation currentObservation;
157+
158+
public ObservationAsyncListener(Observation currentObservation) {
159+
this.currentObservation = currentObservation;
160+
}
161+
162+
@Override
163+
public void onStartAsync(AsyncEvent event) {
164+
}
165+
166+
@Override
167+
public void onTimeout(AsyncEvent event) {
168+
this.currentObservation.stop();
169+
}
170+
171+
@Override
172+
public void onComplete(AsyncEvent event) {
173+
this.currentObservation.stop();
174+
}
175+
176+
@Override
177+
public void onError(AsyncEvent event) {
178+
this.currentObservation.error(unwrapServletException(event.getThrowable()));
179+
this.currentObservation.stop();
180+
}
181+
182+
}
183+
152184
}

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class ServerHttpObservationFilterTests {
5050
private final MockHttpServletResponse response = new MockHttpServletResponse();
5151

5252

53+
@Test
54+
void filterShouldNotProcessAsyncDispatch() {
55+
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
56+
}
57+
5358
@Test
5459
void filterShouldFillObservationContext() throws Exception {
5560
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
@@ -60,7 +65,7 @@ void filterShouldFillObservationContext() throws Exception {
6065
assertThat(context.getCarrier()).isEqualTo(this.request);
6166
assertThat(context.getResponse()).isEqualTo(this.response);
6267
assertThat(context.getPathPattern()).isNull();
63-
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
68+
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
6469
}
6570

6671
@Test
@@ -109,6 +114,16 @@ void filterShouldSetDefaultErrorStatusForBubblingExceptions() {
109114
.hasLowCardinalityKeyValue("status", "500");
110115
}
111116

117+
@Test
118+
void shouldCloseObservationAfterAsyncCompletion() throws Exception {
119+
this.request.setAsyncSupported(true);
120+
this.request.startAsync();
121+
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
122+
this.request.getAsyncContext().complete();
123+
124+
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
125+
}
126+
112127
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
113128
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
114129
.hasObservationWithNameEqualTo("http.server.requests").that();

0 commit comments

Comments
 (0)