diff --git a/src/main/java/app/quickcase/sdk/spring/logging/AccessLogFilter.java b/src/main/java/app/quickcase/sdk/spring/logging/AccessLogFilter.java index 9ce3d39..779da82 100644 --- a/src/main/java/app/quickcase/sdk/spring/logging/AccessLogFilter.java +++ b/src/main/java/app/quickcase/sdk/spring/logging/AccessLogFilter.java @@ -13,6 +13,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.slf4j.event.Level; import org.slf4j.spi.LoggingEventBuilder; import org.springframework.http.HttpMethod; @@ -23,14 +24,15 @@ public class AccessLogFilter implements Filter { private static final String KEY_STATUS = "responseCode"; private static final String KEY_DURATION = "duration"; - private final Clock clock; + private final static Clock clock = Clock.systemDefaultZone(); + private final AccessLogLevelStrategy logLevelStrategy; public AccessLogFilter() { - this.clock = Clock.systemDefaultZone(); + this.logLevelStrategy = new DefaultAccessLogLevelStrategy(); } - public AccessLogFilter(Clock clock) { - this.clock = clock; + public AccessLogFilter(AccessLogLevelStrategy logLevelStrategy) { + this.logLevelStrategy = logLevelStrategy; } @Override @@ -43,7 +45,7 @@ public void doFilter(ServletRequest request, final String method = httpRequest.getMethod(); final String requestURI = httpRequest.getRequestURI(); - log.atTrace() + log.atLevel(logLevelStrategy.onReceived(method, requestURI)) .addKeyValue(KEY_METHOD, method) .addKeyValue(KEY_URI, requestURI) .log("Request received: {} {}", method, requestURI); @@ -56,17 +58,17 @@ public void doFilter(ServletRequest request, final Duration duration = Duration.between(start, clock.instant()); final int status = httpResponse.getStatus(); - successLogBuilder(method) - .addKeyValue(KEY_METHOD, method) - .addKeyValue(KEY_URI, requestURI) - .addKeyValue(KEY_STATUS, status) - .addKeyValue(KEY_DURATION, duration.toMillis()) - .log("Request processed ({}) in {}ms: {} {}", status, duration.toMillis(), method, requestURI); + log.atLevel(logLevelStrategy.onCompleted(method, requestURI, status)) + .addKeyValue(KEY_METHOD, method) + .addKeyValue(KEY_URI, requestURI) + .addKeyValue(KEY_STATUS, status) + .addKeyValue(KEY_DURATION, duration.toMillis()) + .log("Request processed ({}) in {}ms: {} {}", status, duration.toMillis(), method, requestURI); } catch (Exception exception) { final Duration duration = Duration.between(start, clock.instant()); - log.atError() + log.atLevel(logLevelStrategy.onException(method, requestURI, exception)) .addKeyValue(KEY_METHOD, method) .addKeyValue(KEY_URI, requestURI) .addKeyValue(KEY_DURATION, duration.toMillis()) diff --git a/src/main/java/app/quickcase/sdk/spring/logging/AccessLogLevelStrategy.java b/src/main/java/app/quickcase/sdk/spring/logging/AccessLogLevelStrategy.java new file mode 100644 index 0000000..e13f4d6 --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/logging/AccessLogLevelStrategy.java @@ -0,0 +1,15 @@ +package app.quickcase.sdk.spring.logging; + +import org.slf4j.event.Level; + +public interface AccessLogLevelStrategy { + default Level onReceived(String method, String uri) { + return Level.DEBUG; + } + default Level onCompleted(String method, String uri, int status) { + return Level.INFO; + } + default Level onException(String method, String uri, Exception exception) { + return Level.ERROR; + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/logging/DefaultAccessLogLevelStrategy.java b/src/main/java/app/quickcase/sdk/spring/logging/DefaultAccessLogLevelStrategy.java new file mode 100644 index 0000000..0f177bf --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/logging/DefaultAccessLogLevelStrategy.java @@ -0,0 +1,4 @@ +package app.quickcase.sdk.spring.logging; + +public class DefaultAccessLogLevelStrategy implements AccessLogLevelStrategy { +} diff --git a/src/main/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategy.java b/src/main/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategy.java new file mode 100644 index 0000000..2e244ae --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategy.java @@ -0,0 +1,62 @@ +package app.quickcase.sdk.spring.logging; + +import java.util.Set; + +import lombok.Builder; +import lombok.Singular; +import org.slf4j.event.Level; +import org.springframework.http.HttpMethod; + +/** + * Simple strategy to "exclude" requests matching either the provided HTTP methods or URI patterns from access logs. + * Exclusion is performed by lowering log level for the request to the specified exclusion level (TRACE by default). + * + * Non-matched requests will be logged at their default level: DEBUG on reception, INFO on completion and ERROR on + * unhandled exception. + */ +@Builder +public class ExcludingAccessLogLevelStrategy implements AccessLogLevelStrategy { + /** + * Request HTTP methods to exclude from access logs. + */ + @Singular + private final Set excludedMethods; + + /** + * Request URI patterns to exclude from access logs. + */ + @Singular + private final Set excludedUriPatterns; + + /** + * The level at which requests matching exclusion criteria will be logged. + */ + @Builder.Default + private Level exclusionLevel = Level.TRACE; + + @Override + public Level onReceived(String method, String uri) { + if (excluded(method, uri)) { + return exclusionLevel; + } + + return AccessLogLevelStrategy.super.onReceived(method, uri); + } + + @Override + public Level onCompleted(String method, String uri, int status) { + if (excluded(method, uri)) { + return exclusionLevel; + } + + return AccessLogLevelStrategy.super.onCompleted(method, uri, status); + } + + private boolean excluded(String method, String uri) { + if (excludedMethods.stream().anyMatch((httpMethod) -> httpMethod.matches(method))) { + return true; + } + + return excludedUriPatterns.stream().anyMatch(uri::matches); + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/logging/LoggingConfiguration.java b/src/main/java/app/quickcase/sdk/spring/logging/LoggingAutoConfiguration.java similarity index 55% rename from src/main/java/app/quickcase/sdk/spring/logging/LoggingConfiguration.java rename to src/main/java/app/quickcase/sdk/spring/logging/LoggingAutoConfiguration.java index 2e32198..5dbb3a0 100644 --- a/src/main/java/app/quickcase/sdk/spring/logging/LoggingConfiguration.java +++ b/src/main/java/app/quickcase/sdk/spring/logging/LoggingAutoConfiguration.java @@ -1,6 +1,7 @@ package app.quickcase.sdk.spring.logging; import jakarta.servlet.Filter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -11,22 +12,31 @@ * access logs and request tracing. */ @Configuration -public class LoggingConfiguration { +public class LoggingAutoConfiguration { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) + @ConditionalOnMissingBean(MDCBoundaryFilter.class) public Filter mdcBoundaryFilter() { return new MDCBoundaryFilter(); } @Bean @Order(Ordered.HIGHEST_PRECEDENCE + 1) + @ConditionalOnMissingBean(TraceRequestFilter.class) public Filter traceRequestFilter() { return new TraceRequestFilter(); } + @Bean + @ConditionalOnMissingBean(AccessLogLevelStrategy.class) + public AccessLogLevelStrategy defaultAccessLogLevelStrategy() { + return new DefaultAccessLogLevelStrategy(); + } + @Bean @Order(Ordered.HIGHEST_PRECEDENCE + 2) - public Filter accessLogFilter() { - return new AccessLogFilter(); + @ConditionalOnMissingBean(AccessLogFilter.class) + public Filter accessLogFilter(AccessLogLevelStrategy accessLogLevelStrategy) { + return new AccessLogFilter(accessLogLevelStrategy); } } diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f0cc336 --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +app.quickcase.sdk.spring.logging.LoggingAutoConfiguration \ No newline at end of file diff --git a/src/test/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategyTest.java b/src/test/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategyTest.java new file mode 100644 index 0000000..47cceeb --- /dev/null +++ b/src/test/java/app/quickcase/sdk/spring/logging/ExcludingAccessLogLevelStrategyTest.java @@ -0,0 +1,117 @@ +package app.quickcase.sdk.spring.logging; + +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import org.springframework.http.HttpMethod; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class ExcludingAccessLogLevelStrategyTest { + + @Nested + class OnReceived { + @Test + @DisplayName("Given an excluded method onReceived should return exclusion level") + public void testOnReceivedWithExcludedMethodReturnsExclusionLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedMethods(Set.of(HttpMethod.GET)) + .build(); + + Level result = strategy.onReceived(HttpMethod.GET.name(), "/test"); + + assertThat(result, is(Level.TRACE)); + } + + @Test + @DisplayName("Given a not excluded method onReceived should return default level") + public void testOnReceivedWithoutExcludedMethodReturnsDelegatedLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedMethods(Set.of(HttpMethod.POST)) + .build(); + + Level result = strategy.onReceived(HttpMethod.GET.name(), "/test"); + + assertThat(result, is(Level.DEBUG)); + } + + @Test + @DisplayName("Given an excluded URI onReceived should return exclusion level") + public void testOnReceivedWithExcludedUriReturnsExclusionLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedUriPatterns(Set.of("/test.*")) + .build(); + + Level result = strategy.onReceived(HttpMethod.GET.name(), "/test"); + + assertThat(result, is(Level.TRACE)); + } + + @Test + @DisplayName("Given a not excluded URI onReceived should return default level") + public void testOnReceivedWithoutExcludedUriReturnsDefaultLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedUriPatterns(Set.of("/abc.*")) + .build(); + + Level result = strategy.onReceived(HttpMethod.GET.name(), "/test"); + + assertThat(result, is(Level.DEBUG)); + } + } + + @Nested + class OnCompleted { + @Test + @DisplayName("Given an excluded method onCompleted should return exclusion level") + public void testOnCompletedWithExcludedMethodReturnsExclusionLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedMethods(Set.of(HttpMethod.GET)) + .build(); + + Level result = strategy.onCompleted(HttpMethod.GET.name(), "/test", 200); + + assertThat(result, is(Level.TRACE)); + } + + @Test + @DisplayName("Given a not excluded method onCompleted should return default Level") + public void testOnCompletedWithoutExcludedMethodReturnsDelegatedLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedMethods(Set.of(HttpMethod.POST)) + .build(); + + Level result = strategy.onCompleted(HttpMethod.GET.name(), "/test", 200); + + assertThat(result, is(Level.INFO)); + } + + @Test + @DisplayName("Given an excluded URI onCompleted should return exclusion Level") + public void testOnCompletedWithExcludedUriReturnsExclusionLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedUriPatterns(Set.of("/test.*")) + .build(); + + Level result = strategy.onCompleted(HttpMethod.GET.name(), "/test", 200); + + assertThat(result, is(Level.TRACE)); + } + + @Test + @DisplayName("Given a not excluded URI onCompleted should return default Level") + public void testOnCompletedWithoutExcludedUriReturnsDefaultLevel() { + ExcludingAccessLogLevelStrategy strategy = ExcludingAccessLogLevelStrategy.builder() + .excludedUriPatterns(Set.of("/abc.*")) + .build(); + + Level result = strategy.onCompleted(HttpMethod.GET.name(), "/test", 200); + + assertThat(result, is(Level.INFO)); + } + } +}