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:
Václav Muzikář 2024-12-11 17:27:44 +01:00 committed by GitHub
parent 27eaaefc4f
commit 9993e17346
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 610 additions and 76 deletions

View File

@ -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.

View File

@ -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:

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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();

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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());

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -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.