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:
Brian Clozel
2023-03-14 17:16:56 +01:00
parent e5ee369e70
commit 96a429a561
16 changed files with 419 additions and 31 deletions

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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));
}