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..20ec59091f8156 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; @@ -79,6 +87,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 +100,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 +125,12 @@ 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."; + @BuildStep void setupLogFilters(BuildProducer filters) { filters.produce(new LogCleanupFilterBuildItem("org.jboss.threads", "JBoss Threads version")); @@ -204,6 +221,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, @@ -246,7 +264,8 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe context.registerSubstitution(InheritableLevel.ActualLevel.class, String.class, InheritableLevel.Substitution.class); context.registerSubstitution(InheritableLevel.Inherited.class, String.class, InheritableLevel.Substitution.class); shutdownListenerBuildItemBuildProducer.produce(new ShutdownListenerBuildItem( - recorder.initializeLogging(log, buildLog, categoryMinLevelDefaults.content, alwaysEnableLogStream, + recorder.initializeLogging(log, buildLog, discoverLogComponents(combinedIndexBuildItem.getIndex()), + categoryMinLevelDefaults.content, alwaysEnableLogStream, devUiLogHandler, handlers, namedHandlers, consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue) .collect(Collectors.toList()), @@ -276,6 +295,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..ecf2bb5b260f03 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 names 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..fea3cf55d62cc3 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 names 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..a35c9105e3470c 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,6 +76,7 @@ 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(), @@ -83,6 +86,7 @@ public static void handleFailedStart(RuntimeValue>> ba } public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig buildConfig, + DiscoveredLogComponents discoveredLogComponents, final Map categoryDefaultMinLevels, final boolean enableWebStream, final RuntimeValue> devUiConsoleHandler, @@ -126,12 +130,14 @@ 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, + final Handler consoleHandler = configureConsoleHandler(false, config.console, consoleRuntimeConfig.getValue(), + errorManager, cleanupFiler, namedFilters, possibleConsoleFormatters, possibleBannerSupplier, launchMode); errorManager = consoleHandler.getErrorManager(); handlers.add(consoleHandler); @@ -157,11 +163,12 @@ public void close() throws SecurityException { if (config.file.enable) { handlers.add( - configureFileHandler(config.file, errorManager, cleanupFiler, possibleFileFormatters)); + configureFileHandler(false, config.file, errorManager, cleanupFiler, namedFilters, possibleFileFormatters)); } if (config.syslog.enable) { - final Handler syslogHandler = configureSyslogHandler(config.syslog, errorManager, cleanupFiler); + final Handler syslogHandler = configureSyslogHandler(false, config.syslog, errorManager, cleanupFiler, + namedFilters); if (syslogHandler != null) { handlers.add(syslogHandler); } @@ -184,8 +191,8 @@ public void close() throws SecurityException { } Map namedHandlers = shouldCreateNamedHandlers(config, additionalNamedHandlers) - ? createNamedHandlers(config, consoleRuntimeConfig.getValue(), additionalNamedHandlers, - possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, launchMode, + ? createNamedHandlers(false, config, consoleRuntimeConfig.getValue(), additionalNamedHandlers, + possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, namedFilters, launchMode, shutdownNotifier) : Collections.emptyMap(); if (!categories.isEmpty()) { @@ -238,6 +245,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. @@ -266,16 +294,16 @@ public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConf final ArrayList handlers = new ArrayList<>(3); if (config.console.enable) { - final Handler consoleHandler = configureConsoleHandler(config.console, consoleConfig, errorManager, + final Handler consoleHandler = configureConsoleHandler(true, config.console, consoleConfig, errorManager, logCleanupFilter, - Collections.emptyList(), new RuntimeValue<>(Optional.empty()), launchMode); + Collections.emptyMap(), Collections.emptyList(), new RuntimeValue<>(Optional.empty()), launchMode); errorManager = consoleHandler.getErrorManager(); handlers.add(consoleHandler); } - Map namedHandlers = createNamedHandlers(config, consoleConfig, Collections.emptyList(), + Map namedHandlers = createNamedHandlers(true, config, consoleConfig, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), errorManager, logCleanupFilter, launchMode, dummy); + Collections.emptyList(), errorManager, logCleanupFilter, Collections.emptyMap(), launchMode, dummy); for (Map.Entry entry : categories.entrySet()) { final String categoryName = entry.getKey(); @@ -352,12 +380,15 @@ public static InheritableLevel getLogLevelNoInheritance(String categoryName, return inheritableLevel; } - private static Map createNamedHandlers(LogConfig config, ConsoleRuntimeConfig consoleRuntimeConfig, + private static Map createNamedHandlers(boolean isBuildTimeSetup, LogConfig config, + ConsoleRuntimeConfig consoleRuntimeConfig, List>> additionalNamedHandlers, List>> possibleConsoleFormatters, List>> possibleFileFormatters, ErrorManager errorManager, - LogCleanupFilter cleanupFilter, LaunchMode launchMode, + LogCleanupFilter cleanupFilter, + Map namedFilters, + LaunchMode launchMode, ShutdownNotifier shutdownHandler) { Map namedHandlers = new HashMap<>(); for (Entry consoleConfigEntry : config.consoleHandlers.entrySet()) { @@ -365,9 +396,10 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedConsoleConfig.enable) { continue; } - final Handler consoleHandler = configureConsoleHandler(namedConsoleConfig, consoleRuntimeConfig, errorManager, + final Handler consoleHandler = configureConsoleHandler(isBuildTimeSetup, namedConsoleConfig, consoleRuntimeConfig, + errorManager, cleanupFilter, - possibleConsoleFormatters, null, launchMode); + namedFilters, possibleConsoleFormatters, null, launchMode); addToNamedHandlers(namedHandlers, consoleHandler, consoleConfigEntry.getKey()); } for (Entry fileConfigEntry : config.fileHandlers.entrySet()) { @@ -375,7 +407,8 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedFileConfig.enable) { continue; } - final Handler fileHandler = configureFileHandler(namedFileConfig, errorManager, cleanupFilter, + final Handler fileHandler = configureFileHandler(isBuildTimeSetup, namedFileConfig, errorManager, cleanupFilter, + namedFilters, possibleFileFormatters); addToNamedHandlers(namedHandlers, fileHandler, fileConfigEntry.getKey()); } @@ -384,7 +417,8 @@ private static Map createNamedHandlers(LogConfig config, Consol if (!namedSyslogConfig.enable) { continue; } - final Handler syslogHandler = configureSyslogHandler(namedSyslogConfig, errorManager, cleanupFilter); + final Handler syslogHandler = configureSyslogHandler(isBuildTimeSetup, namedSyslogConfig, errorManager, + cleanupFilter, namedFilters); if (syslogHandler != null) { addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey()); } @@ -471,9 +505,11 @@ public void initializeLoggingForImageBuild() { } } - private static Handler configureConsoleHandler(final ConsoleConfig config, ConsoleRuntimeConfig consoleRuntimeConfig, + private static Handler configureConsoleHandler(final boolean isBuildTimeSetup, final ConsoleConfig config, + ConsoleRuntimeConfig consoleRuntimeConfig, final ErrorManager defaultErrorManager, final LogCleanupFilter cleanupFilter, + final Map namedFilters, final List>> possibleFormatters, final RuntimeValue>> possibleBannerSupplier, LaunchMode launchMode) { Formatter formatter = null; @@ -516,7 +552,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(consoleHandler, isBuildTimeSetup, defaultErrorManager, cleanupFilter, config.filter, namedFilters); Handler handler = config.async.enable ? createAsyncHandler(config.async, config.level, consoleHandler) : consoleHandler; @@ -553,8 +589,10 @@ public void close() throws SecurityException { return handler; } - private static Handler configureFileHandler(final FileConfig config, final ErrorManager errorManager, - final LogCleanupFilter cleanupFilter, final List>> possibleFileFormatters) { + private static Handler configureFileHandler(final boolean isBuildTimeSetup, final FileConfig config, + final ErrorManager errorManager, + final LogCleanupFilter cleanupFilter, Map namedFilters, + final List>> possibleFileFormatters) { FileHandler handler; FileConfig.RotationConfig rotationConfig = config.rotation; if (rotationConfig.fileSuffix.isPresent()) { @@ -596,6 +634,7 @@ private static Handler configureFileHandler(final FileConfig config, final Error handler.setErrorManager(errorManager); handler.setLevel(config.level); handler.setFilter(cleanupFilter); + applyFilter(handler, isBuildTimeSetup, errorManager, cleanupFilter, config.filter, namedFilters); if (formatterWarning) { handler.getErrorManager().error("Multiple file formatters were activated", null, ErrorManager.GENERIC_FAILURE); @@ -607,9 +646,27 @@ private static Handler configureFileHandler(final FileConfig config, final Error return handler; } - private static Handler configureSyslogHandler(final SyslogConfig config, + private static void applyFilter(Handler handler, boolean isBuildTimeSetup, ErrorManager errorManager, + LogCleanupFilter cleanupFilter, + Optional filterName, + Map namedFilters) { + if (filterName.isEmpty() || isBuildTimeSetup) { + 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 boolean isBuildTimeSetup, final SyslogConfig config, final ErrorManager errorManager, - final LogCleanupFilter logCleanupFilter) { + final LogCleanupFilter logCleanupFilter, final Map namedFilters) { try { final SyslogHandler handler = new SyslogHandler(config.endpoint.getHostString(), config.endpoint.getPort()); handler.setAppName(config.appName.orElse(getProcessName())); @@ -625,6 +682,7 @@ private static Handler configureSyslogHandler(final SyslogConfig config, handler.setFormatter(formatter); handler.setErrorManager(errorManager); handler.setFilter(logCleanupFilter); + applyFilter(handler, isBuildTimeSetup, errorManager, logCleanupFilter, config.filter, namedFilters); 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..012540aac6f883 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 names of the filter to link to the file handler. + */ + @ConfigItem + Optional filter; + /** * Syslog async logging config */