Move reactive server instrumentation out of WebFilter
Prior to this commit, the Observation instrumentation for Reactive server applications was implemented with a `WebFilter`. This allowed to record observations and set up a tracing context for the controller handlers. The limitation of this approach is that all processing happening at a lower level is not aware of any observation. Here, the `HttpWebHandlerAdapter` handles several interesting aspects: * logging of HTTP requests and responses at the TRACE level * logging of client disconnect errors * handling of unresolved errors With the current instrumentation, these logging statements will miss the tracing context information. As a result, this commit deprecates the `ServerHttpObservationFilter` in favor of a more direct instrumentation of the `HttpWebHandlerAdapter`. This enables a more precise instrumentattion and allows to set up the current observation earlier in the reactor context: log statements will now contain the relevant information. Fixes gh-30013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class ServerHttpObservationFilterTests {
|
||||
|
||||
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.server.adapter;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebExceptionHandler;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Observability-related tests for {@link HttpWebHandlerAdapter}.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class HttpWebHandlerAdapterObservabilityTests {
|
||||
|
||||
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
private final MockServerHttpRequest request = MockServerHttpRequest.post("/test/resource").build();
|
||||
|
||||
private final MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
|
||||
@Test
|
||||
void handlerShouldSetObservationContextOnExchange() {
|
||||
HttpStatusSuccessStubWebHandler targetHandler = new HttpStatusSuccessStubWebHandler(HttpStatus.OK);
|
||||
createWebHandler(targetHandler).handle(this.request, this.response).block();
|
||||
assertThat(targetHandler.observationContext).isPresent();
|
||||
assertThat(targetHandler.observationContext.get().getCarrier()).isEqualTo(this.request);
|
||||
assertThat(targetHandler.observationContext.get().getResponse()).isEqualTo(this.response);
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerShouldSetCurrentObservationInReactorContext() {
|
||||
ReactorContextWebHandler targetHandler = new ReactorContextWebHandler();
|
||||
createWebHandler(targetHandler).handle(this.request, this.response).block();
|
||||
assertThat(targetHandler.contextView.getOrEmpty(ObservationThreadLocalAccessor.KEY)).isPresent();
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerShouldSeeHandledException() {
|
||||
ThrowingExceptionWebHandler throwingHandler = new ThrowingExceptionWebHandler(new IllegalStateException("testing error"));
|
||||
ExceptionHandlingWebHandler targetHandler = new ExceptionHandlingWebHandler(throwingHandler, List.of(new BadRequestExceptionHandler()));
|
||||
createWebHandler(targetHandler).handle(this.request, this.response).block();
|
||||
assertThat(throwingHandler.observationContext).isPresent();
|
||||
assertThat(throwingHandler.observationContext.get().getError()).isInstanceOf(IllegalStateException.class);
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "CLIENT_ERROR");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerShouldRecordObservationWhenCancelled() {
|
||||
HttpStatusSuccessStubWebHandler targetHandler = new HttpStatusSuccessStubWebHandler(HttpStatus.OK);
|
||||
StepVerifier.create(createWebHandler(targetHandler).handle(this.request, this.response)).thenCancel().verify();
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN");
|
||||
}
|
||||
|
||||
private HttpWebHandlerAdapter createWebHandler(WebHandler targetHandler) {
|
||||
HttpWebHandlerAdapter handlerAdapter = new HttpWebHandlerAdapter(targetHandler);
|
||||
handlerAdapter.setObservationRegistry(this.observationRegistry);
|
||||
return handlerAdapter;
|
||||
}
|
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
|
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("http.server.requests").that();
|
||||
}
|
||||
|
||||
private static class HttpStatusSuccessStubWebHandler implements WebHandler {
|
||||
|
||||
private final HttpStatus responseStatus;
|
||||
|
||||
private Optional<ServerRequestObservationContext> observationContext;
|
||||
|
||||
public HttpStatusSuccessStubWebHandler(HttpStatus responseStatus) {
|
||||
this.responseStatus = responseStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
this.observationContext = ServerRequestObservationContext.findCurrent(exchange);
|
||||
exchange.getResponse().setStatusCode(this.responseStatus);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactorContextWebHandler implements WebHandler {
|
||||
|
||||
ContextView contextView;
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.OK);
|
||||
return Mono.deferContextual(contextView -> {
|
||||
this.contextView = contextView;
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThrowingExceptionWebHandler implements WebHandler {
|
||||
|
||||
private final Throwable exception;
|
||||
|
||||
private Optional<ServerRequestObservationContext> observationContext;
|
||||
|
||||
private ThrowingExceptionWebHandler(Throwable exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
this.observationContext = ServerRequestObservationContext.findCurrent(exchange);
|
||||
return Mono.error(this.exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BadRequestExceptionHandler implements WebExceptionHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
return exchange.getResponse().setComplete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,13 +45,13 @@ public class ExceptionHandlingWebHandlerTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void handleErrorSignal() throws Exception {
|
||||
void handleErrorSignal() {
|
||||
createWebHandler(new BadRequestExceptionHandler()).handle(this.exchange).block();
|
||||
assertThat(this.exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleErrorSignalWithMultipleHttpErrorHandlers() throws Exception {
|
||||
void handleErrorSignalWithMultipleHttpErrorHandlers() {
|
||||
createWebHandler(
|
||||
new UnresolvedExceptionHandler(),
|
||||
new UnresolvedExceptionHandler(),
|
||||
@@ -62,17 +62,14 @@ public class ExceptionHandlingWebHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedException() throws Exception {
|
||||
void unresolvedException() {
|
||||
Mono<Void> mono = createWebHandler(new UnresolvedExceptionHandler()).handle(this.exchange);
|
||||
StepVerifier.create(mono).expectErrorMessage("boo").verify();
|
||||
assertThat(this.exchange.getResponse().getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedExceptionWithWebHttpHandlerAdapter() throws Exception {
|
||||
|
||||
// HttpWebHandlerAdapter handles unresolved errors
|
||||
|
||||
void unresolvedExceptionWithWebHttpHandlerAdapter() {
|
||||
new HttpWebHandlerAdapter(createWebHandler(new UnresolvedExceptionHandler()))
|
||||
.handle(this.exchange.getRequest(), this.exchange.getResponse()).block();
|
||||
|
||||
@@ -80,11 +77,18 @@ public class ExceptionHandlingWebHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thrownExceptionBecomesErrorSignal() throws Exception {
|
||||
void thrownExceptionBecomesErrorSignal() {
|
||||
createWebHandler(new BadRequestExceptionHandler()).handle(this.exchange).block();
|
||||
assertThat(this.exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void thrownExceptionIsStoredAsExchangeAttribute() {
|
||||
createWebHandler(new BadRequestExceptionHandler()).handle(this.exchange).block();
|
||||
Exception exceptionAttribute = this.exchange.getAttribute(ExceptionHandlingWebHandler.HANDLED_WEB_EXCEPTION);
|
||||
assertThat(exceptionAttribute).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
private WebHandler createWebHandler(WebExceptionHandler... handlers) {
|
||||
return new ExceptionHandlingWebHandler(this.targetHandler, Arrays.asList(handlers));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user