diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 418af27f5bf1f4..177502e5913ab0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -1,7 +1,9 @@ package io.quarkus.deployment.logging; +import java.lang.reflect.Modifier; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -13,6 +15,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.logging.Filter; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; @@ -29,9 +32,13 @@ import org.aesh.command.completer.OptionCompleter; import org.aesh.command.invocation.CommandInvocation; import org.aesh.command.option.Option; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; import org.jboss.logmanager.EmbeddedConfigurator; import org.jboss.logmanager.LogManager; @@ -49,6 +56,7 @@ import io.quarkus.deployment.annotations.Produce; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ConsoleCommandBuildItem; import io.quarkus.deployment.builditem.ConsoleFormatterBannerBuildItem; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; @@ -65,6 +73,7 @@ import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.WebSocketLogHandlerBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; @@ -79,6 +88,7 @@ import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.deployment.util.JandexUtil; import io.quarkus.dev.console.CurrentAppExceptionHighlighter; import io.quarkus.dev.spi.DevModeType; import io.quarkus.gizmo.AnnotationCreator; @@ -91,11 +101,13 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.logging.LoggingFilter; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.console.ConsoleRuntimeConfig; import io.quarkus.runtime.logging.CategoryBuildTimeConfig; import io.quarkus.runtime.logging.CleanupFilterConfig; +import io.quarkus.runtime.logging.DiscoveredLogComponents; import io.quarkus.runtime.logging.InheritableLevel; import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.runtime.logging.LogCleanupFilterElement; @@ -114,6 +126,13 @@ public final class LoggingResourceProcessor { "isMinLevelEnabled", boolean.class, int.class, String.class); + private static final DotName LOGGING_FILTER = DotName.createSimple(LoggingFilter.class.getName()); + private static final DotName FILTER = DotName.createSimple(Filter.class.getName()); + private static final String ILLEGAL_LOGGING_FILTER_USE_MESSAGE = "'@" + LoggingFilter.class.getName() + + "' can only be used on classes that implement '" + + Filter.class.getName() + "' and that are marked as final."; + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + @BuildStep void setupLogFilters(BuildProducer filters) { filters.produce(new LogCleanupFilterBuildItem("org.jboss.threads", "JBoss Threads version")); @@ -204,6 +223,7 @@ void miscSetup( @Record(ExecutionTime.RUNTIME_INIT) LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSetupRecorder recorder, LogConfig log, LogBuildTimeConfig buildLog, + CombinedIndexBuildItem combinedIndexBuildItem, LogCategoryMinLevelDefaultsBuildItem categoryMinLevelDefaults, Optional logStreamHandlerBuildItem, List handlerBuildItems, @@ -214,7 +234,8 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe List logStreamBuildItems, BuildProducer shutdownListenerBuildItemBuildProducer, LaunchModeBuildItem launchModeBuildItem, - List logCleanupFilters) { + List logCleanupFilters, + BuildProducer reflectiveClassBuildItemBuildProducer) { if (!launchModeBuildItem.isAuxiliaryApplication() || launchModeBuildItem.getAuxiliaryDevModeType().orElse(null) == DevModeType.TEST_ONLY) { final List>> handlers = handlerBuildItems.stream() @@ -245,13 +266,22 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe .map(LogFileFormatBuildItem::getFormatterValue).collect(Collectors.toList()); context.registerSubstitution(InheritableLevel.ActualLevel.class, String.class, InheritableLevel.Substitution.class); context.registerSubstitution(InheritableLevel.Inherited.class, String.class, InheritableLevel.Substitution.class); + + DiscoveredLogComponents discoveredLogComponents = discoverLogComponents(combinedIndexBuildItem.getIndex()); + if (!discoveredLogComponents.getNameToFilterClass().isEmpty()) { + reflectiveClassBuildItemBuildProducer.produce(new ReflectiveClassBuildItem(true, false, false, + discoveredLogComponents.getNameToFilterClass().values().toArray( + EMPTY_STRING_ARRAY))); + } + shutdownListenerBuildItemBuildProducer.produce(new ShutdownListenerBuildItem( - recorder.initializeLogging(log, buildLog, categoryMinLevelDefaults.content, alwaysEnableLogStream, + recorder.initializeLogging(log, buildLog, discoveredLogComponents, + categoryMinLevelDefaults.content, alwaysEnableLogStream, devUiLogHandler, handlers, namedHandlers, consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue) .collect(Collectors.toList()), possibleFileFormatters, - possibleSupplier, launchModeBuildItem.getLaunchMode()))); + possibleSupplier, launchModeBuildItem.getLaunchMode(), true))); LogConfig logConfig = new LogConfig(); ConfigInstantiator.handleObject(logConfig); for (LogCleanupFilterBuildItem i : logCleanupFilters) { @@ -276,6 +306,57 @@ public void run() { return new LoggingSetupBuildItem(); } + private DiscoveredLogComponents discoverLogComponents(IndexView index) { + Collection loggingFilterInstances = index.getAnnotations(LOGGING_FILTER); + DiscoveredLogComponents result = new DiscoveredLogComponents(); + + Map filtersMap = new HashMap<>(); + for (AnnotationInstance instance : loggingFilterInstances) { + AnnotationTarget target = instance.target(); + if (target.kind() != AnnotationTarget.Kind.CLASS) { + throw new IllegalStateException("Unimplemented mode of use of '" + LoggingFilter.class.getName() + "'"); + } + ClassInfo classInfo = target.asClass(); + if (!Modifier.isFinal(classInfo.flags())) { + throw new RuntimeException( + ILLEGAL_LOGGING_FILTER_USE_MESSAGE + " Offending class is '" + classInfo.name() + "'"); + } + boolean isFilterImpl = false; + ClassInfo currentClassInfo = classInfo; + while ((currentClassInfo != null) && (!JandexUtil.DOTNAME_OBJECT.equals(currentClassInfo.name()))) { + boolean hasFilterInterface = false; + List ifaces = currentClassInfo.interfaceNames(); + for (DotName iface : ifaces) { + if (FILTER.equals(iface)) { + hasFilterInterface = true; + break; + } + } + if (hasFilterInterface) { + isFilterImpl = true; + break; + } + currentClassInfo = index.getClassByName(currentClassInfo.superName()); + } + if (!isFilterImpl) { + throw new RuntimeException( + ILLEGAL_LOGGING_FILTER_USE_MESSAGE + " Offending class is '" + classInfo.name() + "'"); + } + + MethodInfo ctor = classInfo.method(""); + if ((ctor == null) || (ctor.typeParameters().size() > 0)) { + throw new RuntimeException("Classes annotated with '" + LoggingFilter.class.getName() + + "' must have a no-args constructor. Offending class is '" + classInfo.name() + "'"); + } + filtersMap.put(instance.value("name").asString(), classInfo.name().toString()); + } + if (!filtersMap.isEmpty()) { + result.setNameToFilterClass(filtersMap); + } + + return result; + } + @BuildStep(onlyIfNot = IsNormal.class) @Produce(TestSetupBuildItem.class) @Produce(LogConsoleFormatBuildItem.class) diff --git a/core/runtime/src/main/java/io/quarkus/logging/LoggingFilter.java b/core/runtime/src/main/java/io/quarkus/logging/LoggingFilter.java new file mode 100644 index 00000000000000..bf2adb61f2b4f0 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/logging/LoggingFilter.java @@ -0,0 +1,23 @@ +package io.quarkus.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Makes the filter class known to Quarkus by the specified name. + * The filter can then be configured for a handler (like the logging handler using {@code quarkus.log.console.filter}). + * + * This class must ONLY be placed on implementations of {@link java.util.logging.Filter} that are marked as {@code final}. + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LoggingFilter { + + /** + * Name with which the filter is referred to in configuration + */ + String name(); +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java index e888f45159bc7c..a9773156dfb72d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/ConsoleConfig.java @@ -55,6 +55,12 @@ public class ConsoleConfig { @ConfigItem(defaultValue = "0") int darken; + /** + * The name of the filter to link to the console handler. + */ + @ConfigItem + Optional filter; + /** * Console async logging config */ diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/DiscoveredLogComponents.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/DiscoveredLogComponents.java new file mode 100644 index 00000000000000..a7be611de09d85 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/DiscoveredLogComponents.java @@ -0,0 +1,21 @@ +package io.quarkus.runtime.logging; + +import java.util.Collections; +import java.util.Map; + +public class DiscoveredLogComponents { + + private Map nameToFilterClass = Collections.emptyMap(); + + public Map getNameToFilterClass() { + return nameToFilterClass; + } + + public void setNameToFilterClass(Map nameToFilterClass) { + this.nameToFilterClass = nameToFilterClass; + } + + public static DiscoveredLogComponents ofEmpty() { + return new DiscoveredLogComponents(); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java index 5e9b9239daa78e..9e8492fb82e31e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java @@ -40,6 +40,12 @@ public class FileConfig { @ConfigItem(defaultValue = DEFAULT_LOG_FILE_NAME) File path; + /** + * The name of the filter to link to the file handler. + */ + @ConfigItem + Optional filter; + /** * File async logging config */ diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index e3bf40d330e4f7..f10f9c3dab7313 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -18,6 +18,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.ErrorManager; +import java.util.logging.Filter; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; @@ -29,6 +30,7 @@ import org.jboss.logmanager.LogContext; import org.jboss.logmanager.Logger; import org.jboss.logmanager.errormanager.OnlyOnceErrorManager; +import org.jboss.logmanager.filters.AllFilter; import org.jboss.logmanager.formatters.ColorPatternFormatter; import org.jboss.logmanager.formatters.PatternFormatter; import org.jboss.logmanager.handlers.AsyncHandler; @@ -74,15 +76,17 @@ public static void handleFailedStart(RuntimeValue>> ba ConsoleRuntimeConfig consoleRuntimeConfig = new ConsoleRuntimeConfig(); ConfigInstantiator.handleObject(consoleRuntimeConfig); new LoggingSetupRecorder(new RuntimeValue<>(consoleRuntimeConfig)).initializeLogging(config, buildConfig, + DiscoveredLogComponents.ofEmpty(), Collections.emptyMap(), false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), banner, LaunchMode.DEVELOPMENT); + Collections.emptyList(), banner, LaunchMode.DEVELOPMENT, false); } public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig buildConfig, + DiscoveredLogComponents discoveredLogComponents, final Map categoryDefaultMinLevels, final boolean enableWebStream, final RuntimeValue> devUiConsoleHandler, @@ -90,7 +94,9 @@ public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig b final List>> additionalNamedHandlers, final List>> possibleConsoleFormatters, final List>> possibleFileFormatters, - final RuntimeValue>> possibleBannerSupplier, LaunchMode launchMode) { + final RuntimeValue>> possibleBannerSupplier, + LaunchMode launchMode, + boolean validateFilters) { ShutdownNotifier shutdownNotifier = new ShutdownNotifier(); final Map categories = config.categories; @@ -126,13 +132,15 @@ public void accept(String loggerName, CleanupFilterConfig config) { handler.setFilter(cleanupFiler); } + Map namedFilters = createNamedFilters(discoveredLogComponents); + final ArrayList handlers = new ArrayList<>( 3 + additionalHandlers.size() + (config.handlers.isPresent() ? config.handlers.get().size() : 0)); if (config.console.enable) { final Handler consoleHandler = configureConsoleHandler(config.console, consoleRuntimeConfig.getValue(), - errorManager, cleanupFiler, - possibleConsoleFormatters, possibleBannerSupplier, launchMode); + errorManager, cleanupFiler, namedFilters, possibleConsoleFormatters, possibleBannerSupplier, + launchMode, validateFilters); errorManager = consoleHandler.getErrorManager(); handlers.add(consoleHandler); } @@ -157,11 +165,13 @@ public void close() throws SecurityException { if (config.file.enable) { handlers.add( - configureFileHandler(config.file, errorManager, cleanupFiler, possibleFileFormatters)); + configureFileHandler(config.file, errorManager, cleanupFiler, namedFilters, possibleFileFormatters, + validateFilters)); } if (config.syslog.enable) { - final Handler syslogHandler = configureSyslogHandler(config.syslog, errorManager, cleanupFiler); + final Handler syslogHandler = configureSyslogHandler(config.syslog, errorManager, cleanupFiler, + namedFilters, validateFilters); if (syslogHandler != null) { handlers.add(syslogHandler); } @@ -185,8 +195,8 @@ public void close() throws SecurityException { Map namedHandlers = shouldCreateNamedHandlers(config, additionalNamedHandlers) ? createNamedHandlers(config, consoleRuntimeConfig.getValue(), additionalNamedHandlers, - possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, launchMode, - shutdownNotifier) + possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, namedFilters, launchMode, + shutdownNotifier, false) : Collections.emptyMap(); if (!categories.isEmpty()) { Map additionalNamedHandlersMap; @@ -238,6 +248,27 @@ public void accept(String categoryName, CategoryConfig config) { return shutdownNotifier; } + private static Map createNamedFilters(DiscoveredLogComponents discoveredLogComponents) { + if (discoveredLogComponents.getNameToFilterClass().isEmpty()) { + return Collections.emptyMap(); + } + + Map nameToFilter = new HashMap<>(); + discoveredLogComponents.getNameToFilterClass().forEach(new BiConsumer<>() { + @Override + public void accept(String name, String className) { + try { + nameToFilter.put(name, + (Filter) Class.forName(className, true, Thread.currentThread().getContextClassLoader()) + .getConstructor().newInstance()); + } catch (Exception e) { + throw new RuntimeException("Unable to create instance of Logging Filter '" + className + "'"); + } + } + }); + return nameToFilter; + } + /** * WARNING: this method is part of the recorder but is actually called statically at build time. * You may not push RuntimeValue's to it. @@ -267,15 +298,15 @@ public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConf if (config.console.enable) { final Handler consoleHandler = configureConsoleHandler(config.console, consoleConfig, errorManager, - logCleanupFilter, - Collections.emptyList(), new RuntimeValue<>(Optional.empty()), launchMode); + logCleanupFilter, Collections.emptyMap(), Collections.emptyList(), + new RuntimeValue<>(Optional.empty()), launchMode, false); errorManager = consoleHandler.getErrorManager(); handlers.add(consoleHandler); } Map namedHandlers = createNamedHandlers(config, consoleConfig, Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), errorManager, logCleanupFilter, launchMode, dummy); + Collections.emptyList(), Collections.emptyList(), errorManager, logCleanupFilter, + Collections.emptyMap(), launchMode, dummy, true); for (Map.Entry entry : categories.entrySet()) { final String categoryName = entry.getKey(); @@ -356,18 +387,18 @@ private static Map createNamedHandlers(LogConfig config, Consol List>> additionalNamedHandlers, List>> possibleConsoleFormatters, List>> possibleFileFormatters, - ErrorManager errorManager, - LogCleanupFilter cleanupFilter, LaunchMode launchMode, - ShutdownNotifier shutdownHandler) { + ErrorManager errorManager, LogCleanupFilter cleanupFilter, + Map namedFilters, LaunchMode launchMode, + ShutdownNotifier shutdownHandler, boolean validateFilters) { Map namedHandlers = new HashMap<>(); for (Entry consoleConfigEntry : config.consoleHandlers.entrySet()) { ConsoleConfig namedConsoleConfig = consoleConfigEntry.getValue(); if (!namedConsoleConfig.enable) { continue; } - final Handler consoleHandler = configureConsoleHandler(namedConsoleConfig, consoleRuntimeConfig, errorManager, - cleanupFilter, - possibleConsoleFormatters, null, launchMode); + final Handler consoleHandler = configureConsoleHandler(namedConsoleConfig, consoleRuntimeConfig, + errorManager, cleanupFilter, namedFilters, possibleConsoleFormatters, null, launchMode, + validateFilters); addToNamedHandlers(namedHandlers, consoleHandler, consoleConfigEntry.getKey()); } for (Entry fileConfigEntry : config.fileHandlers.entrySet()) { @@ -375,8 +406,8 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedFileConfig.enable) { continue; } - final Handler fileHandler = configureFileHandler(namedFileConfig, errorManager, cleanupFilter, - possibleFileFormatters); + final Handler fileHandler = configureFileHandler(namedFileConfig, errorManager, cleanupFilter, namedFilters, + possibleFileFormatters, validateFilters); addToNamedHandlers(namedHandlers, fileHandler, fileConfigEntry.getKey()); } for (Entry sysLogConfigEntry : config.syslogHandlers.entrySet()) { @@ -384,7 +415,8 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedSyslogConfig.enable) { continue; } - final Handler syslogHandler = configureSyslogHandler(namedSyslogConfig, errorManager, cleanupFilter); + final Handler syslogHandler = configureSyslogHandler(namedSyslogConfig, errorManager, cleanupFilter, + namedFilters, validateFilters); if (syslogHandler != null) { addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey()); } @@ -442,8 +474,7 @@ public void run() { } private static void addNamedHandlersToRootHandlers(Optional> handlerNames, Map namedHandlers, - ArrayList effectiveHandlers, - ErrorManager errorManager) { + ArrayList effectiveHandlers, ErrorManager errorManager) { if (handlerNames.isEmpty()) { return; } @@ -471,11 +502,15 @@ public void initializeLoggingForImageBuild() { } } - private static Handler configureConsoleHandler(final ConsoleConfig config, ConsoleRuntimeConfig consoleRuntimeConfig, + private static Handler configureConsoleHandler(final ConsoleConfig config, + ConsoleRuntimeConfig consoleRuntimeConfig, final ErrorManager defaultErrorManager, final LogCleanupFilter cleanupFilter, + final Map namedFilters, final List>> possibleFormatters, - final RuntimeValue>> possibleBannerSupplier, LaunchMode launchMode) { + final RuntimeValue>> possibleBannerSupplier, + LaunchMode launchMode, + boolean validateFilters) { Formatter formatter = null; boolean formatterWarning = false; @@ -516,7 +551,7 @@ private static Handler configureConsoleHandler(final ConsoleConfig config, Conso config.stderr ? ConsoleHandler.Target.SYSTEM_ERR : ConsoleHandler.Target.SYSTEM_OUT, formatter); consoleHandler.setLevel(config.level); consoleHandler.setErrorManager(defaultErrorManager); - consoleHandler.setFilter(cleanupFilter); + applyFilter(validateFilters, defaultErrorManager, cleanupFilter, config.filter, namedFilters, consoleHandler); Handler handler = config.async.enable ? createAsyncHandler(config.async, config.level, consoleHandler) : consoleHandler; @@ -554,7 +589,9 @@ public void close() throws SecurityException { } private static Handler configureFileHandler(final FileConfig config, final ErrorManager errorManager, - final LogCleanupFilter cleanupFilter, final List>> possibleFileFormatters) { + final LogCleanupFilter cleanupFilter, Map namedFilters, + final List>> possibleFileFormatters, + final boolean validateFilters) { FileHandler handler; FileConfig.RotationConfig rotationConfig = config.rotation; if (rotationConfig.fileSuffix.isPresent()) { @@ -596,6 +633,7 @@ private static Handler configureFileHandler(final FileConfig config, final Error handler.setErrorManager(errorManager); handler.setLevel(config.level); handler.setFilter(cleanupFilter); + applyFilter(validateFilters, errorManager, cleanupFilter, config.filter, namedFilters, handler); if (formatterWarning) { handler.getErrorManager().error("Multiple file formatters were activated", null, ErrorManager.GENERIC_FAILURE); @@ -607,9 +645,25 @@ private static Handler configureFileHandler(final FileConfig config, final Error return handler; } - private static Handler configureSyslogHandler(final SyslogConfig config, - final ErrorManager errorManager, - final LogCleanupFilter logCleanupFilter) { + private static void applyFilter(boolean validateFilters, ErrorManager errorManager, LogCleanupFilter cleanupFilter, + Optional filterName, Map namedFilters, Handler handler) { + if (filterName.isEmpty() || !validateFilters) { + handler.setFilter(cleanupFilter); + } else { + String name = filterName.get(); + Filter filter = namedFilters.get(name); + if (filter == null) { + errorManager.error("Unable to find named filter '" + name + "'", null, ErrorManager.GENERIC_FAILURE); + handler.setFilter(cleanupFilter); + } else { + handler.setFilter(new AllFilter(List.of(cleanupFilter, filter))); + } + } + } + + private static Handler configureSyslogHandler(final SyslogConfig config, final ErrorManager errorManager, + final LogCleanupFilter logCleanupFilter, + final Map namedFilters, final boolean validateFilters) { try { final SyslogHandler handler = new SyslogHandler(config.endpoint.getHostString(), config.endpoint.getPort()); handler.setAppName(config.appName.orElse(getProcessName())); @@ -625,6 +679,7 @@ private static Handler configureSyslogHandler(final SyslogConfig config, handler.setFormatter(formatter); handler.setErrorManager(errorManager); handler.setFilter(logCleanupFilter); + applyFilter(validateFilters, errorManager, logCleanupFilter, config.filter, namedFilters, handler); if (config.async.enable) { return createAsyncHandler(config.async, config.level, handler); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java index 38a3e49871c988..f0c64fc80f06fa 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/SyslogConfig.java @@ -89,6 +89,12 @@ public class SyslogConfig { @ConfigItem(defaultValue = "ALL") Level level; + /** + * The name of the filter to link to the file handler. + */ + @ConfigItem + Optional filter; + /** * Syslog async logging config */ diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index 60e877a48f30d5..f946ff505ef159 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -315,6 +315,42 @@ The console log handler is enabled by default. It outputs all log events to the For details of its configuration options, see link:#quarkus-log-logging-log-config_quarkus.log.console-console-logging[the Console Logging configuration reference]. +[TIP] +.Logging filters +==== +Log handlers (like the console log handler) can have a link:https://docs.oracle.com/en/java/javase/11/docs/api/java.logging/java/util/logging/Filter.html[filter] associated with them, whose +purpose is to determine whether a record should actually be logged or not. + +These filters are registered by placing the `@io.quarkus.logging.LoggingFilter` annotation on a (`final`) class that implements `java.util.logging.Filter` and setting the `name` property. + +Finally, the filter is attached using the `filter` configuration property of the appropriate handler. + +Let's say for example that we wanted to filter out logging records that contained the word `test` from the console logs. +We could write a filter like so: + +[source,java] +---- +import io.quarkus.logging.LoggingFilter; +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +@LoggingFilter(name = "my-filter") +public final class TestFilter implements Filter { + @Override + public boolean isLoggable(LogRecord record) { + return !record.getMessage().contains("test"); + } +} +---- + +And we would register this filter to the console handler like so: + +[source, properties] +---- +quarkus.log.console.filter=my-filter +---- +==== + === File log handler The file log handler is disabled by default. It outputs all log events to a file on the application's host. diff --git a/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/LoggingFilter.java b/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/LoggingFilter.java new file mode 100644 index 00000000000000..4410b4cdd7ee6f --- /dev/null +++ b/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/LoggingFilter.java @@ -0,0 +1,31 @@ +package io.quarkus.it.logging.minlevel.set.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import io.quarkus.it.logging.minlevel.set.LoggingWitness; + +@Path("/log/filter") +public class LoggingFilter { + + static final Logger LOG = Logger.getLogger(LoggingFilter.class); + + @GET + @Path("/filtered") + @Produces(MediaType.TEXT_PLAIN) + public boolean filtered() { + return LoggingWitness.loggedWarn("TEST warn message", LOG); + } + + @GET + @Path("/not-filtered") + @Produces(MediaType.TEXT_PLAIN) + public boolean notFiltered() { + return LoggingWitness.loggedWarn("warn message", LOG); + } + +} diff --git a/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/TestFilter.java b/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/TestFilter.java new file mode 100644 index 00000000000000..30163df4312409 --- /dev/null +++ b/integration-tests/logging-min-level-set/src/main/java/io/quarkus/it/logging/minlevel/set/filter/TestFilter.java @@ -0,0 +1,15 @@ +package io.quarkus.it.logging.minlevel.set.filter; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +import io.quarkus.logging.LoggingFilter; + +@LoggingFilter(name = "my-filter") +public final class TestFilter implements Filter { + + @Override + public boolean isLoggable(LogRecord record) { + return !record.getMessage().contains("TEST"); + } +} diff --git a/integration-tests/logging-min-level-set/src/main/resources/application.properties b/integration-tests/logging-min-level-set/src/main/resources/application.properties index 2e1c47781c2c49..e33158f666c731 100644 --- a/integration-tests/logging-min-level-set/src/main/resources/application.properties +++ b/integration-tests/logging-min-level-set/src/main/resources/application.properties @@ -3,3 +3,4 @@ quarkus.log.category."io.quarkus.it.logging.minlevel.set.above".min-level=INFO quarkus.log.category."io.quarkus.it.logging.minlevel.set.below".min-level=TRACE quarkus.log.category."io.quarkus.it.logging.minlevel.set.below.child".min-level=inherit quarkus.log.category."io.quarkus.it.logging.minlevel.set.promote".min-level=ERROR +quarkus.log.console.filter=my-filter diff --git a/integration-tests/logging-min-level-set/src/test/java/io/quarkus/it/logging/minlevel/set/LoggingFilterTest.java b/integration-tests/logging-min-level-set/src/test/java/io/quarkus/it/logging/minlevel/set/LoggingFilterTest.java new file mode 100644 index 00000000000000..2556243f760132 --- /dev/null +++ b/integration-tests/logging-min-level-set/src/test/java/io/quarkus/it/logging/minlevel/set/LoggingFilterTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.logging.minlevel.set; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource(SetRuntimeLogLevels.class) +public class LoggingFilterTest { + + @Test + public void testFiltered() { + given() + .when().get("/log/filter/filtered") + .then() + .statusCode(200) + .body(is("false")); + } + + @Test + public void testNotFiltered() { + given() + .when().get("/log/filter/not-filtered") + .then() + .statusCode(200) + .body(is("true")); + } + +}