mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Ability to specify log category levels through separate options (#35138)
Closes #34957 Co-authored-by: Steve Hawkins <shawkins@redhat.com> Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
parent
27eaaefc4f
commit
9993e17346
@ -61,6 +61,12 @@ See the Javadoc for a detailed description.
|
||||
In this release, admin events might hold additional details about the context when the event is fired. When upgrading you should
|
||||
expect the database schema being updated to add a new column `DETAILS_JSON` to the `ADMIN_EVENT_ENTITY` table.
|
||||
|
||||
= Individual options for category-specific log levels
|
||||
|
||||
It is now possible to set category-specific log levels as individual `log-level-category` options.
|
||||
|
||||
For more details, see the https://www.keycloak.org/server/logging#_configuring_levels_as_individual_options[Logging guide].
|
||||
|
||||
= Infinispan default XML configuration location
|
||||
|
||||
Previous releases ignored any change to `conf/cache-ispn.xml` if the `--cache-config-file` option was not provided.
|
||||
|
||||
@ -69,6 +69,25 @@ This example sets the following log levels:
|
||||
* The hibernate log level in general is set to debug.
|
||||
* To keep SQL abstract syntax trees from creating verbose log output, the specific subcategory `org.hibernate.hql.internal.ast` is set to info. As a result, the SQL abstract syntax trees are omitted instead of appearing at the `debug` level.
|
||||
|
||||
==== Configuring levels as individual options
|
||||
When configuring category-specific log levels, you can also set the log levels as individual `log-level-<category>` options instead of using the `log-level` option for that.
|
||||
This is useful when you want to set the log levels for selected categories without overwriting the previously set `log-level` option.
|
||||
|
||||
.Example
|
||||
If you start the server as:
|
||||
|
||||
<@kc.start parameters="--log-level=\"INFO,org.hibernate:debug\""/>
|
||||
|
||||
you can then set an environmental variable `KC_LOG_LEVEL_ORG_KEYCLOAK=trace` to change the log level for the `org.keycloak` category.
|
||||
|
||||
The `log-level-<category>` options take precedence over `log-level`. This allows you to override what was set in the `log-level` option.
|
||||
For instance if you set `KC_LOG_LEVEL_ORG_HIBERNATE=trace` for the CLI example above, the `org.hibernate` category will use the `trace` level instead of `debug`.
|
||||
|
||||
Bear in mind that when using the environmental variables, the category name must be in uppercase and the dots must be replaced with underscores.
|
||||
When using other config sources, the category name must be specified "as is", for example:
|
||||
|
||||
<@kc.start parameters="--log-level=\"INFO,org.hibernate:debug\" --log-level-org.keycloak=trace"/>
|
||||
|
||||
== Enabling log handlers
|
||||
To enable log handlers, enter the following command:
|
||||
|
||||
|
||||
@ -60,6 +60,12 @@ public class LoggingOptions {
|
||||
.description("The log level of the root category or a comma-separated list of individual categories and their levels. For the root category, you don't need to specify a category.")
|
||||
.build();
|
||||
|
||||
public static final Option<Level> LOG_LEVEL_CATEGORY = new OptionBuilder<>("log-level-<category>", Level.class)
|
||||
.category(OptionCategory.LOGGING)
|
||||
.description("The log level of a category. Takes precedence over the 'log-level' option.")
|
||||
.caseInsensitiveExpectedValues(true)
|
||||
.build();
|
||||
|
||||
public enum Output {
|
||||
DEFAULT,
|
||||
JSON;
|
||||
|
||||
@ -2,9 +2,12 @@ package org.keycloak.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Option<T> {
|
||||
public static final Pattern WILDCARD_PLACEHOLDER_PATTERN = Pattern.compile("<.+>");
|
||||
|
||||
private final Class<T> type;
|
||||
private final String key;
|
||||
|
||||
@ -47,6 +47,7 @@ import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.common.profile.ProfileException;
|
||||
import org.keycloak.config.DeprecatedMetadata;
|
||||
@ -101,16 +102,12 @@ public class Picocli {
|
||||
}
|
||||
|
||||
private ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
|
||||
private Set<PropertyMapper<?>> allowedMappers;
|
||||
private List<String> unrecognizedArgs = new ArrayList<>();
|
||||
|
||||
public void parseAndRun(List<String> cliArgs) {
|
||||
// perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled
|
||||
CommandLine cmd = createCommandLine(spec -> spec
|
||||
.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
|
||||
@Override
|
||||
public <T> T set(T value) throws Exception {
|
||||
return null; // just ignore
|
||||
}
|
||||
})));
|
||||
CommandLine cmd = createCommandLine(spec -> {}).setUnmatchedArgumentsAllowed(true);
|
||||
String[] argArray = cliArgs.toArray(new String[0]);
|
||||
|
||||
try {
|
||||
@ -157,6 +154,16 @@ public class Picocli {
|
||||
|
||||
currentSpec = subCommand.getCommandSpec();
|
||||
|
||||
currentSpec.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
|
||||
@Override
|
||||
public <T> T set(T value) {
|
||||
if (value != null) {
|
||||
unrecognizedArgs.addAll(Arrays.asList((String[]) value));
|
||||
}
|
||||
return null; // doesn't matter
|
||||
}
|
||||
}));
|
||||
|
||||
addHelp(currentSpec);
|
||||
}
|
||||
|
||||
@ -326,6 +333,17 @@ public class Picocli {
|
||||
* @param abstractCommand
|
||||
*/
|
||||
public void validateConfig(List<String> cliArgs, AbstractCommand abstractCommand) {
|
||||
unrecognizedArgs.removeIf(arg -> {
|
||||
if (arg.contains("=")) {
|
||||
arg = arg.substring(0, arg.indexOf("="));
|
||||
}
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(arg);
|
||||
return mapper != null && mapper.hasWildcard() && allowedMappers.contains(mapper);
|
||||
});
|
||||
if (!unrecognizedArgs.isEmpty()) {
|
||||
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), unrecognizedArgs);
|
||||
}
|
||||
|
||||
if (cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG) && !wasBuildEverRun()) {
|
||||
throw new PropertyException(Messages.optimizedUsedForFirstStartup());
|
||||
}
|
||||
@ -363,53 +381,54 @@ public class Picocli {
|
||||
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
for (PropertyMapper<?> mapper : mappers) {
|
||||
ConfigValue configValue = Configuration.getConfigValue(mapper.getFrom());
|
||||
String configValueStr = configValue.getValue();
|
||||
mapper.getKcConfigValues().forEach(configValue -> {
|
||||
String configValueStr = configValue.getValue();
|
||||
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr == null) {
|
||||
if (Environment.isRuntimeMode() && mapper.isEnabled() && mapper.isRequired()) {
|
||||
handleRequired(missingOption, mapper);
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr == null) {
|
||||
if (Environment.isRuntimeMode() && mapper.isEnabled() && mapper.isRequired()) {
|
||||
handleRequired(missingOption, mapper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isUserModifiable(configValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (disabledMappers.contains(mapper)) {
|
||||
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
|
||||
continue; // we found enabled mapper with the same name
|
||||
if (!isUserModifiable(configValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only check build-time for a rebuild, we'll check the runtime later
|
||||
if (!mapper.isRunTime() || !isRebuild()) {
|
||||
if (PropertyMapper.isCliOption(configValue)) {
|
||||
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
|
||||
} else {
|
||||
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
|
||||
if (disabledMappers.contains(mapper)) {
|
||||
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
|
||||
return; // we found enabled mapper with the same name
|
||||
}
|
||||
|
||||
// only check build-time for a rebuild, we'll check the runtime later
|
||||
if (!mapper.isRunTime() || !isRebuild()) {
|
||||
if (PropertyMapper.isCliOption(configValue)) {
|
||||
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
|
||||
} else {
|
||||
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapper.isBuildTime() && !options.includeBuildTime) {
|
||||
String currentValue = getRawPersistedProperty(mapper.getFrom()).orElse(null);
|
||||
if (!configValueStr.equals(currentValue)) {
|
||||
ignoredBuildTime.add(mapper.getFrom());
|
||||
return;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapper.isBuildTime() && !options.includeBuildTime) {
|
||||
String currentValue = getRawPersistedProperty(mapper.getFrom()).orElse(null);
|
||||
if (!configValueStr.equals(currentValue)) {
|
||||
ignoredBuildTime.add(mapper.getFrom());
|
||||
continue;
|
||||
if (mapper.isRunTime() && !options.includeRuntime) {
|
||||
ignoredRunTime.add(mapper.getFrom());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mapper.isRunTime() && !options.includeRuntime) {
|
||||
ignoredRunTime.add(mapper.getFrom());
|
||||
continue;
|
||||
}
|
||||
|
||||
mapper.validate(configValue);
|
||||
mapper.validate(configValue);
|
||||
|
||||
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
|
||||
handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata);
|
||||
});
|
||||
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
|
||||
handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata);
|
||||
});
|
||||
});;
|
||||
}
|
||||
}
|
||||
|
||||
@ -641,7 +660,7 @@ public class Picocli {
|
||||
}
|
||||
}
|
||||
|
||||
private static IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCommand abstractCommand, String commandName) {
|
||||
private IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCommand abstractCommand, String commandName) {
|
||||
IncludeOptions result = new IncludeOptions();
|
||||
if (abstractCommand == null) {
|
||||
return result;
|
||||
@ -659,7 +678,7 @@ public class Picocli {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
|
||||
private void addCommandOptions(List<String> cliArgs, CommandLine command) {
|
||||
if (command != null && command.getCommand() instanceof AbstractCommand) {
|
||||
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
|
||||
|
||||
@ -671,7 +690,7 @@ public class Picocli {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
|
||||
private void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
|
||||
final Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
|
||||
|
||||
// Since we can't run sanitizeDisabledMappers sooner, PropertyMappers.getRuntime|BuildTimeMappers() at this point
|
||||
@ -685,6 +704,8 @@ public class Picocli {
|
||||
}
|
||||
|
||||
addMappedOptionsToArgGroups(commandLine, mappers);
|
||||
|
||||
allowedMappers = mappers.values().stream().flatMap(List::stream).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static <T extends Map<OptionCategory, List<PropertyMapper<?>>>> void combinePropertyMappers(T origMappers, T additionalMappers) {
|
||||
|
||||
@ -39,8 +39,6 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
|
||||
@Override
|
||||
public void run() {
|
||||
doBeforeRun();
|
||||
HttpPropertyMappers.validateConfig();
|
||||
HostnameV2PropertyMappers.validateConfig();
|
||||
validateConfig();
|
||||
|
||||
if (isDevProfile()) {
|
||||
@ -56,6 +54,13 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validateConfig() {
|
||||
super.validateConfig(); // we want to run the generic validation here first to check for unknown options
|
||||
HttpPropertyMappers.validateConfig();
|
||||
HostnameV2PropertyMappers.validateConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionCategory> getOptionCategories() {
|
||||
EnumSet<OptionCategory> excludedCategories = excludedCategories();
|
||||
|
||||
@ -129,6 +129,8 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
if (mapper != null) {
|
||||
mapper = mapper.forKey(key);
|
||||
|
||||
String to = mapper.getTo();
|
||||
|
||||
if (to != null) {
|
||||
|
||||
@ -50,6 +50,8 @@ public class KcEnvConfigSource extends PropertiesConfigSource {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
if (mapper != null) {
|
||||
mapper = mapper.forEnvKey(key);
|
||||
|
||||
String to = mapper.getTo();
|
||||
|
||||
if (to != null) {
|
||||
|
||||
@ -22,12 +22,16 @@ import io.smallrye.config.ConfigValue;
|
||||
|
||||
import io.smallrye.config.Priorities;
|
||||
import jakarta.annotation.Priority;
|
||||
import org.apache.commons.collections4.IteratorUtils;
|
||||
import org.apache.commons.collections4.iterators.FilterIterator;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
|
||||
@ -49,7 +53,8 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
@Priority(Priorities.APPLICATION - 10)
|
||||
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
|
||||
private static ThreadLocal<Boolean> disable = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Boolean> disable = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Boolean> disableAdditionalNames = new ThreadLocal<>();
|
||||
|
||||
public static void disable() {
|
||||
disable.set(true);
|
||||
@ -73,7 +78,26 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
|
||||
return filterRuntime(context.iterateNames());
|
||||
// We need to iterate through names to get wildcard option names.
|
||||
// Additionally, wildcardValuesTransformer might also trigger iterateNames.
|
||||
// Hence we need to disable this to prevent infinite recursion.
|
||||
// But we don't want to disable the whole interceptor, as wildcardValuesTransformer
|
||||
// might still need mappers to work.
|
||||
List<String> mappedWildcardNames = List.of();
|
||||
if (!Boolean.TRUE.equals(disableAdditionalNames.get())) {
|
||||
disableAdditionalNames.set(true);
|
||||
try {
|
||||
mappedWildcardNames = PropertyMappers.getWildcardMappers().stream()
|
||||
.map(WildcardPropertyMapper::getToWithWildcards)
|
||||
.flatMap(Set::stream)
|
||||
.toList();
|
||||
} finally {
|
||||
disableAdditionalNames.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// this could be optimized by filtering the wildcard names in the stream above
|
||||
return filterRuntime(IteratorUtils.chainedIterator(mappedWildcardNames.iterator(), context.iterateNames()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -5,7 +5,9 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.isTrue;
|
||||
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
@ -24,6 +26,7 @@ public final class LoggingPropertyMappers {
|
||||
private static final String CONSOLE_ENABLED_MSG = "Console log handler is activated";
|
||||
private static final String FILE_ENABLED_MSG = "File log handler is activated";
|
||||
private static final String SYSLOG_ENABLED_MSG = "Syslog is activated";
|
||||
private static final String DEFAULT_ROOT_LOG_LEVEL = toLevel(LoggingOptions.LOG_LEVEL.getDefaultValue().orElseThrow().get(0)).getName();
|
||||
|
||||
private LoggingPropertyMappers() {
|
||||
}
|
||||
@ -100,10 +103,18 @@ public final class LoggingPropertyMappers {
|
||||
// Log level
|
||||
fromOption(LoggingOptions.LOG_LEVEL)
|
||||
.to("quarkus.log.level")
|
||||
.transformer(LoggingPropertyMappers::resolveLogLevel)
|
||||
.transformer(LoggingPropertyMappers::resolveRootLogLevel)
|
||||
.validator(LoggingPropertyMappers::validateLogLevel)
|
||||
.paramLabel("category:level")
|
||||
.build(),
|
||||
fromOption(LoggingOptions.LOG_LEVEL_CATEGORY)
|
||||
.to("quarkus.log.category.\"<categories>\".level")
|
||||
.validator(LoggingPropertyMappers::validateCategoryLogLevel)
|
||||
.wildcardKeysTransformer(LoggingPropertyMappers::getConfiguredLogCategories)
|
||||
.transformer((v,c) -> toLevel(v).getName())
|
||||
.wildcardMapFrom(LoggingOptions.LOG_LEVEL, LoggingPropertyMappers::resolveCategoryLogLevelFromParentLogLevelOption) // a fallback to log-level
|
||||
.paramLabel("level")
|
||||
.build(),
|
||||
// Syslog
|
||||
fromOption(LoggingOptions.LOG_SYSLOG_ENABLED)
|
||||
.mapFrom(LoggingOptions.LOG, LoggingPropertyMappers.resolveLogHandler("syslog"))
|
||||
@ -189,10 +200,6 @@ public final class LoggingPropertyMappers {
|
||||
return LogContext.getLogContext().getLevelForName(categoryLevel.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private static void setCategoryLevel(String category, String level) {
|
||||
LogContext.getLogContext().getLogger(category).setLevel(toLevel(level));
|
||||
}
|
||||
|
||||
record CategoryLevel(String category, String levelName) {}
|
||||
|
||||
private static CategoryLevel validateLogLevel(String level) {
|
||||
@ -217,21 +224,54 @@ public final class LoggingPropertyMappers {
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveLogLevel(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) {
|
||||
String rootLevel = LoggingOptions.DEFAULT_LOG_LEVEL.name();
|
||||
|
||||
for (String level : value.split(",")) {
|
||||
var categoryLevel = validateLogLevel(level);
|
||||
private static String resolveRootLogLevel(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) {
|
||||
for (CategoryLevel categoryLevel : parseLogLevels(value)) {
|
||||
if (categoryLevel.category == null) {
|
||||
rootLevel = categoryLevel.levelName;
|
||||
} else {
|
||||
setCategoryLevel(categoryLevel.category, categoryLevel.levelName);
|
||||
return categoryLevel.levelName;
|
||||
}
|
||||
}
|
||||
return DEFAULT_ROOT_LOG_LEVEL; // defaults are not resolved in the mapper if transformer is present, so doing it explicitly here
|
||||
}
|
||||
|
||||
private static Set<String> getConfiguredLogCategories(Set<String> categories) {
|
||||
for (CategoryLevel categoryLevel : parseLogLevels(Configuration.getKcConfigValue("log-level").getValue())) {
|
||||
if (categoryLevel.category != null) {
|
||||
categories.add(categoryLevel.category);
|
||||
}
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static void validateCategoryLogLevel(String logLevel) {
|
||||
try {
|
||||
toLevel(logLevel);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new PropertyException(Messages.invalidLogLevel(logLevel));
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveCategoryLogLevelFromParentLogLevelOption(String category, String parentLogLevelValue, ConfigSourceInterceptorContext context) {
|
||||
String rootLevel = DEFAULT_ROOT_LOG_LEVEL;
|
||||
for (CategoryLevel categoryLevel : parseLogLevels(parentLogLevelValue)) {
|
||||
if (category.equals(categoryLevel.category)) {
|
||||
return categoryLevel.levelName;
|
||||
} else if (categoryLevel.category == null) {
|
||||
rootLevel = categoryLevel.levelName;
|
||||
}
|
||||
}
|
||||
return rootLevel;
|
||||
}
|
||||
|
||||
private static List<CategoryLevel> parseLogLevels(String value) {
|
||||
if (value == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return Stream.of(value.split(","))
|
||||
.map(LoggingPropertyMappers::validateLogLevel)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static String resolveLogOutput(String value, ConfigSourceInterceptorContext context) {
|
||||
boolean isDefault = LoggingOptions.DEFAULT_CONSOLE_OUTPUT.name().toLowerCase(Locale.ROOT).equals(value);
|
||||
return Boolean.valueOf(!isDefault).toString();
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR_CHAR;
|
||||
@ -27,10 +28,12 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
@ -38,23 +41,22 @@ import io.smallrye.config.ConfigValue;
|
||||
import io.smallrye.config.ConfigValue.ConfigValueBuilder;
|
||||
import io.smallrye.config.ExpressionConfigSourceInterceptor;
|
||||
import io.smallrye.config.Expressions;
|
||||
|
||||
import org.keycloak.config.DeprecatedMetadata;
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyException;
|
||||
import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler;
|
||||
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.configuration.KcEnvConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
public class PropertyMapper<T> {
|
||||
|
||||
private final Option<T> option;
|
||||
protected final Option<T> option;
|
||||
private final String to;
|
||||
private BooleanSupplier enabled;
|
||||
private String enabledWhen;
|
||||
@ -69,13 +71,21 @@ public class PropertyMapper<T> {
|
||||
private final String description;
|
||||
private final BooleanSupplier required;
|
||||
private final String requiredWhen;
|
||||
private final String from;
|
||||
|
||||
PropertyMapper(PropertyMapper<T> mapper, String from, String to, BiFunction<String, ConfigSourceInterceptorContext, String> parentMapper) {
|
||||
this(mapper.option, to, mapper.enabled, mapper.enabledWhen, mapper.mapper, mapper.mapFrom, parentMapper,
|
||||
mapper.paramLabel, mapper.mask, mapper.validator, mapper.description, mapper.required,
|
||||
mapper.requiredWhen, from);
|
||||
}
|
||||
|
||||
PropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
|
||||
BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
|
||||
String mapFrom, BiFunction<String, ConfigSourceInterceptorContext, String> parentMapper,
|
||||
String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator,
|
||||
String description, BooleanSupplier required, String requiredWhen) {
|
||||
String description, BooleanSupplier required, String requiredWhen, String from) {
|
||||
this.option = option;
|
||||
this.from = from == null ? MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey() : from;
|
||||
this.to = to == null ? getFrom() : to;
|
||||
this.enabled = enabled;
|
||||
this.enabledWhen = enabledWhen;
|
||||
@ -173,7 +183,7 @@ public class PropertyMapper<T> {
|
||||
}
|
||||
|
||||
public String getFrom() {
|
||||
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey();
|
||||
return from;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
@ -234,6 +244,14 @@ public class PropertyMapper<T> {
|
||||
return option.getDeprecatedMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* An option is considered a wildcard option if its key contains a wildcard placeholder (e.g. log-level-<category>).
|
||||
* The placeholder must be denoted by the '<' and '>' characters.
|
||||
*/
|
||||
public boolean hasWildcard() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private ConfigValue transformValue(String name, ConfigValue configValue, ConfigSourceInterceptorContext context, boolean parentValue) {
|
||||
String value = configValue.getValue();
|
||||
String mappedValue = value;
|
||||
@ -261,7 +279,7 @@ public class PropertyMapper<T> {
|
||||
}
|
||||
|
||||
// by unsetting the ordinal this will not be seen as directly modified by the user
|
||||
return configValue.from().withValue(mappedValue).withRawValue(value).withConfigSourceOrdinal(0).build();
|
||||
return configValue.from().withName(name).withValue(mappedValue).withRawValue(value).withConfigSourceOrdinal(0).build();
|
||||
}
|
||||
|
||||
private ConfigValue convertValue(ConfigValue configValue) {
|
||||
@ -272,6 +290,11 @@ public class PropertyMapper<T> {
|
||||
return configValue.withValue(ofNullable(configValue.getValue()).map(String::trim).orElse(null));
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ValueMapper {
|
||||
String map(String name, String value, ConfigSourceInterceptorContext context);
|
||||
}
|
||||
|
||||
private final class ContextWrapper implements ConfigSourceInterceptorContext {
|
||||
private final ConfigSourceInterceptorContext context;
|
||||
private final ConfigValue value;
|
||||
@ -315,6 +338,8 @@ public class PropertyMapper<T> {
|
||||
private String description;
|
||||
private BooleanSupplier isRequired = () -> false;
|
||||
private String requiredWhen = "";
|
||||
private Function<Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private ValueMapper wildcardMapFrom;
|
||||
|
||||
public Builder(Option<T> option) {
|
||||
this.option = option;
|
||||
@ -439,11 +464,30 @@ public class PropertyMapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> wildcardKeysTransformer(Function<Set<String>, Set<String>> wildcardValuesTransformer) {
|
||||
this.wildcardKeysTransformer = wildcardValuesTransformer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> wildcardMapFrom(Option<?> mapFrom, ValueMapper function) {
|
||||
this.mapFrom = mapFrom.getKey();
|
||||
this.wildcardMapFrom = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PropertyMapper<T> build() {
|
||||
if (paramLabel == null && Boolean.class.equals(option.getType())) {
|
||||
paramLabel = Boolean.TRUE + "|" + Boolean.FALSE;
|
||||
}
|
||||
return new PropertyMapper<>(option, to, isEnabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, isMasked, validator, description, isRequired, requiredWhen);
|
||||
// The wildcard pattern (e.g. log-level-<category>) is matching only a-z, 0-0 and dots. For env vars, dots are replaced by underscores.
|
||||
var fromWildcardMatcher = WILDCARD_PLACEHOLDER_PATTERN.matcher(option.getKey());
|
||||
if (fromWildcardMatcher.find()) {
|
||||
return new WildcardPropertyMapper<>(option, to, isEnabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, isMasked, validator, description, isRequired, requiredWhen, fromWildcardMatcher, wildcardKeysTransformer, wildcardMapFrom);
|
||||
}
|
||||
if (wildcardKeysTransformer != null || wildcardMapFrom != null) {
|
||||
throw new AssertionError("Wildcard operations not expected with non-wildcard mapper");
|
||||
}
|
||||
return new PropertyMapper<>(option, to, isEnabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, isMasked, validator, description, isRequired, requiredWhen, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,4 +569,32 @@ public class PropertyMapper<T> {
|
||||
KeycloakConfigSourceProvider.getConfigSourceDisplayName(configValue.getConfigSourceName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Keycloak config values for the mapper. A multivalued config option is a config option that
|
||||
* has a wildcard in its name, e.g. log-level-<category>.
|
||||
*
|
||||
* @return a list of config values where the key is the resolved wildcard (e.g. category) and the value is the config value
|
||||
*/
|
||||
public List<ConfigValue> getKcConfigValues() {
|
||||
return List.of(Configuration.getConfigValue(getFrom()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PropertyMapper tailored for the given env var key.
|
||||
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
|
||||
* wildcard key.
|
||||
*/
|
||||
public PropertyMapper<?> forEnvKey(String key) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PropertyMapper tailored for the given key.
|
||||
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
|
||||
* wildcard key.
|
||||
*/
|
||||
public PropertyMapper<?> forKey(String key) {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -83,7 +84,7 @@ public final class PropertyMappers {
|
||||
if (mapper == null) {
|
||||
return context.proceed(name);
|
||||
}
|
||||
return mapper.getConfigValue(name, context);
|
||||
return mapper.forKey(name).getConfigValue(name, context);
|
||||
}
|
||||
|
||||
public static boolean isSpiBuildTimeProperty(String name) {
|
||||
@ -178,6 +179,10 @@ public final class PropertyMappers {
|
||||
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static Set<WildcardPropertyMapper<?>> getWildcardMappers() {
|
||||
return MAPPERS.getWildcardMappers();
|
||||
}
|
||||
|
||||
public static boolean isSupported(PropertyMapper<?> mapper) {
|
||||
ConfigSupportLevel supportLevel = mapper.getCategory().getSupportLevel();
|
||||
return supportLevel.equals(ConfigSupportLevel.SUPPORTED) || supportLevel.equals(ConfigSupportLevel.DEPRECATED);
|
||||
@ -220,6 +225,7 @@ public final class PropertyMappers {
|
||||
|
||||
private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
|
||||
private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();
|
||||
private final Set<WildcardPropertyMapper<?>> wildcardMappers = new HashSet<>();
|
||||
|
||||
public void addAll(PropertyMapper<?>[] mappers) {
|
||||
for (PropertyMapper<?> mapper : mappers) {
|
||||
@ -238,10 +244,14 @@ public final class PropertyMappers {
|
||||
}
|
||||
|
||||
public void addMapper(PropertyMapper<?> mapper) {
|
||||
if (mapper.hasWildcard()) {
|
||||
wildcardMappers.add((WildcardPropertyMapper<?>)mapper);
|
||||
}
|
||||
handleMapper(mapper, this::add);
|
||||
}
|
||||
|
||||
public void removeMapper(PropertyMapper<?> mapper) {
|
||||
wildcardMappers.remove(mapper);
|
||||
handleMapper(mapper, this::remove);
|
||||
}
|
||||
|
||||
@ -252,6 +262,30 @@ public final class PropertyMappers {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PropertyMapper<?>> get(Object key) {
|
||||
// First check if the requested option matches any wildcard mappers
|
||||
String strKey = (String) key;
|
||||
List ret = wildcardMappers.stream()
|
||||
.filter(m -> m.matchesWildcardOptionName(strKey))
|
||||
.toList();
|
||||
if (!ret.isEmpty()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If no wildcard mappers match, check for exact matches
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PropertyMapper<?>> remove(Object mapper) {
|
||||
return super.remove(mapper);
|
||||
}
|
||||
|
||||
public Set<WildcardPropertyMapper<?>> getWildcardMappers() {
|
||||
return Collections.unmodifiableSet(wildcardMappers);
|
||||
}
|
||||
|
||||
public void sanitizeDisabledMappers() {
|
||||
if (Environment.getParsedCommand().isEmpty()) return; // do not sanitize when no command is present
|
||||
|
||||
|
||||
@ -0,0 +1,160 @@
|
||||
package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||
|
||||
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
|
||||
public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
|
||||
|
||||
private final Matcher fromWildcardMatcher;
|
||||
private final Pattern fromWildcardPattern;
|
||||
private final Pattern envVarNameWildcardPattern;
|
||||
private Matcher toWildcardMatcher;
|
||||
private Pattern toWildcardPattern;
|
||||
private final Function<Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private final ValueMapper wildcardMapFrom;
|
||||
|
||||
public WildcardPropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
|
||||
BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
|
||||
String mapFrom, BiFunction<String, ConfigSourceInterceptorContext, String> parentMapper,
|
||||
String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator,
|
||||
String description, BooleanSupplier required, String requiredWhen, Matcher fromWildcardMatcher, Function<Set<String>, Set<String>> wildcardKeysTransformer, ValueMapper wildcardMapFrom) {
|
||||
super(option, to, enabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, mask, validator, description, required, requiredWhen, null);
|
||||
this.wildcardMapFrom = wildcardMapFrom;
|
||||
this.fromWildcardMatcher = fromWildcardMatcher;
|
||||
// Includes handling for both "--" prefix for CLI options and "kc." prefix
|
||||
this.fromWildcardPattern = Pattern.compile("(?:" + ARG_PREFIX + "|kc\\.)" + fromWildcardMatcher.replaceFirst("([\\\\\\\\.a-zA-Z0-9]+)"));
|
||||
|
||||
// Not using toEnvVarFormat because it would process the whole string incl the <...> wildcard.
|
||||
Matcher envVarMatcher = WILDCARD_PLACEHOLDER_PATTERN.matcher(option.getKey().toUpperCase().replace("-", "_"));
|
||||
this.envVarNameWildcardPattern = Pattern.compile("KC_" + envVarMatcher.replaceFirst("([_A-Z0-9]+)"));
|
||||
|
||||
if (to != null) {
|
||||
toWildcardMatcher = WILDCARD_PLACEHOLDER_PATTERN.matcher(to);
|
||||
if (!toWildcardMatcher.find()) {
|
||||
throw new IllegalArgumentException("Attempted to map a wildcard option to a non-wildcard option");
|
||||
}
|
||||
|
||||
this.toWildcardPattern = Pattern.compile(toWildcardMatcher.replaceFirst("([\\\\\\\\.a-zA-Z0-9]+)"));
|
||||
}
|
||||
|
||||
this.wildcardKeysTransformer = wildcardKeysTransformer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasWildcard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
String getTo(String wildcardKey) {
|
||||
return toWildcardMatcher.replaceFirst(wildcardKey);
|
||||
}
|
||||
|
||||
String getFrom(String wildcardKey) {
|
||||
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + fromWildcardMatcher.replaceFirst(wildcardKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConfigValue> getKcConfigValues() {
|
||||
return this.getWildcardKeys().stream().map(v -> Configuration.getConfigValue(getFrom(v))).toList();
|
||||
}
|
||||
|
||||
public Set<String> getWildcardKeys() {
|
||||
// this is not optimal
|
||||
// TODO find an efficient way to get all values that match the wildcard
|
||||
Set<String> values = StreamSupport.stream(Configuration.getPropertyNames().spliterator(), false)
|
||||
.map(n -> getMappedKey(n, false))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (wildcardKeysTransformer != null) {
|
||||
return wildcardKeysTransformer.apply(values);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapped key for the given option name if a relevant mapping is available, or empty otherwise.
|
||||
* Currently, it only attempts to extract the wildcard key from the given option name.
|
||||
* E.g. for the option "log-level-<category>" and the option name "log-level-io.quarkus",
|
||||
* the wildcard value would be "io.quarkus".
|
||||
*/
|
||||
private Optional<String> getMappedKey(String originalKey, boolean tryTo) {
|
||||
Matcher matcher = fromWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
return Optional.of(matcher.group(1));
|
||||
}
|
||||
|
||||
if (tryTo && toWildcardPattern != null) {
|
||||
matcher = toWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
return Optional.of(matcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getToWithWildcards() {
|
||||
if (toWildcardMatcher == null) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
return getWildcardKeys().stream()
|
||||
.map(v -> toWildcardMatcher.replaceFirst(v))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given option name matches the wildcard pattern of this option.
|
||||
* E.g. check if "log-level-io.quarkus" matches the wildcard pattern "log-level-<category>".
|
||||
*/
|
||||
public boolean matchesWildcardOptionName(String name) {
|
||||
return fromWildcardPattern.matcher(name).matches() || envVarNameWildcardPattern.matcher(name).matches()
|
||||
|| (toWildcardPattern != null && toWildcardPattern.matcher(name).matches());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMapper<?> forEnvKey(String key) {
|
||||
Matcher matcher = envVarNameWildcardPattern.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalStateException("Env var '" + key + "' does not match the expected pattern '" + envVarNameWildcardPattern + "'");
|
||||
}
|
||||
String value = matcher.group(1);
|
||||
final String wildcardValue = value.toLowerCase().replace("_", "."); // we opiniotatedly convert env var names to CLI format with dots
|
||||
return forWildcardValue(wildcardValue);
|
||||
}
|
||||
|
||||
private PropertyMapper<?> forWildcardValue(final String wildcardValue) {
|
||||
String to = getTo(wildcardValue);
|
||||
String from = getFrom(wildcardValue);
|
||||
return new PropertyMapper<T>(this, from, to, wildcardMapFrom == null ? null : (v, context) -> wildcardMapFrom.map(wildcardValue, v, context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMapper<?> forKey(String key) {
|
||||
final String wildcardValue = getMappedKey(key, true).orElseThrow();
|
||||
return forWildcardValue(wildcardValue);
|
||||
}
|
||||
|
||||
}
|
||||
@ -386,4 +386,29 @@ public class PicocliTest extends AbstractConfigurationTest {
|
||||
assertEquals(Integer.MAX_VALUE, nonRunningPicocli.exitCode); // "running" state
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrongLevelForCategory() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-level-org.keycloak=wrong");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertTrue(nonRunningPicocli.getErrString().contains("Invalid log level: wrong. Possible values are: warn, trace, debug, error, fatal, info."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wildcardLevelForCategory() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-level-org.keycloak=warn");
|
||||
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
|
||||
var value = nonRunningPicocli.config.getConfigValue("quarkus.log.category.\"org.keycloak\".level");
|
||||
assertEquals("quarkus.log.category.\"org.keycloak\".level", value.getName());
|
||||
assertEquals("WARN", value.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wildcardLevelFromParent() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-level=org.keycloak:warn");
|
||||
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
|
||||
var value = nonRunningPicocli.config.getConfigValue("quarkus.log.category.\"org.keycloak\".level");
|
||||
assertEquals("quarkus.log.category.\"org.keycloak\".level", value.getName());
|
||||
assertEquals("WARN", value.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -532,4 +532,35 @@ public class ConfigurationTest extends AbstractConfigurationTest {
|
||||
assertEquals(Integer.toString(maxCount), config.getConfigValue(prop).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardCliOptionCanBeMappedToQuarkusOption() {
|
||||
ConfigArgsConfigSource.setCliArgs("--log-level-org.keycloak=trace");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("TRACE", config.getConfigValue("quarkus.log.category.\"org.keycloak\".level").getValue());
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.category.\"io.quarkus\".level").getValue());
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.category.\"foo.bar\".level").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardEnvVarOptionCanBeMappedToQuarkusOption() {
|
||||
putEnvVar("KC_LOG_LEVEL_IO_QUARKUS", "trace");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.category.\"org.keycloak\".level").getValue());
|
||||
assertEquals("TRACE", config.getConfigValue("quarkus.log.category.\"io.quarkus\".level").getValue());
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.category.\"foo.bar\".level").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardOptionFromConfigFile() {
|
||||
putEnvVar("SOME_CATEGORY_LOG_LEVEL", "debug");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("DEBUG", config.getConfigValue("quarkus.log.category.\"io.k8s\".level").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardPropertiesDontMatchEnvVarsFormat() {
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.category.\"io.quarkus\".level").getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,4 +184,26 @@ public class LoggingConfigurationTest extends AbstractConfigurationTest {
|
||||
"quarkus.log.file.level", "DEBUG"
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logLevelTakesPrecedenceOverCategoryLevel() {
|
||||
ConfigArgsConfigSource.setCliArgs("--log-level=org.keycloak:error");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.level").getValue());
|
||||
assertEquals("ERROR", config.getConfigValue("quarkus.log.category.\"org.keycloak\".level").getValue());
|
||||
|
||||
ConfigArgsConfigSource.setCliArgs("--log-level=org.keycloak:error", "--log-level-org.keycloak=trace");
|
||||
config = createConfig();
|
||||
assertEquals("INFO", config.getConfigValue("quarkus.log.level").getValue());
|
||||
assertEquals("TRACE", config.getConfigValue("quarkus.log.category.\"org.keycloak\".level").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownCategoryLevelIsResolvedFromRootLevel() {
|
||||
ConfigArgsConfigSource.setCliArgs("--log-level=warn,org.keycloak:error", "--log-level-org.keycloak=trace");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("WARN", config.getConfigValue("quarkus.log.level").getValue());
|
||||
assertEquals("TRACE", config.getConfigValue("quarkus.log.category.\"org.keycloak\".level").getValue());
|
||||
assertEquals("WARN", config.getConfigValue("quarkus.log.category.\"foo.bar\".level").getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefault.unittest}
|
||||
%user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest
|
||||
log-level=${SOME_LOG_LEVEL:info}
|
||||
log-level-io.k8s=${SOME_CATEGORY_LOG_LEVEL}
|
||||
KC_LOG_LEVEL_IO_QUARKUS=trace
|
||||
config-keystore=src/test/resources/keystore
|
||||
config-keystore-password=secret
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.config.LoggingOptions;
|
||||
import org.keycloak.it.junit5.extension.CLIResult;
|
||||
import org.keycloak.it.junit5.extension.DistributionTest;
|
||||
import org.keycloak.it.junit5.extension.DryRun;
|
||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||
import org.keycloak.it.utils.KeycloakDistribution;
|
||||
import org.keycloak.it.utils.RawDistRootPath;
|
||||
@ -53,7 +54,7 @@ public class LoggingDistTest {
|
||||
@Test
|
||||
@Launch({ "start-dev", "--log-level=warn" })
|
||||
void testSetRootLevel(CLIResult cliResult) {
|
||||
assertFalse(cliResult.getOutput().contains("INFO [io.quarkus]"));
|
||||
assertFalse(cliResult.getOutput().contains("INFO [io.quarkus]"));
|
||||
assertFalse(cliResult.getOutput().contains("Listening on:"));
|
||||
cliResult.assertStartedDevMode();
|
||||
}
|
||||
@ -168,10 +169,18 @@ public class LoggingDistTest {
|
||||
|
||||
@Test
|
||||
@Launch({"start-dev", "--log-console-level=wrong"})
|
||||
@DryRun
|
||||
void wrongLevelForHandlers(CLIResult cliResult) {
|
||||
cliResult.assertError("Invalid value for option '--log-console-level': wrong. Expected values are (case insensitive): off, fatal, error, warn, info, debug, trace, all");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start-dev", "--log-level-org.keycloak=wrong"})
|
||||
@DryRun
|
||||
void wrongLevelForCategory(CLIResult cliResult) {
|
||||
cliResult.assertError("Invalid log level: wrong. Possible values are: warn, trace, debug, error, fatal, info.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start-dev", "--log=console,file", "--log-console-level=debug", "--log-file-level=debug"})
|
||||
void levelRootDefault(CLIResult cliResult, RawDistRootPath path) {
|
||||
@ -217,14 +226,25 @@ public class LoggingDistTest {
|
||||
|
||||
// log contains DB migration status + build time logs
|
||||
assertThat(output, not(containsString("DEBUG [org.hibernate")));
|
||||
assertThat(output, not(containsString("INFO [org.keycloak")));
|
||||
assertThat(output, not(containsString("INFO [io.quarkus")));
|
||||
assertThat(output, not(containsString("INFO [org.keycloak")));
|
||||
assertThat(output, not(containsString("INFO [io.quarkus")));
|
||||
|
||||
var fileLog = readDefaultFileLog(path);
|
||||
assertThat(fileLog, notNullValue());
|
||||
assertTrue(fileLog.isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start-dev", "--log-level=error,org.keycloak:warn,org.hibernate:debug", "--log-level-org.keycloak=trace"})
|
||||
void categoryLogLevel(CLIResult cliResult) {
|
||||
var output = cliResult.getOutput();
|
||||
|
||||
assertThat(output, containsString("DEBUG [org.hibernate"));
|
||||
assertThat(output, not(containsString("TRACE [org.hibernate")));
|
||||
assertThat(output, containsString("TRACE [org.keycloak"));
|
||||
assertThat(output, not(containsString("INFO [io.quarkus")));
|
||||
}
|
||||
|
||||
protected static String readDefaultFileLog(RawDistRootPath path) {
|
||||
Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingOptions.DEFAULT_LOG_PATH);
|
||||
File logFile = new File(logFilePath.toString());
|
||||
|
||||
@ -142,6 +142,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
|
||||
Truststore:
|
||||
|
||||
|
||||
@ -167,6 +167,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
--log-syslog-app-name <name>
|
||||
Set the app name used when formatting the message in RFC5424 format. Default:
|
||||
keycloak. Available only when Syslog is activated.
|
||||
|
||||
@ -142,6 +142,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
|
||||
Truststore:
|
||||
|
||||
|
||||
@ -167,6 +167,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
--log-syslog-app-name <name>
|
||||
Set the app name used when formatting the message in RFC5424 format. Default:
|
||||
keycloak. Available only when Syslog is activated.
|
||||
|
||||
@ -308,6 +308,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
|
||||
Truststore:
|
||||
|
||||
|
||||
@ -368,6 +368,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
--log-syslog-app-name <name>
|
||||
Set the app name used when formatting the message in RFC5424 format. Default:
|
||||
keycloak. Available only when Syslog is activated.
|
||||
|
||||
@ -315,6 +315,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
|
||||
Truststore:
|
||||
|
||||
|
||||
@ -369,6 +369,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
--log-syslog-app-name <name>
|
||||
Set the app name used when formatting the message in RFC5424 format. Default:
|
||||
keycloak. Available only when Syslog is activated.
|
||||
|
||||
@ -266,6 +266,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
|
||||
Truststore:
|
||||
|
||||
|
||||
@ -320,6 +320,10 @@ Logging:
|
||||
The log level of the root category or a comma-separated list of individual
|
||||
categories and their levels. For the root category, you don't need to
|
||||
specify a category. Default: info.
|
||||
--log-level-<category> <level>
|
||||
The log level of a category. Takes precedence over the 'log-level' option.
|
||||
Possible values are (case insensitive): off, fatal, error, warn, info,
|
||||
debug, trace, all.
|
||||
--log-syslog-app-name <name>
|
||||
Set the app name used when formatting the message in RFC5424 format. Default:
|
||||
keycloak. Available only when Syslog is activated.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user