From addc97f5cd78b8f8a8cb827e38da866ced8627b2 Mon Sep 17 00:00:00 2001 From: Stephan Schnabel Date: Thu, 20 Aug 2020 19:40:48 +0200 Subject: [PATCH] Add request filter. --- README.md | 9 +++ pom.xml | 10 +++ .../request/RequestLoggingHttpFilter.java | 76 ++++++++++++++++++ .../request/RequestLoggingTurboFilter.java | 32 ++++++++ .../request/RequestLoggingController.java | 35 +++++++++ .../logging/request/RequestLoggingTest.java | 78 +++++++++++++++++++ src/test/resources/application-test.yaml | 8 ++ 7 files changed, 248 insertions(+) create mode 100644 src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingHttpFilter.java create mode 100644 src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingTurboFilter.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingController.java create mode 100644 src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingTest.java create mode 100644 src/test/resources/application-test.yaml diff --git a/README.md b/README.md index 30645e4..0070370 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ logger: user: {} ``` + +### Set log level based on HTTP request header + +Confguration: + * *enabled*: enable HTTP request filter (`true` is default) + * *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/master/core/src/main/java/io/micronaut/core/order/Ordered.java) (highest is default) + * *pattern*: filter pattern (`/**` is default) + * *header*: name of HTTP header (`x-log-level` is default) + ## Build & Release ### Dependency updates diff --git a/pom.xml b/pom.xml index a053834..80b8357 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,16 @@ micronaut-test-junit5 test + + io.micronaut + micronaut-http-client + test + + + io.micronaut + micronaut-http-server-netty + test + diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingHttpFilter.java new file mode 100644 index 0000000..8f4d659 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingHttpFilter.java @@ -0,0 +1,76 @@ +package io.kokuwa.micronaut.logging.request; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.reactivestreams.Publisher; +import org.slf4j.MDC; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.turbo.TurboFilter; +import io.kokuwa.micronaut.logging.LogbackUtil; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.order.Ordered; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.runtime.server.EmbeddedServer; + +/** + * Http request logging filter. + * + * @author Stephan Schnabel + */ +@Requires(beans = EmbeddedServer.class) +@Requires(property = RequestLoggingHttpFilter.ENABLED, notEquals = "false") +@Filter("${" + RequestLoggingHttpFilter.PREFIX + ".pattern:" + RequestLoggingHttpFilter.DEFAULT_PATTERN + ":/**}") +public class RequestLoggingHttpFilter implements HttpServerFilter { + + public static final String PREFIX = "logger.request"; + public static final String ENABLED = PREFIX + ".enabled"; + public static final String MDC_FILTER_NAME = PREFIX + ".filter"; + public static final String MDC_KEY = "level"; + + public static final String DEFAULT_HEADER = "x-log-level"; + public static final String DEFAULT_PATTERN = "/**"; + + private final LogbackUtil logback; + private final String header; + private final int order; + + public RequestLoggingHttpFilter( + LogbackUtil logback, + @Value("${" + PREFIX + ".header:" + DEFAULT_HEADER + "}") String header, + @Value("${" + PREFIX + ".order:" + Ordered.HIGHEST_PRECEDENCE + "}") int order) { + this.logback = logback; + this.header = header; + this.order = order; + } + + @PostConstruct + void startTurbofilter() { + logback.getTurboFilter(RequestLoggingTurboFilter.class, MDC_FILTER_NAME, RequestLoggingTurboFilter::new).start(); + } + + @PreDestroy + void stopTurbofilter() { + logback.getTurboFilter(RequestLoggingTurboFilter.class, MDC_FILTER_NAME).ifPresent(TurboFilter::stop); + } + + @Override + public int getOrder() { + return order; + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + var level = request.getHeaders().getFirst(header).map(Level::valueOf); + if (level.isPresent()) { + MDC.put(MDC_KEY, level.get().toString()); + } + return chain.proceed(request); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingTurboFilter.java new file mode 100644 index 0000000..6a58770 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/request/RequestLoggingTurboFilter.java @@ -0,0 +1,32 @@ +package io.kokuwa.micronaut.logging.request; + +import org.slf4j.MDC; +import org.slf4j.Marker; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.spi.FilterReply; + +/** + * Filter for log levels based on MDC. + * + * @author Stephan Schnabel + */ +public class RequestLoggingTurboFilter extends TurboFilter { + + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + + if (!isStarted()) { + return FilterReply.NEUTRAL; + } + + var value = MDC.get(RequestLoggingHttpFilter.MDC_KEY); + if (value == null) { + return FilterReply.NEUTRAL; + } + + return level.isGreaterOrEqual(Level.valueOf(value)) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingController.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingController.java new file mode 100644 index 0000000..ffd7f65 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingController.java @@ -0,0 +1,35 @@ +package io.kokuwa.micronaut.logging.request; + +import ch.qos.logback.classic.Level; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import lombok.extern.slf4j.Slf4j; + +/** + * Contoller for testing {@link RequestLoggingHttpFilter}. + * + * @author Stephan Schnabel + */ +@Controller +@Slf4j +public class RequestLoggingController { + + @Get + String run() { + + var level = Level.OFF; + if (log.isTraceEnabled()) { + level = Level.TRACE; + } else if (log.isDebugEnabled()) { + level = Level.DEBUG; + } else if (log.isInfoEnabled()) { + level = Level.INFO; + } else if (log.isWarnEnabled()) { + level = Level.WARN; + } else if (log.isErrorEnabled()) { + level = Level.ERROR; + } + + return level.toString(); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingTest.java new file mode 100644 index 0000000..160545a --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/request/RequestLoggingTest.java @@ -0,0 +1,78 @@ +package io.kokuwa.micronaut.logging.request; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ch.qos.logback.classic.Level; +import io.kokuwa.micronaut.logging.AbstractTest; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; + +/** + * Test for {@link RequestLoggingHttpFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("request") +public class RequestLoggingTest extends AbstractTest { + + @Inject + @Client("/") + HttpClient client; + + @DisplayName("header missing") + @Test + void headerMissing() { + assertLevel(Level.INFO, null); + } + + @DisplayName("header invalid, use DEBUG as default from logback") + @Test + void headerInvalid() { + assertLevel(Level.DEBUG, "TRCE"); + } + + @DisplayName("level trace (below default)") + @Test + void headerLevelTrace() { + assertLevel(Level.TRACE, "TRACE"); + } + + @DisplayName("level debug (below default)") + @Test + void headerLevelDebug() { + assertLevel(Level.DEBUG, "DEBUG"); + } + + @DisplayName("level info (is default)") + @Test + void headerLevelInfo() { + assertLevel(Level.INFO, "INFO"); + } + + @DisplayName("level warn (above default)") + @Test + void headerLevelWarn() { + assertLevel(Level.INFO, "WARN"); + } + + private void assertLevel(Level expected, String header) { + + var request = HttpRequest.GET("/"); + if (header != null) { + request.getHeaders().add(RequestLoggingHttpFilter.DEFAULT_HEADER, header); + } + + var response = client.toBlocking().exchange(request, String.class); + assertEquals(HttpStatus.OK, response.getStatus(), "status"); + assertTrue(response.getBody().isPresent(), "body"); + assertEquals(expected, Level.valueOf(response.body()), "level"); + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml new file mode 100644 index 0000000..ad659d3 --- /dev/null +++ b/src/test/resources/application-test.yaml @@ -0,0 +1,8 @@ +micronaut: + http: + client: + logger-name: io.kokuwa.Test + +logger: + levels: + io.kokuwa: INFO