From 0f2a6e2528c7f0649b467e4002a27af8e7e8546b Mon Sep 17 00:00:00 2001 From: Hamza Remmal <2861-remmal@users.noreply.gitlab.epfl.ch> Date: Thu, 26 Dec 2024 15:27:32 +0000 Subject: [PATCH] chore: unify the generation of the request id + return it in the response --- autograde-service/pom.xml | 2 +- .../epfl/autograde/config/FiltersConfig.java | 28 ++++++ .../filters/AssignRequestIdFilter.java | 54 +++++++++++ .../autograde/logging/MdcInterceptor.java | 30 ------ .../logging/WebMvcConfiguration.java | 22 ----- .../src/main/resources/logback-spring.xml | 2 +- ...AssignRequestIdFilterIntegrationTests.java | 54 +++++++++++ .../AssignRequestIdFilterUnitTests.java | 95 +++++++++++++++++++ 8 files changed, 233 insertions(+), 54 deletions(-) create mode 100644 autograde-service/src/main/java/ch/epfl/autograde/config/FiltersConfig.java create mode 100644 autograde-service/src/main/java/ch/epfl/autograde/filters/AssignRequestIdFilter.java delete mode 100644 autograde-service/src/main/java/ch/epfl/autograde/logging/MdcInterceptor.java delete mode 100644 autograde-service/src/main/java/ch/epfl/autograde/logging/WebMvcConfiguration.java create mode 100644 autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterIntegrationTests.java create mode 100644 autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterUnitTests.java diff --git a/autograde-service/pom.xml b/autograde-service/pom.xml index df959467..9a871e68 100644 --- a/autograde-service/pom.xml +++ b/autograde-service/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>3.1.1</version> + <version>3.3.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> diff --git a/autograde-service/src/main/java/ch/epfl/autograde/config/FiltersConfig.java b/autograde-service/src/main/java/ch/epfl/autograde/config/FiltersConfig.java new file mode 100644 index 00000000..cb681e20 --- /dev/null +++ b/autograde-service/src/main/java/ch/epfl/autograde/config/FiltersConfig.java @@ -0,0 +1,28 @@ +package ch.epfl.autograde.config; + +import ch.epfl.autograde.filters.AssignRequestIdFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +/** + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.3.0 + */ +@Configuration +public class FiltersConfig { + + @Bean + public FilterRegistrationBean<?> requestIdFilterRegistration(AssignRequestIdFilter filter) { + final var registration = new FilterRegistrationBean<>(); + registration.setFilter(filter); + // We should generate a request id only for api endpoints + registration.addUrlPatterns("/api/v1/*"); + // Ensure that all the requests will have a request id by running the filter first + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + +} diff --git a/autograde-service/src/main/java/ch/epfl/autograde/filters/AssignRequestIdFilter.java b/autograde-service/src/main/java/ch/epfl/autograde/filters/AssignRequestIdFilter.java new file mode 100644 index 00000000..8bead833 --- /dev/null +++ b/autograde-service/src/main/java/ch/epfl/autograde/filters/AssignRequestIdFilter.java @@ -0,0 +1,54 @@ +package ch.epfl.autograde.filters; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.UUID; + +/** + * A filter that assigns a unique Request ID to every incoming HTTP request. + * + * <p> + * This filter generates a unique {@code Request ID} for each HTTP request and ensures it is: + * <ul> + * <li>Added to the request attributes, making it accessible to downstream components.</li> + * <li>Included as a header in the HTTP response ({@code X-Request-Id}).</li> + * <li>Logged in the Mapped Diagnostic Context (MDC) for consistent tracking in logs.</li> + * </ul> + * This filter is designed to run once per request and should be one of the earliest filters + * in the filter chain. + * </p> + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.3.0 + * @see ch.epfl.autograde.config.FiltersConfig#requestIdFilterRegistration(AssignRequestIdFilter) + * @see org.springframework.web.filter.OncePerRequestFilter + */ +@Slf4j +@Component +@RequiredArgsConstructor +public final class AssignRequestIdFilter extends OncePerRequestFilter { + + public static final String REQUEST_ID_HEADER = "X-Request-Id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // Generate a unique Request ID + // TODO: Extract the UUID to be a distributed service to have a real UUID and not an UUID per node + final var requestId = UUID.randomUUID().toString(); + request.setAttribute(REQUEST_ID_HEADER, requestId); + response.setHeader(REQUEST_ID_HEADER, requestId); + MDC.put(REQUEST_ID_HEADER, requestId); + log.trace("Generated Request-Id '{}' for request from '{}'", requestId, request.getRemoteAddr()); + chain.doFilter(request, response); + } + +} diff --git a/autograde-service/src/main/java/ch/epfl/autograde/logging/MdcInterceptor.java b/autograde-service/src/main/java/ch/epfl/autograde/logging/MdcInterceptor.java deleted file mode 100644 index 1d375ae1..00000000 --- a/autograde-service/src/main/java/ch/epfl/autograde/logging/MdcInterceptor.java +++ /dev/null @@ -1,30 +0,0 @@ -package ch.epfl.autograde.logging; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.MDC; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.util.UUID; - -/** - * Interceptor that adds a unique request id to the MDC, - * so that it can be used in the logs to track a request through the system. - * - * @implNote To work with async requests, you need to create a TaskDecorator that copies the MDC to the new thread. - */ -@Component -public class MdcInterceptor implements HandlerInterceptor { - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - MDC.put("requestId", UUID.randomUUID().toString()); - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - MDC.remove("requestId"); - } -} diff --git a/autograde-service/src/main/java/ch/epfl/autograde/logging/WebMvcConfiguration.java b/autograde-service/src/main/java/ch/epfl/autograde/logging/WebMvcConfiguration.java deleted file mode 100644 index 9c3a8a23..00000000 --- a/autograde-service/src/main/java/ch/epfl/autograde/logging/WebMvcConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package ch.epfl.autograde.logging; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Component -public class WebMvcConfiguration implements WebMvcConfigurer { - - private final MdcInterceptor mdcInterceptor; - - @Autowired - public WebMvcConfiguration(MdcInterceptor mdcInterceptor) { - this.mdcInterceptor = mdcInterceptor; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(mdcInterceptor); - } -} diff --git a/autograde-service/src/main/resources/logback-spring.xml b/autograde-service/src/main/resources/logback-spring.xml index 64736462..5a2b05fd 100644 --- a/autograde-service/src/main/resources/logback-spring.xml +++ b/autograde-service/src/main/resources/logback-spring.xml @@ -1,7 +1,7 @@ <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> - <pattern>%d{ISO8601} %X{requestId} [%thread] %-5level %logger{36} - %msg%n</pattern> + <pattern>%d{ISO8601} %X{X-Request-Id} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> diff --git a/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterIntegrationTests.java b/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterIntegrationTests.java new file mode 100644 index 00000000..2e5cb0dd --- /dev/null +++ b/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterIntegrationTests.java @@ -0,0 +1,54 @@ +package ch.epfl.autograde.filters; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.3.0 + * @see ch.epfl.autograde.filters.AssignRequestIdFilter + */ +@AutoConfigureMockMvc +@SpringBootTest +final class AssignRequestIdFilterIntegrationTests { + + @Autowired + private MockMvc mockMvc; + + /** + * @implNote We call a fake-endpoint to test: + * <ul> + * <li>The filter is called when calling an API Endpoint</li> + * <li>The filter is executed before the authentication filters</li> + * <li>The filter is called before dispatching to the controller</li> + * </ul> + */ + @Test @DisplayName("Request to API endpoints should have a 'X-Request-Id' header") + void apiRequestShouldHaveARequestID() throws Exception { + mockMvc.perform(get("/api/v1/fake-endpoint")) + .andExpect(header().exists(AssignRequestIdFilter.REQUEST_ID_HEADER)); + } + + /** + * @implNote We call a fake-endpoint to test: + * <ul> + * <li>The filter is not called when calling a non-API Endpoint</li> + * <li>The filter is executed before the authentication filters</li> + * <li>The filter is called before dispatching to the controller</li> + * </ul> + */ + @Test @DisplayName("Request to non-API endpoints should not have a 'X-Request-Id' header") + void nonApiRequestShouldNotHaveARequestID() throws Exception { + mockMvc.perform(get("/not-api/v1/fake-endpoint")) + .andExpect(header().doesNotExist(AssignRequestIdFilter.REQUEST_ID_HEADER)); + } + +} diff --git a/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterUnitTests.java b/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterUnitTests.java new file mode 100644 index 00000000..c56f2de2 --- /dev/null +++ b/autograde-service/src/test/java/ch/epfl/autograde/filters/AssignRequestIdFilterUnitTests.java @@ -0,0 +1,95 @@ +package ch.epfl.autograde.filters; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.MDC; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.3.0 + * @see ch.epfl.autograde.filters.AssignRequestIdFilter + */ +@DisplayName("AssignRequestIdFilter Unit Tests") +@ExtendWith(MockitoExtension.class) +final class AssignRequestIdFilterUnitTests { + + private final AssignRequestIdFilter filter = new AssignRequestIdFilter(); + + @Test @DisplayName("Filter adds the 'X-Request-Id' attribute to the HttpServletRequest") + void filterSetsTheAttributeInTheRequest( + @Mock HttpServletRequest request, + @Mock HttpServletResponse response, + @Mock FilterChain chain + ) throws Exception { + filter.doFilter(request, response, chain); + verify(request).setAttribute(eq(AssignRequestIdFilter.REQUEST_ID_HEADER), notNull()); + } + + @Test @DisplayName("Filter adds the 'X-Request-Id' header to the HttpServletResponse") + void filterSetsTheHeaderInTheResponse( + @Mock HttpServletRequest request, + @Mock HttpServletResponse response, + @Mock FilterChain chain + ) throws Exception { + filter.doFilter(request, response, chain); + verify(response).setHeader(eq(AssignRequestIdFilter.REQUEST_ID_HEADER), notNull()); + } + + @Test @DisplayName("Filter adds the 'X-Request-Id' to the MDC") + void filterSetsMDCContext( + @Mock HttpServletRequest request, + @Mock HttpServletResponse response, + @Mock FilterChain chain + ) throws Exception { + filter.doFilter(request, response, chain); + assertNotNull(MDC.get(AssignRequestIdFilter.REQUEST_ID_HEADER)); + } + + @Test @DisplayName("Filter calls the rest of the FilterChain") + void filterCallsTheRestOfTheChain( + @Mock HttpServletRequest request, + @Mock HttpServletResponse response, + @Mock FilterChain chain + ) throws Exception { + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } + + @Test + @DisplayName("Filter generates a unique 'X-Request-Id'") + void filterUsesSameRequestIdAcrossAll( + @Mock HttpServletRequest request, + @Mock HttpServletResponse response, + @Mock FilterChain chain, + @Captor ArgumentCaptor<String> requestCaptor, + @Captor ArgumentCaptor<String> responseCaptor + ) throws Exception { + filter.doFilter(request, response, chain); + + verify(request).setAttribute(eq(AssignRequestIdFilter.REQUEST_ID_HEADER), requestCaptor.capture()); + verify(response).setHeader(eq(AssignRequestIdFilter.REQUEST_ID_HEADER), responseCaptor.capture()); + + final var requestAttrId = requestCaptor.getValue(); + final var responseHeaderId = responseCaptor.getValue(); + final var mdcRequestId = MDC.get(AssignRequestIdFilter.REQUEST_ID_HEADER); + + assertAll( + () -> assertNotNull(requestAttrId), + () -> assertNotNull(responseHeaderId), + () -> assertNotNull(mdcRequestId), + () -> assertEquals(requestAttrId, responseHeaderId), + () -> assertEquals(requestAttrId, mdcRequestId) + ); + } + +} -- GitLab