Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions src/main/java/app/quickcase/sdk/spring/logging/AccessLogFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package app.quickcase.sdk.spring.logging;

public class DefaultAccessLogLevelStrategy implements AccessLogLevelStrategy {
}
Original file line number Diff line number Diff line change
@@ -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<HttpMethod> excludedMethods;

/**
* Request URI patterns to exclude from access logs.
*/
@Singular
private final Set<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app.quickcase.sdk.spring.logging.LoggingAutoConfiguration
Original file line number Diff line number Diff line change
@@ -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));
}
}
}