Skip to content

Commit 9993e17

Browse files
vmuzikarshawkins
andauthored
Ability to specify log category levels through separate options (#35138)
Closes #34957 Co-authored-by: Steve Hawkins <[email protected]> Signed-off-by: Václav Muzikář <[email protected]>
1 parent 27eaaef commit 9993e17

28 files changed

+609
-75
lines changed

docs/documentation/release_notes/topics/26_1_0.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ See the Javadoc for a detailed description.
6161
In this release, admin events might hold additional details about the context when the event is fired. When upgrading you should
6262
expect the database schema being updated to add a new column `DETAILS_JSON` to the `ADMIN_EVENT_ENTITY` table.
6363

64+
= Individual options for category-specific log levels
65+
66+
It is now possible to set category-specific log levels as individual `log-level-category` options.
67+
68+
For more details, see the https://www.keycloak.org/server/logging#_configuring_levels_as_individual_options[Logging guide].
69+
6470
= Infinispan default XML configuration location
6571

6672
Previous releases ignored any change to `conf/cache-ispn.xml` if the `--cache-config-file` option was not provided.

docs/guides/server/logging.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ This example sets the following log levels:
6969
* The hibernate log level in general is set to debug.
7070
* 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.
7171
72+
==== Configuring levels as individual options
73+
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.
74+
This is useful when you want to set the log levels for selected categories without overwriting the previously set `log-level` option.
75+
76+
.Example
77+
If you start the server as:
78+
79+
<@kc.start parameters="--log-level=\"INFO,org.hibernate:debug\""/>
80+
81+
you can then set an environmental variable `KC_LOG_LEVEL_ORG_KEYCLOAK=trace` to change the log level for the `org.keycloak` category.
82+
83+
The `log-level-<category>` options take precedence over `log-level`. This allows you to override what was set in the `log-level` option.
84+
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`.
85+
86+
Bear in mind that when using the environmental variables, the category name must be in uppercase and the dots must be replaced with underscores.
87+
When using other config sources, the category name must be specified "as is", for example:
88+
89+
<@kc.start parameters="--log-level=\"INFO,org.hibernate:debug\" --log-level-org.keycloak=trace"/>
90+
7291
== Enabling log handlers
7392
To enable log handlers, enter the following command:
7493

quarkus/config-api/src/main/java/org/keycloak/config/LoggingOptions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ public String toString() {
6060
.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.")
6161
.build();
6262

63+
public static final Option<Level> LOG_LEVEL_CATEGORY = new OptionBuilder<>("log-level-<category>", Level.class)
64+
.category(OptionCategory.LOGGING)
65+
.description("The log level of a category. Takes precedence over the 'log-level' option.")
66+
.caseInsensitiveExpectedValues(true)
67+
.build();
68+
6369
public enum Output {
6470
DEFAULT,
6571
JSON;

quarkus/config-api/src/main/java/org/keycloak/config/Option.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import java.util.List;
44
import java.util.Optional;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
57
import java.util.stream.Collectors;
68

79
public class Option<T> {
10+
public static final Pattern WILDCARD_PLACEHOLDER_PATTERN = Pattern.compile("<.+>");
811

912
private final Class<T> type;
1013
private final String key;

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import java.util.function.BiConsumer;
4848
import java.util.function.Consumer;
4949
import java.util.function.Function;
50+
import java.util.stream.Collectors;
5051

5152
import org.keycloak.common.profile.ProfileException;
5253
import org.keycloak.config.DeprecatedMetadata;
@@ -101,16 +102,12 @@ private static class IncludeOptions {
101102
}
102103

103104
private ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
105+
private Set<PropertyMapper<?>> allowedMappers;
106+
private List<String> unrecognizedArgs = new ArrayList<>();
104107

105108
public void parseAndRun(List<String> cliArgs) {
106109
// perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled
107-
CommandLine cmd = createCommandLine(spec -> spec
108-
.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
109-
@Override
110-
public <T> T set(T value) throws Exception {
111-
return null; // just ignore
112-
}
113-
})));
110+
CommandLine cmd = createCommandLine(spec -> {}).setUnmatchedArgumentsAllowed(true);
114111
String[] argArray = cliArgs.toArray(new String[0]);
115112

116113
try {
@@ -157,6 +154,16 @@ private CommandLine createCommandLineForCommand(List<String> cliArgs, List<Comma
157154

158155
currentSpec = subCommand.getCommandSpec();
159156

157+
currentSpec.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
158+
@Override
159+
public <T> T set(T value) {
160+
if (value != null) {
161+
unrecognizedArgs.addAll(Arrays.asList((String[]) value));
162+
}
163+
return null; // doesn't matter
164+
}
165+
}));
166+
160167
addHelp(currentSpec);
161168
}
162169

@@ -326,6 +333,17 @@ private static boolean wasBuildEverRun() {
326333
* @param abstractCommand
327334
*/
328335
public void validateConfig(List<String> cliArgs, AbstractCommand abstractCommand) {
336+
unrecognizedArgs.removeIf(arg -> {
337+
if (arg.contains("=")) {
338+
arg = arg.substring(0, arg.indexOf("="));
339+
}
340+
PropertyMapper<?> mapper = PropertyMappers.getMapper(arg);
341+
return mapper != null && mapper.hasWildcard() && allowedMappers.contains(mapper);
342+
});
343+
if (!unrecognizedArgs.isEmpty()) {
344+
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), unrecognizedArgs);
345+
}
346+
329347
if (cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG) && !wasBuildEverRun()) {
330348
throw new PropertyException(Messages.optimizedUsedForFirstStartup());
331349
}
@@ -363,53 +381,54 @@ public void validateConfig(List<String> cliArgs, AbstractCommand abstractCommand
363381
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
364382
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
365383
for (PropertyMapper<?> mapper : mappers) {
366-
ConfigValue configValue = Configuration.getConfigValue(mapper.getFrom());
367-
String configValueStr = configValue.getValue();
384+
mapper.getKcConfigValues().forEach(configValue -> {
385+
String configValueStr = configValue.getValue();
368386

369-
// don't consider missing or anything below standard env properties
370-
if (configValueStr == null) {
371-
if (Environment.isRuntimeMode() && mapper.isEnabled() && mapper.isRequired()) {
372-
handleRequired(missingOption, mapper);
387+
// don't consider missing or anything below standard env properties
388+
if (configValueStr == null) {
389+
if (Environment.isRuntimeMode() && mapper.isEnabled() && mapper.isRequired()) {
390+
handleRequired(missingOption, mapper);
391+
}
392+
return;
373393
}
374-
continue;
375-
}
376-
if (!isUserModifiable(configValue)) {
377-
continue;
378-
}
379-
380-
if (disabledMappers.contains(mapper)) {
381-
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
382-
continue; // we found enabled mapper with the same name
394+
if (!isUserModifiable(configValue)) {
395+
return;
383396
}
384397

385-
// only check build-time for a rebuild, we'll check the runtime later
386-
if (!mapper.isRunTime() || !isRebuild()) {
387-
if (PropertyMapper.isCliOption(configValue)) {
388-
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
389-
} else {
390-
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
398+
if (disabledMappers.contains(mapper)) {
399+
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
400+
return; // we found enabled mapper with the same name
391401
}
402+
403+
// only check build-time for a rebuild, we'll check the runtime later
404+
if (!mapper.isRunTime() || !isRebuild()) {
405+
if (PropertyMapper.isCliOption(configValue)) {
406+
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
407+
} else {
408+
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
409+
}
410+
}
411+
return;
392412
}
393-
continue;
394-
}
395413

396-
if (mapper.isBuildTime() && !options.includeBuildTime) {
397-
String currentValue = getRawPersistedProperty(mapper.getFrom()).orElse(null);
398-
if (!configValueStr.equals(currentValue)) {
399-
ignoredBuildTime.add(mapper.getFrom());
400-
continue;
414+
if (mapper.isBuildTime() && !options.includeBuildTime) {
415+
String currentValue = getRawPersistedProperty(mapper.getFrom()).orElse(null);
416+
if (!configValueStr.equals(currentValue)) {
417+
ignoredBuildTime.add(mapper.getFrom());
418+
return;
419+
}
420+
}
421+
if (mapper.isRunTime() && !options.includeRuntime) {
422+
ignoredRunTime.add(mapper.getFrom());
423+
return;
401424
}
402-
}
403-
if (mapper.isRunTime() && !options.includeRuntime) {
404-
ignoredRunTime.add(mapper.getFrom());
405-
continue;
406-
}
407425

408-
mapper.validate(configValue);
426+
mapper.validate(configValue);
409427

410-
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
411-
handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata);
412-
});
428+
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
429+
handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata);
430+
});
431+
});;
413432
}
414433
}
415434

@@ -641,7 +660,7 @@ private static void addHelp(CommandSpec currentSpec) {
641660
}
642661
}
643662

644-
private static IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCommand abstractCommand, String commandName) {
663+
private IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCommand abstractCommand, String commandName) {
645664
IncludeOptions result = new IncludeOptions();
646665
if (abstractCommand == null) {
647666
return result;
@@ -659,7 +678,7 @@ private static IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCo
659678
return result;
660679
}
661680

662-
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
681+
private void addCommandOptions(List<String> cliArgs, CommandLine command) {
663682
if (command != null && command.getCommand() instanceof AbstractCommand) {
664683
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
665684

@@ -671,7 +690,7 @@ private static void addCommandOptions(List<String> cliArgs, CommandLine command)
671690
}
672691
}
673692

674-
private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
693+
private void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
675694
final Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
676695

677696
// Since we can't run sanitizeDisabledMappers sooner, PropertyMappers.getRuntime|BuildTimeMappers() at this point
@@ -685,6 +704,8 @@ private static void addOptionsToCli(CommandLine commandLine, IncludeOptions incl
685704
}
686705

687706
addMappedOptionsToArgGroups(commandLine, mappers);
707+
708+
allowedMappers = mappers.values().stream().flatMap(List::stream).collect(Collectors.toUnmodifiableSet());
688709
}
689710

690711
private static <T extends Map<OptionCategory, List<PropertyMapper<?>>>> void combinePropertyMappers(T origMappers, T additionalMappers) {

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
3939
@Override
4040
public void run() {
4141
doBeforeRun();
42-
HttpPropertyMappers.validateConfig();
43-
HostnameV2PropertyMappers.validateConfig();
4442
validateConfig();
4543

4644
if (isDevProfile()) {
@@ -56,6 +54,13 @@ protected void doBeforeRun() {
5654

5755
}
5856

57+
@Override
58+
protected void validateConfig() {
59+
super.validateConfig(); // we want to run the generic validation here first to check for unknown options
60+
HttpPropertyMappers.validateConfig();
61+
HostnameV2PropertyMappers.validateConfig();
62+
}
63+
5964
@Override
6065
public List<OptionCategory> getOptionCategories() {
6166
EnumSet<OptionCategory> excludedCategories = excludedCategories();

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ public void accept(String key, String value) {
129129
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
130130

131131
if (mapper != null) {
132+
mapper = mapper.forKey(key);
133+
132134
String to = mapper.getTo();
133135

134136
if (to != null) {

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KcEnvConfigSource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ private static Map<String, String> buildProperties() {
5050
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
5151

5252
if (mapper != null) {
53+
mapper = mapper.forEnvKey(key);
54+
5355
String to = mapper.getTo();
5456

5557
if (to != null) {

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222

2323
import io.smallrye.config.Priorities;
2424
import jakarta.annotation.Priority;
25+
import org.apache.commons.collections4.IteratorUtils;
2526
import org.apache.commons.collections4.iterators.FilterIterator;
2627
import org.keycloak.quarkus.runtime.Environment;
2728
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
2829
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
30+
import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper;
2931

3032
import java.util.Iterator;
33+
import java.util.List;
34+
import java.util.Set;
3135

3236
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
3337

@@ -49,7 +53,8 @@
4953
@Priority(Priorities.APPLICATION - 10)
5054
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
5155

52-
private static ThreadLocal<Boolean> disable = new ThreadLocal<>();
56+
private static final ThreadLocal<Boolean> disable = new ThreadLocal<>();
57+
private static final ThreadLocal<Boolean> disableAdditionalNames = new ThreadLocal<>();
5358

5459
public static void disable() {
5560
disable.set(true);
@@ -73,7 +78,26 @@ static boolean isRuntime(String name) {
7378

7479
@Override
7580
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
76-
return filterRuntime(context.iterateNames());
81+
// We need to iterate through names to get wildcard option names.
82+
// Additionally, wildcardValuesTransformer might also trigger iterateNames.
83+
// Hence we need to disable this to prevent infinite recursion.
84+
// But we don't want to disable the whole interceptor, as wildcardValuesTransformer
85+
// might still need mappers to work.
86+
List<String> mappedWildcardNames = List.of();
87+
if (!Boolean.TRUE.equals(disableAdditionalNames.get())) {
88+
disableAdditionalNames.set(true);
89+
try {
90+
mappedWildcardNames = PropertyMappers.getWildcardMappers().stream()
91+
.map(WildcardPropertyMapper::getToWithWildcards)
92+
.flatMap(Set::stream)
93+
.toList();
94+
} finally {
95+
disableAdditionalNames.remove();
96+
}
97+
}
98+
99+
// this could be optimized by filtering the wildcard names in the stream above
100+
return filterRuntime(IteratorUtils.chainedIterator(mappedWildcardNames.iterator(), context.iterateNames()));
77101
}
78102

79103
@Override

0 commit comments

Comments
 (0)