Instrument Servlet server apps for Observability

This commit introduces the new `HttpRequestsObservationFilter`
This `Filter` can be used to instrument Servlet-based web frameworks for
Micrometer Observations. While the Servlet request and responses are
automatically used for extracting KeyValues for observations, web
frameworks still need to provide the matching URL pattern, if supported.

This can be done by fetching the observation context from the request
attributes and contributing to it.

This commit instruments Spring MVC (annotation and functional variants),
effectively replacing Spring Boot's `WebMvcMetricsFilter`.

See gh-28880
This commit is contained in:
Brian Clozel
2022-09-12 11:36:37 +02:00
parent ac9360b624
commit 6eded96740
13 changed files with 756 additions and 0 deletions

View File

@@ -26,6 +26,8 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.observation.HttpRequestsObservationContext;
import org.springframework.web.observation.HttpRequestsObservationFilter;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.function.HandlerFunction;
@@ -33,12 +35,14 @@ import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RouterFunctionMapping}.
* @author Arjen Poutsma
* @author Brian Clozel
*/
@@ -170,11 +174,15 @@ class RouterFunctionMappingTests {
assertThat(result).isNotNull();
assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/match");
assertThat(HttpRequestsObservationFilter.findObservationContext(request))
.hasValueSatisfying(context -> assertThat(context.getPathPattern()).isEqualTo("/match"));
assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE)).isEqualTo(handlerFunction);
}
private MockHttpServletRequest createTestRequest(String path) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
request.setAttribute(HttpRequestsObservationFilter.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE,
new HttpRequestsObservationContext(request, new MockHttpServletResponse()));
ServletRequestPathUtils.parseAndCache(request);
return request;
}

View File

@@ -49,12 +49,15 @@ import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.observation.HttpRequestsObservationContext;
import org.springframework.web.observation.HttpRequestsObservationFilter;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.UrlPathHelper;
@@ -288,6 +291,18 @@ class RequestMappingInfoHandlerMappingTests {
assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/{path1}/2");
}
@PathPatternsParameterizedTest
void handleMatchBestMatchingPatternAttributeInObservationContext(TestRequestMappingInfoHandlerMapping mapping) {
RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build();
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2");
HttpRequestsObservationContext observationContext = new HttpRequestsObservationContext(request, new MockHttpServletResponse());
request.setAttribute(HttpRequestsObservationFilter.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observationContext);
mapping.handleMatch(key, "/1/2", request);
assertThat(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)).isEqualTo("/{path1}/2");
assertThat(observationContext.getPathPattern()).isEqualTo("/{path1}/2");
}
@PathPatternsParameterizedTest // gh-22543
void handleMatchBestMatchingPatternAttributeNoPatternsDefined(TestRequestMappingInfoHandlerMapping mapping) {
String path = "";