Skip to content

Commit 76604db

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-32986
1 parent 38b7209 commit 76604db

File tree

2 files changed

+59
-11
lines changed

2 files changed

+59
-11
lines changed

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

+43-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);
@@ -139,13 +141,44 @@ private Observation createOrFetchObservation(HttpServletRequest request, HttpSer
139141
return observation;
140142
}
141143

142-
private Throwable unwrapServletException(Throwable ex) {
144+
@Nullable
145+
static Throwable unwrapServletException(Throwable ex) {
143146
return (ex instanceof ServletException) ? ex.getCause() : ex;
144147
}
145148

146149
@Nullable
147-
private Throwable fetchException(HttpServletRequest request) {
150+
static Throwable fetchException(ServletRequest request) {
148151
return (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
149152
}
150153

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+
151184
}

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
@@ -111,6 +116,16 @@ void filterShouldSetDefaultErrorStatusForBubblingExceptions() {
111116
.hasLowCardinalityKeyValue("status", "500");
112117
}
113118

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

0 commit comments

Comments
 (0)