diff --git a/README.md b/README.md index f83fd31..30645e4 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,66 @@ ## Features - * Buildin appender: - * console format - * stackdriver format (with support for error reporting) - * logback filter for log-levels per mdc +### Preconfigured Appender -## Usage +Buildin appender: + * console format + * stackdriver format (with support for error reporting) -MDC example: +### Set log level based on MDC values + +Confguration: + * *enabled*: enable MDC filter (`true` is default) + * *key*: MDC key, is optional (will use name instead, see example `user` below) + * *level*: log level to use (`TRACE` is default) + * *loggers*: whitelist of logger names, matches all loggers if empty + * *values*: values for matching MDC key, matches all values if empty + +Example for setting different values for different values/logger: ``` logger: levels: io.kokuwa: INFO mdc: - gateway: + gateway-debug: + key: gateway level: DEBUG loggers: - io.kokuwa values: - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 + gateway-trace: + key: gateway + level: TRACE + loggers: + - io.kokuwa + - io.micronaut + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f +``` + +Example for omiting level and key: +``` +logger: + levels: + io.kokuwa: INFO + mdc: + gateway: + loggers: + - io.kokuwa + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f + - 0a44738b-0c3a-4798-8210-2495485f10b2 +``` + +Example for minimal configuration: +``` +logger: + levels: + io.kokuwa: INFO + mdc: + user: {} ``` ## Build & Release @@ -47,6 +87,4 @@ mvn release:prepare release:perform release:clean -B ## Open Topics - * tests * configure mdc on refresh event - * examples and documentation \ No newline at end of file diff --git a/pom.xml b/pom.xml index e6e815e..a053834 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,11 @@ micronaut-runtime provided + + io.micronaut.test + micronaut-test-junit5 + test + @@ -103,6 +108,12 @@ + + + src/test/resources + true + + diff --git a/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java b/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java new file mode 100644 index 0000000..625191e --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java @@ -0,0 +1,50 @@ +package io.kokuwa.micronaut.logging; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import javax.inject.Singleton; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.turbo.TurboFilter; +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; + +/** + * Utility class for Logback operations. + * + * @author Stephan Schnabel + */ +@Requires(classes = LoggerContext.class) +@BootstrapContextCompatible +@Singleton +@Internal +public class LogbackUtil { + + private final LoggerContext context; + + public LogbackUtil() { + this.context = (LoggerContext) LoggerFactory.getILoggerFactory(); + } + + public Optional getTurboFilter(Class type, String name) { + return context.getTurboFilterList().stream() + .filter(filter -> Objects.equals(name, filter.getName())) + .filter(type::isInstance) + .map(type::cast).findAny(); + } + + public T getTurboFilter(Class type, String name, Supplier defaultFilter) { + return getTurboFilter(type, name).orElseGet(() -> { + var filter = defaultFilter.get(); + filter.setName(name); + filter.setContext(context); + context.addTurboFilter(filter); + return filter; + }); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java deleted file mode 100644 index 4468643..0000000 --- a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.kokuwa.micronaut.logging; - -import java.util.Objects; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.LoggerContext; -import io.micronaut.context.annotation.BootstrapContextCompatible; -import io.micronaut.context.annotation.Context; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.env.Environment; -import io.micronaut.context.event.ApplicationEventListener; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.type.Argument; -import io.micronaut.runtime.context.scope.refresh.RefreshEvent; - -/** - * Configure mdc filter. - * - * @author Stephan Schnabel - */ -@BootstrapContextCompatible -@Context -@Requires(classes = LoggerContext.class) -@Requires(property = MDCTurboFilterConfigurer.LOGGER_MDCS_PROPERTY_PREFIX) -@Internal -public class MDCTurboFilterConfigurer implements ApplicationEventListener { - - public static final String LOGGER_MDCS_PROPERTY_PREFIX = "logger.mdc"; - - private static final Logger log = LoggerFactory.getLogger(MDCTurboFilterConfigurer.class); - private final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); - private final Environment environment; - - public MDCTurboFilterConfigurer(Environment environment) { - this.environment = environment; - configure(); - } - - @Override - public void onApplicationEvent(RefreshEvent event) { - if (event.getSource().keySet().stream().anyMatch(key -> key.startsWith(LOGGER_MDCS_PROPERTY_PREFIX))) { - configure(); - } - } - - private void configure() { - for (var name : environment.getPropertyEntries(LOGGER_MDCS_PROPERTY_PREFIX)) { - - var prefix = LOGGER_MDCS_PROPERTY_PREFIX + "." + name + "."; - var key = environment.getProperty(prefix + "key", String.class, name); - var loggers = environment.getProperty(prefix + "loggers", Argument.setOf(String.class)).orElseGet(Set::of); - var values = environment.getProperty(prefix + "values", Argument.setOf(String.class)).orElseGet(Set::of); - var level = Level.valueOf(environment.getProperty(prefix + "level", String.class, Level.TRACE.toString())); - - getFilter(name, key).setLoggers(loggers).setValues(values).setLevel(level); - - log.info("Configured MDC filter {} for key {} with level {}.", name, key, level); - } - } - - private MDCTurboFilter getFilter(String name, String key) { - - // get filter - - var filterName = LOGGER_MDCS_PROPERTY_PREFIX + "." + key; - var filterOptional = context.getTurboFilterList().stream() - .filter(f -> Objects.equals(filterName, f.getName())) - .filter(MDCTurboFilter.class::isInstance) - .map(MDCTurboFilter.class::cast) - .findAny(); - if (filterOptional.isPresent()) { - return filterOptional.get(); - } - - // add filter - - var filter = new MDCTurboFilter(name, key, context); - filter.start(); - context.addTurboFilter(filter); - return filter; - } -} diff --git a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java similarity index 70% rename from src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java index 1f00fd6..c6f01b1 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java @@ -1,4 +1,4 @@ -package io.kokuwa.micronaut.logging; +package io.kokuwa.micronaut.logging.mdc; import java.util.HashMap; import java.util.HashSet; @@ -11,29 +11,21 @@ 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.Context; import ch.qos.logback.core.spi.FilterReply; /** - * Filter for log levels based on mdc. + * Filter for log levels based on MDC. * * @author Stephan Schnabel */ public class MDCTurboFilter extends TurboFilter { - private final String key; private final Map cache = new HashMap<>(); private final Set loggers = new HashSet<>(); private final Set values = new HashSet<>(); + private String key; private Level level; - public MDCTurboFilter(String name, String key, Context context) { - this.key = key; - this.level = Level.TRACE; - this.setName(name); - this.setContext(context); - } - public MDCTurboFilter setLoggers(Set loggers) { this.cache.clear(); this.loggers.clear(); @@ -47,6 +39,11 @@ public class MDCTurboFilter extends TurboFilter { return this; } + public MDCTurboFilter setKey(String key) { + this.key = key; + return this; + } + public MDCTurboFilter setLevel(Level level) { this.level = level; return this; @@ -55,17 +52,18 @@ public class MDCTurboFilter extends TurboFilter { @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { - if (logger == null || !isStarted() || values.isEmpty() || loggers.isEmpty()) { + if (logger == null || !isStarted()) { return FilterReply.NEUTRAL; } var value = MDC.get(key); - if (value == null || !values.contains(value)) { + if (value == null || !values.isEmpty() && !values.contains(value)) { return FilterReply.NEUTRAL; } - var isLoggerIncluded = !cache.computeIfAbsent(logger.getName(), k -> loggers.stream().anyMatch(k::startsWith)); - if (isLoggerIncluded) { + var isLoggerIncluded = loggers.isEmpty() + || cache.computeIfAbsent(logger.getName(), k -> loggers.stream().anyMatch(k::startsWith)); + if (!isLoggerIncluded) { return FilterReply.NEUTRAL; } diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java new file mode 100644 index 0000000..300b57e --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java @@ -0,0 +1,60 @@ +package io.kokuwa.micronaut.logging.mdc; + +import java.util.Set; + +import ch.qos.logback.classic.Level; +import io.kokuwa.micronaut.logging.LogbackUtil; +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.env.Environment; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.type.Argument; +import lombok.extern.slf4j.Slf4j; + +/** + * Configure MDC filter. + * + * @author Stephan Schnabel + */ +@Requires(beans = LogbackUtil.class) +@Requires(property = MDCTurboFilterConfigurer.PREFIX) +@Requires(property = MDCTurboFilterConfigurer.ENABLED, notEquals = "false") +@BootstrapContextCompatible +@Context +@Internal +@Slf4j +public class MDCTurboFilterConfigurer { + + public static final String PREFIX = "logger.mdc"; + public static final String ENABLED = PREFIX + ".enabled"; + + private final LogbackUtil logback; + private final Environment environment; + + public MDCTurboFilterConfigurer(LogbackUtil logback, Environment environment) { + this.logback = logback; + this.environment = environment; + configure(); + } + + public void configure() { + for (var name : environment.getPropertyEntries(PREFIX)) { + + var prefix = PREFIX + "." + name + "."; + var key = environment.getProperty(prefix + "key", String.class, name); + var loggers = environment.getProperty(prefix + "loggers", Argument.setOf(String.class)).orElseGet(Set::of); + var values = environment.getProperty(prefix + "values", Argument.setOf(String.class)).orElseGet(Set::of); + var level = Level.valueOf(environment.getProperty(prefix + "level", String.class, Level.TRACE.toString())); + + logback.getTurboFilter(MDCTurboFilter.class, name, MDCTurboFilter::new) + .setKey(key) + .setLevel(level) + .setLoggers(loggers) + .setValues(values) + .start(); + + log.info("Configured MDC filter {} for key {} with level {}.", name, key, level); + } + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java new file mode 100644 index 0000000..f63e4c2 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java @@ -0,0 +1,25 @@ +package io.kokuwa.micronaut.logging; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.Alphanumeric; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.MDC; + +import io.micronaut.test.annotation.MicronautTest; + +/** + * Base for tests regarding logging. + * + * @author Stephan Schnabel + */ +@MicronautTest +@TestMethodOrder(Alphanumeric.class) +public abstract class AbstractTest { + + @BeforeEach + @AfterEach + void setUpMdc() { + MDC.clear(); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java new file mode 100644 index 0000000..f8252be --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java @@ -0,0 +1,111 @@ +package io.kokuwa.micronaut.logging.mdc; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import io.kokuwa.micronaut.logging.AbstractTest; +import io.micronaut.test.annotation.MicronautTest; + +/** + * Test for {@link MDCTurboFilterConfigurer}. + * + * @author Stephan Schnabel + */ +@DisplayName("mdc") +@MicronautTest(environments = "test-mdc") +public class MDCTurboFilterTest extends AbstractTest { + + final Logger logA = LoggerFactory.getLogger("io.kiwios.a"); + final Logger logB = LoggerFactory.getLogger("io.kiwios.b"); + final Logger logC = LoggerFactory.getLogger("io.kiwios.c"); + final Logger logOther = LoggerFactory.getLogger("org.example"); + + @DisplayName("no key set") + @Test + void noKeySet() { + assertFalse(logA.isDebugEnabled()); + assertFalse(logA.isTraceEnabled()); + assertFalse(logB.isDebugEnabled()); + assertFalse(logB.isTraceEnabled()); + assertFalse(logC.isDebugEnabled()); + assertFalse(logC.isTraceEnabled()); + assertFalse(logOther.isDebugEnabled()); + assertFalse(logOther.isTraceEnabled()); + } + + @DisplayName("match nothing") + @Test + void matchNothing() { + MDC.put("key", "value-4"); + assertFalse(logA.isDebugEnabled()); + assertFalse(logA.isTraceEnabled()); + assertFalse(logB.isDebugEnabled()); + assertFalse(logB.isTraceEnabled()); + assertFalse(logC.isDebugEnabled()); + assertFalse(logC.isTraceEnabled()); + assertFalse(logOther.isDebugEnabled()); + assertFalse(logOther.isTraceEnabled()); + } + + @DisplayName("match root logger") + @Test + void matchRootLogger() { + MDC.put("key", "value-3"); + assertTrue(logA.isDebugEnabled()); + assertTrue(logA.isTraceEnabled()); + assertTrue(logB.isDebugEnabled()); + assertTrue(logB.isTraceEnabled()); + assertTrue(logC.isDebugEnabled()); + assertTrue(logC.isTraceEnabled()); + assertFalse(logOther.isDebugEnabled()); + assertFalse(logOther.isTraceEnabled()); + } + + @DisplayName("match single filter") + @Test + void matchSingleFilter() { + MDC.put("key", "value-1"); + assertTrue(logA.isDebugEnabled()); + assertFalse(logA.isTraceEnabled()); + assertTrue(logB.isDebugEnabled()); + assertFalse(logB.isTraceEnabled()); + assertFalse(logC.isDebugEnabled()); + assertFalse(logC.isTraceEnabled()); + assertFalse(logOther.isDebugEnabled()); + assertFalse(logOther.isTraceEnabled()); + } + + @DisplayName("match multiple filter") + @Test + void matchMultipleFilter() { + MDC.put("key", "value-2"); + assertTrue(logA.isDebugEnabled()); + assertFalse(logA.isTraceEnabled()); + assertTrue(logB.isDebugEnabled()); + assertTrue(logB.isTraceEnabled()); + assertTrue(logC.isDebugEnabled()); + assertTrue(logC.isTraceEnabled()); + assertFalse(logOther.isDebugEnabled()); + assertFalse(logOther.isTraceEnabled()); + } + + @DisplayName("match simple config") + @Test + void matchSimpleConfig() { + MDC.put("user", "foobar"); + assertTrue(logA.isDebugEnabled()); + assertTrue(logA.isTraceEnabled()); + assertTrue(logB.isDebugEnabled()); + assertTrue(logB.isTraceEnabled()); + assertTrue(logC.isDebugEnabled()); + assertTrue(logC.isTraceEnabled()); + assertTrue(logOther.isDebugEnabled()); + assertTrue(logOther.isTraceEnabled()); + } +} diff --git a/src/test/resources/META-INF/build-info.properties b/src/test/resources/META-INF/build-info.properties new file mode 100644 index 0000000..5401a6c --- /dev/null +++ b/src/test/resources/META-INF/build-info.properties @@ -0,0 +1,2 @@ +serviceName: ${project.artifactId} +serviceVersion: ${project.version} diff --git a/src/test/resources/application-test-mdc.yaml b/src/test/resources/application-test-mdc.yaml new file mode 100644 index 0000000..d6150b0 --- /dev/null +++ b/src/test/resources/application-test-mdc.yaml @@ -0,0 +1,26 @@ +logger: + mdc: + key1: + key: key + level: DEBUG + loggers: + - io.kiwios.a + - io.kiwios.b + values: + - value-1 + - value-2 + key2: + key: key + level: TRACE + loggers: + - io.kiwios.b + - io.kiwios.c + values: + - value-2 + key: + level: TRACE + loggers: + - io.kiwios + values: + - value-3 + user: {}