diff --git a/build.gradle.kts b/build.gradle.kts index d6f5767..8fa9c4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,16 @@ tasks.withType { finalizedBy(tasks.jacocoTestReport) } +val mockitoAgent = configurations.create("mockitoAgent") +dependencies { + mockitoAgent("org.mockito:mockito-core") { isTransitive = false } +} +tasks { + test { + jvmArgs("-javaagent:${mockitoAgent.asPath}") + } +} + tasks.jacocoTestReport { dependsOn(tasks.test) reports { diff --git a/src/main/java/it/gov/pagopa/template/config/RestTemplateConfig.java b/src/main/java/it/gov/pagopa/template/config/RestTemplateConfig.java new file mode 100644 index 0000000..56db112 --- /dev/null +++ b/src/main/java/it/gov/pagopa/template/config/RestTemplateConfig.java @@ -0,0 +1,67 @@ +package it.gov.pagopa.template.config; + +import it.gov.pagopa.template.performancelogger.RestInvokePerformanceLogger; +import jakarta.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.ResponseErrorHandler; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; + +@Slf4j +@Configuration(proxyBeanMethods = false) +public class RestTemplateConfig { + private final int connectTimeoutMillis; + private final int readTimeoutHandlerMillis; + + public RestTemplateConfig( + @Value("${rest.default-timeout.connect-millis}") int connectTimeoutMillis, + @Value("${rest.default-timeout.read-millis}") int readTimeoutHandlerMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutHandlerMillis = readTimeoutHandlerMillis; + } + + @Bean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()) + .additionalInterceptors(new RestInvokePerformanceLogger()) + .connectTimeout(Duration.ofMillis(connectTimeoutMillis)) + .readTimeout(Duration.ofMillis(readTimeoutHandlerMillis)); + } + + public static ResponseErrorHandler bodyPrinterWhenError(String applicationName) { + final Logger errorBodyLogger = LoggerFactory.getLogger("REST_INVOKE." + applicationName); + return new DefaultResponseErrorHandler() { + @Override + protected void handleError(@Nonnull ClientHttpResponse response, @Nonnull HttpStatusCode statusCode, + @Nullable URI url, @Nullable HttpMethod method) throws IOException { + try { + super.handleError(response, statusCode, url, method); + } catch (HttpStatusCodeException ex) { + errorBodyLogger.info("{} {} Returned status {} and resulted on exception {} - {}: {}", + method, + url, + ex.getStatusCode(), + ex.getClass().getSimpleName(), + ex.getMessage(), + ex.getResponseBodyAsString()); + throw ex; + } + } + }; + } +} diff --git a/src/main/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLogger.java b/src/main/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLogger.java new file mode 100644 index 0000000..5074f68 --- /dev/null +++ b/src/main/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLogger.java @@ -0,0 +1,55 @@ +package it.gov.pagopa.template.performancelogger; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +/** + * It will execute {@link PerformanceLogger} on each Api request + */ +@Service +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ApiRequestPerformanceLogger implements Filter { + + private static final List blackListPathPrefixList = List.of( + "/actuator", + "/favicon.ico", + "/swagger" + ); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException { + if (servletRequest instanceof HttpServletRequest httpServletRequest && + servletResponse instanceof HttpServletResponse httpServletResponse && + isPerformanceLoggedRequest(httpServletRequest) + ) { + PerformanceLogger.execute( + "API_REQUEST", + getRequestDetails(httpServletRequest), + () -> { + filterChain.doFilter(servletRequest, servletResponse); + return "ok"; + }, + x -> "HttpStatus: " + httpServletResponse.getStatus(), + null); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } + + private boolean isPerformanceLoggedRequest(HttpServletRequest httpServletRequest) { + String requestURI = httpServletRequest.getRequestURI(); + return blackListPathPrefixList.stream() + .noneMatch(requestURI::startsWith); + } + + static String getRequestDetails(HttpServletRequest request) { + return "%s %s".formatted(request.getMethod(), request.getRequestURI()); + } +} diff --git a/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLogger.java b/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLogger.java new file mode 100644 index 0000000..f136115 --- /dev/null +++ b/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLogger.java @@ -0,0 +1,89 @@ +package it.gov.pagopa.template.performancelogger; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.springframework.util.function.ThrowingFunction; +import org.springframework.util.function.ThrowingSupplier; + +import java.util.function.Function; + +/** + * Utility class to produce performance log + */ +@Slf4j +public final class PerformanceLogger { + private PerformanceLogger() { + } + + private static final PerformanceLoggerThresholdLevels defaultThresholdLevels = new PerformanceLoggerThresholdLevels(60, 300); + + /** + * It will execute the provided logic printing the timing required to execute it. + * + * @param appenderName The name of the appender which could be used to set logging level + * @param contextData A string printed together with the performance log in order to identify it + * @param logic The logic to execute and take its time + * @param payloadBuilder An optional function which till take the output of the invoked logic in order to print a payload after the performance log + * @param thresholdLevels An optional object to configure the log level based on the logic duration (if not provided, it will use {@link #defaultThresholdLevels} + * @return The object returned by the monitored logic + */ + public static T execute(String appenderName, String contextData, ThrowingSupplier logic, ThrowingFunction payloadBuilder, PerformanceLoggerThresholdLevels thresholdLevels) { + long startTime = System.currentTimeMillis(); + String payload = ""; + try { + T out = logic.get(); + payload = buildPayload(out, payloadBuilder); + return out; + } catch (Exception e) { + payload = "Exception %s: %s".formatted(e.getClass(), e.getMessage()); + throw e; + } finally { + log(appenderName, contextData, startTime, payload, thresholdLevels); + } + } + + private static String buildPayload(T out, Function payloadBuilder) { + String payload; + if (payloadBuilder != null) { + if (out != null) { + try { + payload = payloadBuilder.apply(out); + } catch (Exception e) { + log.warn("Something went wrong while building payload", e); + payload = "Payload builder thrown Exception %s: %s".formatted(e.getClass(), e.getMessage()); + } + } else { + payload = "Returned null"; + } + } else { + payload = ""; + } + return payload; + } + + public static void log(String appenderName, String contextData, long startTime, String payload, PerformanceLoggerThresholdLevels thresholdLevels) { + long durationMillis = System.currentTimeMillis() - startTime; + Level level = resolveLevel(durationMillis, thresholdLevels); + LoggerFactory.getLogger("PERFORMANCE_LOG." + appenderName) + .atLevel(level) + .log( + "{}Time occurred to perform business logic: {} ms. {}", + contextData != null ? "[" + contextData + "] " : "", + durationMillis, + payload); + } + + static Level resolveLevel(long durationMillis, PerformanceLoggerThresholdLevels thresholdLevels) { + long durationSeconds = durationMillis / 1000; + thresholdLevels = ObjectUtils.firstNonNull(thresholdLevels, defaultThresholdLevels); + if (durationSeconds < thresholdLevels.getWarn()) { + return Level.INFO; + } else if (durationSeconds < thresholdLevels.getError()) { + return Level.WARN; + } else { + return Level.ERROR; + } + } +} diff --git a/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerThresholdLevels.java b/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerThresholdLevels.java new file mode 100644 index 0000000..dec7b5b --- /dev/null +++ b/src/main/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerThresholdLevels.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.template.performancelogger; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PerformanceLoggerThresholdLevels { + /** Number of seconds from which the log will be printed as a WARN */ + private long warn; + /** Number of seconds from which the log will be printed as an ERROR */ + private long error; +} diff --git a/src/main/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLogger.java b/src/main/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLogger.java new file mode 100644 index 0000000..4e52371 --- /dev/null +++ b/src/main/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLogger.java @@ -0,0 +1,28 @@ +package it.gov.pagopa.template.performancelogger; + +import jakarta.annotation.Nonnull; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +/** + * It will execute {@link PerformanceLogger} on each RestTemplate invocation + */ +public class RestInvokePerformanceLogger implements ClientHttpRequestInterceptor { + + @Override + @Nonnull + public ClientHttpResponse intercept(@Nonnull HttpRequest request, @Nonnull byte[] body, @Nonnull ClientHttpRequestExecution execution) { + return PerformanceLogger.execute( + "REST_INVOKE", + getRequestDetails(request), + () -> execution.execute(request, body), + x -> "HttpStatus: " + x.getStatusCode().value(), + null); + } + + static String getRequestDetails(HttpRequest request) { + return "%s %s".formatted(request.getMethod(), request.getURI()); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1fbd47f..247bb4e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,42 +2,42 @@ server: http: "${SERVER_PORT:8080}" spring: - application: - name: ${artifactId} - version: ${version} - jmx.enabled: true - threads: - virtual: - enabled: true + application: + name: ${artifactId} + version: ${version} + jmx.enabled: true + threads: + virtual: + enabled: true management: - endpoints: - web: - exposure: - include: health, info, prometheus, metrics - base-path: /actuator - endpoint: - health: - probes.enabled: true - logging.slow-indicator-threshold: "\${HEALTH_ACTUATOR_LOGGER_TIMEOUT_DURATION:PT1S}" - group: - readiness.include: "*" - liveness.include: livenessState,diskSpace,ping + endpoints: + web: + exposure: + include: health, info, prometheus, metrics + base-path: /actuator + endpoint: + health: + probes.enabled: true + logging.slow-indicator-threshold: "\${HEALTH_ACTUATOR_LOGGER_TIMEOUT_DURATION:PT1S}" + group: + readiness.include: "*" + liveness.include: livenessState,diskSpace,ping + prometheus: + enabled: true + metrics: + tags: + application: ${artifactId} + export: prometheus: enabled: true - metrics: - tags: - application: ${artifactId} - export: - prometheus: - enabled: true - step: 1m - descriptions: true - enable: - jvm: true - process: true - system: true - http: true - logback: true + step: 1m + descriptions: true + enable: + jvm: true + process: true + system: true + http: true + logback: true logging: level: @@ -45,3 +45,13 @@ logging: it.gov.pagopa: "\${LOG_LEVEL_PAGOPA:INFO}" org.springframework: "\${LOG_LEVEL_SPRING:INFO}" org.springframework.boot.availability: "\${LOG_LEVEL_SPRING_BOOT_AVAILABILITY:DEBUG}" + it.gov.pagopa.template.exception.ControllerExceptionHandler: "\${LOGGING_LEVEL_API_REQUEST_EXCEPTION:INFO}" + org.springdoc.core.utils.SpringDocAnnotationsUtils: "\${LOG_LEVEL_SPRING_DOC:ERROR}" + PERFORMANCE_LOG: "\${LOG_LEVEL_PERFORMANCE_LOG:INFO}" + PERFORMANCE_LOG.API_REQUEST: "\${LOG_LEVEL_PERFORMANCE_LOG_API_REQUEST:\${logging.level.PERFORMANCE_LOG}}" + PERFORMANCE_LOG.REST_INVOKE: "\${LOG_LEVEL_PERFORMANCE_LOG_REST_INVOKE:\${logging.level.PERFORMANCE_LOG}}" + +rest: + default-timeout: + connect-millis: "\${DEFAULT_REST_CONNECT_TIMEOUT_MILLIS:120000}" + read-millis: "\${DEFAULT_REST_READ_TIMEOUT_MILLIS:120000}" diff --git a/src/test/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLoggerTest.java b/src/test/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLoggerTest.java new file mode 100644 index 0000000..1466cd1 --- /dev/null +++ b/src/test/java/it/gov/pagopa/template/performancelogger/ApiRequestPerformanceLoggerTest.java @@ -0,0 +1,122 @@ +package it.gov.pagopa.template.performancelogger; + +import it.gov.pagopa.template.utils.MemoryAppender; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +@ExtendWith(MockitoExtension.class) +class ApiRequestPerformanceLoggerTest { + public static final String APPENDER_NAME = "API_REQUEST"; + + private ServletRequest httpServletRequestMock; + private ServletResponse httpServletResponseMock; + @Mock + private FilterChain filterChainMock; + + private MemoryAppender memoryAppender; + + private ApiRequestPerformanceLogger filter; + + @BeforeEach + void init() { + httpServletRequestMock = Mockito.mock(HttpServletRequest.class); + httpServletResponseMock = Mockito.mock(HttpServletResponse.class); + filter = new ApiRequestPerformanceLogger(); + } + + @BeforeEach + public void setupMemoryAppender() { + this.memoryAppender = PerformanceLoggerTest.buildPerformanceLoggerMemoryAppender(APPENDER_NAME); + } + + @AfterEach + void verifyNoMoreInteractions() throws ServletException, IOException { + Mockito.verify(filterChainMock) + .doFilter(httpServletRequestMock, httpServletResponseMock); + + Mockito.verifyNoMoreInteractions( + httpServletRequestMock, + httpServletResponseMock, + filterChainMock + ); + } + + @Test + void givenNotHttpServletRequestWhenDoFilterThenDontPerformanceLog() throws ServletException, IOException { + // Given + httpServletRequestMock = Mockito.mock(ServletRequest.class); + + // When + filter.doFilter(httpServletRequestMock, httpServletResponseMock, filterChainMock); + + // Then + Assertions.assertEquals(0, memoryAppender.getLoggedEvents().size()); + } + + @Test + void givenNotHttpServletResponseWhenDoFilterThenDontPerformanceLog() throws ServletException, IOException { + // Given + httpServletResponseMock = Mockito.mock(ServletResponse.class); + + // When + filter.doFilter(httpServletRequestMock, httpServletResponseMock, filterChainMock); + + // Then + Assertions.assertEquals(0, memoryAppender.getLoggedEvents().size()); + } + + @Test + void givenNotCoveredPathWhenDoFilterThenDontPerformanceLog() throws ServletException, IOException { + // Given + configureRequestPath("/actuator"); + + // When + filter.doFilter(httpServletRequestMock, httpServletResponseMock, filterChainMock); + + // Then + Assertions.assertEquals(0, memoryAppender.getLoggedEvents().size()); + } + + @Test + void givenCoveredPathWhenDoFilterThenDontPerformanceLog() throws ServletException, IOException { + // Given + configureRequestPath("/api/test"); + + // When + filter.doFilter(httpServletRequestMock, httpServletResponseMock, filterChainMock); + + // Then + PerformanceLoggerTest.assertPerformanceLogMessage(APPENDER_NAME, "GET /api/test", "HttpStatus: 200", memoryAppender); + + Mockito.verify(((HttpServletRequest)httpServletRequestMock), Mockito.times(2)) + .getRequestURI(); + Mockito.verify(((HttpServletRequest)httpServletRequestMock), Mockito.times(1)) + .getMethod(); + Mockito.verify(((HttpServletResponse)httpServletResponseMock)) + .getStatus(); + } + + private void configureRequestPath(String path) { + Mockito.when(((HttpServletRequest)httpServletRequestMock).getRequestURI()) + .thenReturn(path); + Mockito.lenient().when(((HttpServletRequest) httpServletRequestMock).getMethod()) + .thenReturn("GET"); + Mockito.lenient().when(((HttpServletResponse)httpServletResponseMock).getStatus()) + .thenReturn(200); + } + +} diff --git a/src/test/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerTest.java b/src/test/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerTest.java new file mode 100644 index 0000000..4d1184a --- /dev/null +++ b/src/test/java/it/gov/pagopa/template/performancelogger/PerformanceLoggerTest.java @@ -0,0 +1,121 @@ +package it.gov.pagopa.template.performancelogger; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import it.gov.pagopa.template.utils.MemoryAppender; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +public class PerformanceLoggerTest { + public static final String APPENDER_NAME = "APPENDER"; + public static final String CONTEXT_DATA = "TEST"; + + private MemoryAppender memoryAppender; + + @BeforeEach + public void setupMemoryAppender() { + this.memoryAppender = buildPerformanceLoggerMemoryAppender(APPENDER_NAME); + } + + static MemoryAppender buildPerformanceLoggerMemoryAppender(String appender) { + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("PERFORMANCE_LOG."+appender); + MemoryAppender memoryAppender = new MemoryAppender(); + memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + logger.setLevel(ch.qos.logback.classic.Level.INFO); + logger.addAppender(memoryAppender); + memoryAppender.start(); + return memoryAppender; + } + + @Test + void givenExceptionInBusinessLogicWhenThenLogExceptionInfo(){ + // Given + RuntimeException expectedException = new RuntimeException("DUMMY"); + + // When + RuntimeException result = Assertions.assertThrows(RuntimeException.class, () -> PerformanceLogger.execute(APPENDER_NAME, CONTEXT_DATA, () -> { + throw expectedException; + }, null, null)); + + //Then + Assertions.assertSame(expectedException, result); + assertPerformanceLogMessage(APPENDER_NAME, CONTEXT_DATA, "Exception class java.lang.RuntimeException: DUMMY", memoryAppender); + } + + @Test + void givenExceptionInPayloadBuilderWhenThenLogExceptionInfo(){ + // Given + Object expectedResult = new Object(); + + // When + Object result = PerformanceLogger.execute(APPENDER_NAME, CONTEXT_DATA, () -> expectedResult, x -> { + throw new RuntimeException("PayloadBuilder is receiving the expected parameter? " + (x == expectedResult)); + }, null); + + //Then + Assertions.assertSame(expectedResult, result); + assertPerformanceLogMessage(APPENDER_NAME, CONTEXT_DATA, "Payload builder thrown Exception class java.lang.RuntimeException: PayloadBuilder is receiving the expected parameter? true", memoryAppender); + } + + @Test + void givenNoPayloadBuilderWhenThenLogExceptionInfo(){ + // Given + Object expectedResult = new Object(); + + // When + Object result = PerformanceLogger.execute(APPENDER_NAME, CONTEXT_DATA, () -> expectedResult, null, null); + + //Then + Assertions.assertSame(expectedResult, result); + assertPerformanceLogMessage(APPENDER_NAME, CONTEXT_DATA, "", memoryAppender); + } + + @Test + void givenPayloadBuilderReturningNullWhenThenLogExceptionInfo(){ + // Given + Object expectedResult = new Object(); + + // When + Object result = PerformanceLogger.execute(APPENDER_NAME, CONTEXT_DATA, () -> expectedResult, x -> null, null); + + //Then + Assertions.assertSame(expectedResult, result); + assertPerformanceLogMessage(APPENDER_NAME, CONTEXT_DATA, "null", memoryAppender); + } + + @Test + void givenPayloadBuilderAndNullOutputReturningNullWhenThenLogExceptionInfo(){ + // When + Object result = PerformanceLogger.execute(APPENDER_NAME, CONTEXT_DATA, () -> null, x -> null, null); + + //Then + Assertions.assertNull(result); + assertPerformanceLogMessage(APPENDER_NAME, CONTEXT_DATA, "Returned null", memoryAppender); + } + + @Test + void testThresholdLevelTranscoding(){ + PerformanceLoggerThresholdLevels thresholdLevels = new PerformanceLoggerThresholdLevels(1, 2); + + Assertions.assertEquals(Level.INFO, PerformanceLogger.resolveLevel(0, thresholdLevels)); + Assertions.assertEquals(Level.WARN, PerformanceLogger.resolveLevel(1000, thresholdLevels)); + Assertions.assertEquals(Level.ERROR, PerformanceLogger.resolveLevel(2000, thresholdLevels)); + } + + public static void assertPerformanceLogMessage(String expectedAppenderName, String expectedContextData, String expectedPayload, MemoryAppender memoryAppender) { + Assertions.assertEquals(1, memoryAppender.getLoggedEvents().size()); + ILoggingEvent event = memoryAppender.getLoggedEvents().getFirst(); + Assertions.assertEquals("PERFORMANCE_LOG." + expectedAppenderName, event.getLoggerName()); + String logMessage = event.getFormattedMessage(); + Assertions.assertTrue( + logMessage.matches( + "\\[%s] Time occurred to perform business logic: \\d+ ms\\. .*".formatted(expectedContextData) + ) && + logMessage.endsWith(". " + expectedPayload), + "Unexpected logged message: " + logMessage + ); + } +} diff --git a/src/test/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLoggerTest.java b/src/test/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLoggerTest.java new file mode 100644 index 0000000..f1eda14 --- /dev/null +++ b/src/test/java/it/gov/pagopa/template/performancelogger/RestInvokePerformanceLoggerTest.java @@ -0,0 +1,74 @@ +package it.gov.pagopa.template.performancelogger; + +import it.gov.pagopa.template.utils.MemoryAppender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.net.URI; + +@ExtendWith(MockitoExtension.class) +class RestInvokePerformanceLoggerTest { + public static final String APPENDER_NAME = "REST_INVOKE"; + + @Mock + private HttpRequest httpRequestMock; + private final byte[] bodyMock = new byte[0]; + @Mock + private ClientHttpRequestExecution requestExecutionMock; + + private MemoryAppender memoryAppender; + + private RestInvokePerformanceLogger filter; + + @BeforeEach + void init() { + filter = new RestInvokePerformanceLogger(); + } + + @BeforeEach + public void setupMemoryAppender() { + this.memoryAppender = PerformanceLoggerTest.buildPerformanceLoggerMemoryAppender(APPENDER_NAME); + } + + @AfterEach + void verifyNoMoreInteractions() { + Mockito.verifyNoMoreInteractions( + httpRequestMock, + requestExecutionMock + ); + } + + @Test + void givenCoveredPathWhenDoFilterThenDontPerformanceLog() throws IOException { + // Given + ClientHttpResponse expectedResult = Mockito.mock(ClientHttpResponse.class); + Mockito.when(expectedResult.getStatusCode()).thenReturn(HttpStatus.OK); + + Mockito.when(requestExecutionMock.execute(Mockito.same(httpRequestMock), Mockito.same(bodyMock))) + .thenReturn(expectedResult); + + Mockito.when(httpRequestMock.getMethod()).thenReturn(HttpMethod.GET); + Mockito.when(httpRequestMock.getURI()).thenReturn(URI.create("/api/test")); + + // When + ClientHttpResponse result = filter.intercept(httpRequestMock, bodyMock, requestExecutionMock); + + // Then + PerformanceLoggerTest.assertPerformanceLogMessage(APPENDER_NAME, "GET /api/test", "HttpStatus: 200", memoryAppender); + + Assertions.assertSame(expectedResult, result); + } + +} diff --git a/src/test/java/it/gov/pagopa/template/utils/MemoryAppender.java b/src/test/java/it/gov/pagopa/template/utils/MemoryAppender.java new file mode 100644 index 0000000..ed43eca --- /dev/null +++ b/src/test/java/it/gov/pagopa/template/utils/MemoryAppender.java @@ -0,0 +1,27 @@ +package it.gov.pagopa.template.utils; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + +import java.util.Collections; +import java.util.List; + +public class MemoryAppender extends ListAppender { + public void reset() { + this.list.clear(); + } + + public boolean contains(ch.qos.logback.classic.Level level, String string) { + return this.list.stream() + .anyMatch(event -> event.toString().contains(string) + && event.getLevel().equals(level)); + } + + public int getSize() { + return this.list.size(); + } + + public List getLoggedEvents() { + return Collections.unmodifiableList(this.list); + } +}