diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2016-11-29 13:48:00 +0100 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2016-12-01 12:29:40 +0100 |
commit | 1f9bb0b02f646739bfbdfe9dc9e8ed15c86e4e52 (patch) | |
tree | 2338a6b5d034cc174fd18cf36c72970d2004d337 /server/sonar-process | |
parent | 93ea99696bfed149fa1f46888e864b3bde4b4328 (diff) | |
download | sonarqube-1f9bb0b02f646739bfbdfe9dc9e8ed15c86e4e52.tar.gz sonarqube-1f9bb0b02f646739bfbdfe9dc9e8ed15c86e4e52.zip |
SONAR-8335 make changing root level change level of all loggers
Diffstat (limited to 'server/sonar-process')
5 files changed, 585 insertions, 101 deletions
diff --git a/server/sonar-process/src/main/java/org/sonar/process/logging/LogDomain.java b/server/sonar-process/src/main/java/org/sonar/process/logging/LogDomain.java new file mode 100644 index 00000000000..a4a19a64ca7 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/logging/LogDomain.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.logging; + +public enum LogDomain { + SQL("sql"), + ES_CLIENT("es"), + JMX("jmx"); + + private final String key; + + LogDomain(String key) { + this.key = key; + } + + public String getKey() { + return key; + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/logging/LogLevelConfig.java b/server/sonar-process/src/main/java/org/sonar/process/logging/LogLevelConfig.java new file mode 100644 index 00000000000..cbef4a4449b --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/logging/LogLevelConfig.java @@ -0,0 +1,133 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.logging; + +import ch.qos.logback.classic.Level; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.process.ProcessId; + +import static java.util.Objects.requireNonNull; +import static org.slf4j.Logger.ROOT_LOGGER_NAME; + +public final class LogLevelConfig { + private static final String SONAR_LOG_LEVEL_PROPERTY = "sonar.log.level"; + private static final String PROCESS_NAME_PLACEHOLDER = "XXXX"; + private static final String SONAR_PROCESS_LOG_LEVEL_PROPERTY = "sonar.log.level." + PROCESS_NAME_PLACEHOLDER; + + private final Map<String, List<String>> configuredByProperties; + private final Map<String, Level> configuredByHardcodedLevel; + + private LogLevelConfig(Builder builder) { + this.configuredByProperties = Collections.unmodifiableMap(builder.configuredByProperties); + this.configuredByHardcodedLevel = Collections.unmodifiableMap(builder.configuredByHardcodedLevel); + } + + Map<String, List<String>> getConfiguredByProperties() { + return configuredByProperties; + } + + Map<String, Level> getConfiguredByHardcodedLevel() { + return configuredByHardcodedLevel; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final Map<String, List<String>> configuredByProperties = new HashMap<>(); + private final Map<String, Level> configuredByHardcodedLevel = new HashMap<>(); + + private Builder() { + // use static factory method + } + + /** + * Configure the log level of the root logger to be read from the value of properties {@link #SONAR_LOG_LEVEL_PROPERTY} and + * {@link #SONAR_PROCESS_LOG_LEVEL_PROPERTY}. + */ + public Builder rootLevelFor(ProcessId processId) { + checkProcessId(processId); + + levelByProperty(ROOT_LOGGER_NAME, SONAR_LOG_LEVEL_PROPERTY, SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey())); + return this; + } + + /** + * Configure the log level of the logger with the specified name to be read from the value of properties + * {@code sonar.log.level}, {@code sonar.log.level.[process_name]} and {@code sonar.log.level.[process_name].[LogDomain#getKey()]}. + */ + public Builder levelByDomain(String loggerName, ProcessId processId, LogDomain domain) { + checkLoggerName(loggerName); + checkProcessId(processId); + requireNonNull(domain, "LogDomain can't be null"); + String processProperty = SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey()); + levelByProperty(loggerName, SONAR_LOG_LEVEL_PROPERTY, processProperty, processProperty + "." + domain.getKey()); + return this; + } + + private void levelByProperty(String loggerName, String property, String... otherProperties) { + ensureUniqueConfiguration(loggerName); + configuredByProperties.put(loggerName, Stream.concat(Stream.of(property), Arrays.stream(otherProperties)).collect(Collectors.toList())); + } + + /** + * Configure the log level of the logger with the specified name to be the specified one and it should never be + * changed. + */ + public Builder immutableLevel(String loggerName, Level level) { + checkLoggerName(loggerName); + requireNonNull(level, "level can't be null"); + ensureUniqueConfiguration(loggerName); + configuredByHardcodedLevel.put(loggerName, level); + return this; + } + + private void ensureUniqueConfiguration(String loggerName) { + if (configuredByProperties.containsKey(loggerName)) { + throw new IllegalStateException("Configuration by property already registered for " + loggerName); + } + if (configuredByHardcodedLevel.containsKey(loggerName)) { + throw new IllegalStateException("Configuration hardcoded level already registered for " + loggerName); + } + } + + private static void checkProcessId(ProcessId processId) { + requireNonNull(processId, "ProcessId can't be null"); + } + + private static void checkLoggerName(String loggerName) { + requireNonNull(loggerName, "loggerName can't be null"); + if (loggerName.isEmpty()) { + throw new IllegalArgumentException("loggerName can't be empty"); + } + } + + public LogLevelConfig build() { + return new LogLevelConfig(this); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java b/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java index 6d7589a618a..5a55bade6d3 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java +++ b/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java @@ -38,13 +38,11 @@ import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; import java.io.File; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Set; +import java.util.List; import javax.annotation.CheckForNull; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; import org.sonar.process.MessageException; -import org.sonar.process.ProcessId; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; @@ -59,8 +57,6 @@ public class LogbackHelper { private static final String ALL_LOGS_TO_CONSOLE_PROPERTY = "sonar.log.console"; private static final String PROCESS_NAME_PLACEHOLDER = "XXXX"; private static final String THREAD_ID_PLACEHOLDER = "ZZZZ"; - private static final String SONAR_LOG_LEVEL_PROPERTY = "sonar.log.level"; - private static final String SONAR_PROCESS_LOG_LEVEL_PROPERTY = "sonar.log.level." + PROCESS_NAME_PLACEHOLDER; private static final String ROLLING_POLICY_PROPERTY = "sonar.log.rollingPolicy"; private static final String MAX_FILES_PROPERTY = "sonar.log.maxFiles"; private static final String LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level " + PROCESS_NAME_PLACEHOLDER + "[" + THREAD_ID_PLACEHOLDER + "][%logger{20}] %msg%n"; @@ -96,6 +92,60 @@ public class LogbackHelper { return propagator; } + /** + * Applies the specified {@link LogLevelConfig} reading the specified {@link Props}. + * + * @throws IllegalArgumentException if the any level specified in a property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} + */ + public LoggerContext apply(LogLevelConfig logLevelConfig, Props props) { + LoggerContext rootContext = getRootContext(); + logLevelConfig.getConfiguredByProperties().entrySet().forEach(entry -> applyLevelByProperty(props, rootContext.getLogger(entry.getKey()), entry.getValue())); + logLevelConfig.getConfiguredByHardcodedLevel().entrySet().forEach(entry -> applyHardcodedLevel(rootContext, entry.getKey(), entry.getValue())); + return rootContext; + } + + private void applyLevelByProperty(Props props, Logger logger, List<String> properties) { + logger.setLevel(resolveLevel(props, properties.stream().toArray(String[]::new))); + } + + /** + * Resolve a log level reading the value of specified properties. + * <p> + * To compute the applied log level the following rules will be followed: + * <ul>the last property with a defined and valid value in the order of the {@code propertyKeys} argument will be applied</ul> + * <ul>if there is none, {@link Level#INFO INFO} will be returned</ul> + * </p> + * + * @throws IllegalArgumentException if the value of the specified property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} + */ + private static Level resolveLevel(Props props, String... propertyKeys) { + Level newLevel = Level.INFO; + for (String propertyKey : propertyKeys) { + Level level = getPropertyValueAsLevel(props, propertyKey); + if (level != null) { + newLevel = level; + } + } + return newLevel; + } + + private static void applyHardcodedLevel(LoggerContext rootContext, String loggerName, Level newLevel) { + rootContext.getLogger(loggerName).setLevel(newLevel); + } + + public void changeRoot(LogLevelConfig logLevelConfig, Level newLevel) { + ensureSupportedLevel(newLevel); + LoggerContext rootContext = getRootContext(); + rootContext.getLogger(ROOT_LOGGER_NAME).setLevel(newLevel); + logLevelConfig.getConfiguredByProperties().entrySet().forEach(entry -> rootContext.getLogger(entry.getKey()).setLevel(newLevel)); + } + + private static void ensureSupportedLevel(Level newLevel) { + if (!isAllowed(newLevel)) { + throw new IllegalArgumentException(format("%s log level is not supported (allowed levels are %s)", newLevel, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); + } + } + public String buildLogPattern(RootLoggerConfig config) { return LOG_FORMAT .replace(PROCESS_NAME_PLACEHOLDER, config.getProcessId().getKey()) @@ -176,38 +226,6 @@ public class LogbackHelper { return props.valueAsBoolean(ALL_LOGS_TO_CONSOLE_PROPERTY, false); } - /** - * Configure the log level of the root logger reading the value of property {@link #SONAR_LOG_LEVEL_PROPERTY}. - * - * @throws IllegalArgumentException if the value of {@link #SONAR_LOG_LEVEL_PROPERTY} is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} - */ - public Level configureRootLogLevel(Props props, ProcessId processId) { - Level newLevel = resolveLevel(props, SONAR_LOG_LEVEL_PROPERTY, SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey())); - getRootContext().getLogger(ROOT_LOGGER_NAME).setLevel(newLevel); - return newLevel; - } - - /** - * Resolve a log level reading the value of specified properties. - * <p> - * To compute the applied log level the following rules will be followed: - * <ul>the last property with a defined and valid value in the order of the {@code propertyKeys} argument will be applied</ul> - * <ul>if there is none, {@link Level#INFO INFO} will be returned</ul> - * </p> - * - * @throws IllegalArgumentException if the value of the specified property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} - */ - private static Level resolveLevel(Props props, String... propertyKeys) { - Level newLevel = Level.INFO; - for (String propertyKey : propertyKeys) { - Level level = getPropertyValueAsLevel(props, propertyKey); - if (level != null) { - newLevel = level; - } - } - return newLevel; - } - @CheckForNull private static Level getPropertyValueAsLevel(Props props, String propertyKey) { String value = props.value(propertyKey); @@ -232,50 +250,6 @@ public class LogbackHelper { return false; } - /** - * Configure the log level of the root logger to the specified level. - * - * @throws IllegalArgumentException if the specified level is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} - */ - public Level configureRootLogLevel(Level newLevel) { - Logger rootLogger = getRootContext().getLogger(ROOT_LOGGER_NAME); - ensureSupportedLevel(newLevel); - rootLogger.setLevel(newLevel); - return newLevel; - } - - private static void ensureSupportedLevel(Level newLevel) { - if (!isAllowed(newLevel)) { - throw new IllegalArgumentException(format("%s log level is not supported (allowed levels are %s)", newLevel, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); - } - } - - public Level configureLoggerLogLevelFromDomain(String loggerName, Props props, ProcessId processId, LogDomain domain) { - return configureLoggersLogLevelFromDomain(Collections.singleton(loggerName), props, processId, domain); - } - - public Level configureLoggersLogLevelFromDomain(Set<String> loggerNames, Props props, ProcessId processId, LogDomain domain) { - String processProperty = SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey()); - Level newLevel = resolveLevel(props, SONAR_LOG_LEVEL_PROPERTY, processProperty, processProperty + "." + domain.key); - loggerNames.forEach(loggerName -> { - Logger logger = getRootContext().getLogger(loggerName); - logger.setLevel(newLevel); - }); - return newLevel; - } - - /** - * Configure the log level of the specified logger to specified level. - * <p> - * Any level is allowed. - * </p> - */ - public Logger configureLogger(String loggerName, Level level) { - Logger logger = getRootContext().getLogger(loggerName); - logger.setLevel(level); - return logger; - } - public Level getLoggerLevel(String loggerName) { return getRootContext().getLogger(loggerName).getLevel(); } @@ -414,15 +388,4 @@ public class LogbackHelper { } } - public enum LogDomain { - SQL("sql"), - ES_CLIENT("es"), - JMX("jmx"); - - private final String key; - - LogDomain(String key) { - this.key = key; - } - } } diff --git a/server/sonar-process/src/test/java/org/sonar/process/logging/LogLevelConfigTest.java b/server/sonar-process/src/test/java/org/sonar/process/logging/LogLevelConfigTest.java new file mode 100644 index 00000000000..88025203b88 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/logging/LogLevelConfigTest.java @@ -0,0 +1,191 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.logging; + +import ch.qos.logback.classic.Level; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.process.ProcessId; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; +import static org.slf4j.Logger.ROOT_LOGGER_NAME; +import static org.sonar.process.logging.LogLevelConfig.newBuilder; + +public class LogLevelConfigTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private LogLevelConfig.Builder underTest = newBuilder(); + + @Test + public void build_can_create_empty_config_and_returned_maps_are_unmodifiable() { + LogLevelConfig underTest = newBuilder().build(); + + expectUnsupportedOperationException(() -> underTest.getConfiguredByProperties().put("1", Collections.emptyList())); + expectUnsupportedOperationException(() -> underTest.getConfiguredByHardcodedLevel().put("1", Level.ERROR)); + } + + @Test + public void builder_rootLevelFor_add_global_and_process_property_in_order_for_root_logger() { + LogLevelConfig underTest = newBuilder().rootLevelFor(ProcessId.ELASTICSEARCH).build(); + + assertThat(underTest.getConfiguredByProperties()).hasSize(1); + assertThat(underTest.getConfiguredByProperties().get(ROOT_LOGGER_NAME)) + .containsExactly("sonar.log.level", "sonar.log.level.es"); + assertThat(underTest.getConfiguredByHardcodedLevel()).hasSize(0); + } + + @Test + public void builder_rootLevelFor_fails_with_ProcessId_if_loggerName_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("ProcessId can't be null"); + + underTest.rootLevelFor(null); + } + + @Test + public void builder_rootLevelFor_fails_with_ISE_if_called_twice() { + underTest.rootLevelFor(ProcessId.ELASTICSEARCH); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Configuration by property already registered for " + ROOT_LOGGER_NAME); + + underTest.rootLevelFor(ProcessId.WEB_SERVER); + } + + @Test + public void builder_levelByDomain_fails_with_NPE_if_loggerName_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("loggerName can't be null"); + + underTest.levelByDomain(null, ProcessId.WEB_SERVER, LogDomain.JMX); + } + + @Test + public void builder_levelByDomain_fails_with_IAE_if_loggerName_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("loggerName can't be empty"); + + underTest.levelByDomain("", ProcessId.WEB_SERVER, LogDomain.JMX); + } + + @Test + public void builder_levelByDomain_fails_with_NPE_if_ProcessId_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("ProcessId can't be null"); + + underTest.levelByDomain("bar", null, LogDomain.JMX); + } + + @Test + public void builder_levelByDomain_fails_with_NPE_if_LogDomain_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("LogDomain can't be null"); + + underTest.levelByDomain("bar", ProcessId.WEB_SERVER, null); + } + + @Test + public void builder_levelByDomain_adds_global_process_and_domain_properties_in_order_for_specified_logger() { + LogLevelConfig underTest = newBuilder() + .levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.SQL) + .build(); + + assertThat(underTest.getConfiguredByProperties()).hasSize(1); + assertThat(underTest.getConfiguredByProperties().get("foo")) + .containsExactly("sonar.log.level", "sonar.log.level.web", "sonar.log.level.web.sql"); + assertThat(underTest.getConfiguredByHardcodedLevel()).hasSize(0); + } + + @Test + public void builder_levelByDomain_fails_with_ISE_if_loggerName_has_immutableLevel() { + underTest.immutableLevel("bar", Level.INFO); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Configuration hardcoded level already registered for bar"); + + underTest.levelByDomain("bar", ProcessId.WEB_SERVER, LogDomain.JMX); + } + + @Test + public void builder_immutableLevel_fails_with_NPE_if_logger_name_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("loggerName can't be null"); + + underTest.immutableLevel(null, Level.ERROR); + } + + @Test + public void builder_immutableLevel_fails_with_IAE_if_logger_name_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("loggerName can't be empty"); + + underTest.immutableLevel("", Level.ERROR); + } + + @Test + public void builder_immutableLevel_fails_with_NPE_if_level_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("level can't be null"); + + underTest.immutableLevel("foo", null); + } + + @Test + public void builder_immutableLevel_set_specified_level_for_specified_logger() { + LogLevelConfig config = underTest.immutableLevel("bar", Level.INFO).build(); + + assertThat(config.getConfiguredByProperties()).isEmpty(); + assertThat(config.getConfiguredByHardcodedLevel()).hasSize(1); + assertThat(config.getConfiguredByHardcodedLevel().get("bar")).isEqualTo(Level.INFO); + } + + @Test + public void builder_fails_with_ISE_if_immutableLevel_called_twice_for_same_logger() { + underTest.immutableLevel("foo", Level.INFO); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Configuration hardcoded level already registered for foo"); + + underTest.immutableLevel("foo", Level.DEBUG); + } + + @Test + public void builder_fails_with_ISE_if_logger_has_domain_config() { + underTest.levelByDomain("pop", ProcessId.WEB_SERVER, LogDomain.JMX); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Configuration by property already registered for pop"); + + underTest.immutableLevel("pop", Level.DEBUG); + } + + private static void expectUnsupportedOperationException(Runnable runnable) { + try { + runnable.run(); + fail("a UnsupportedOperationException should have been raised"); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnsupportedOperationException.class); + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java b/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java index 1037de3f863..023d8494875 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java @@ -20,7 +20,6 @@ package org.sonar.process.logging; import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.spi.LoggerContextListener; @@ -31,24 +30,34 @@ import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.io.File; import java.util.Properties; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; import org.sonar.process.MessageException; +import org.sonar.process.ProcessId; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; +import static org.slf4j.Logger.ROOT_LOGGER_NAME; +@RunWith(DataProviderRunner.class) public class LogbackHelperTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); private Props props = new Props(new Properties()); private LogbackHelper underTest = new LogbackHelper(); @@ -93,14 +102,6 @@ public class LogbackHelperTest { } @Test - public void configureLogger() { - Logger logger = underTest.configureLogger("my_logger", Level.WARN); - - assertThat(logger.getLevel()).isEqualTo(Level.WARN); - assertThat(logger.getName()).isEqualTo("my_logger"); - } - - @Test public void createRollingPolicy_defaults() { LoggerContext ctx = underTest.getRootContext(); LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); @@ -170,4 +171,164 @@ public class LogbackHelperTest { assertThat(e).hasMessage("Unsupported value for property sonar.log.rollingPolicy: unknown:foo"); } } + + @Test + public void apply_fails_with_IAE_if_global_property_has_unsupported_level() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + + props.set("sonar.log.level", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.apply(config, props); + } + + @Test + public void apply_fails_with_IAE_if_process_property_has_unsupported_level() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + + props.set("sonar.log.level.web", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.apply(config, props); + } + + @Test + public void apply_sets_logger_to_INFO_if_no_property_is_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.INFO); + } + + @Test + public void apply_sets_logger_to_globlal_property_if_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + + props.set("sonar.log.level", "TRACE"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.TRACE); + } + + @Test + public void apply_sets_logger_to_process_property_if_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + + props.set("sonar.log.level.web", "DEBUG"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.DEBUG); + } + + @Test + public void apply_sets_logger_to_process_property_over_global_property_if_both_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().rootLevelFor(ProcessId.WEB_SERVER).build(); + props.set("sonar.log.level", "DEBUG"); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.TRACE); + } + + @Test + public void apply_sets_domain_property_over_process_and_global_property_if_all_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES_CLIENT).build(); + props.set("sonar.log.level", "DEBUG"); + props.set("sonar.log.level.web", "DEBUG"); + props.set("sonar.log.level.web.es", "TRACE"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger("foo").getLevel()).isEqualTo(Level.TRACE); + } + + @Test + public void apply_sets_domain_property_over_process_property_if_both_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES_CLIENT).build(); + props.set("sonar.log.level.web", "DEBUG"); + props.set("sonar.log.level.web.es", "TRACE"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger("foo").getLevel()).isEqualTo(Level.TRACE); + } + + @Test + public void apply_sets_domain_property_over_global_property_if_both_set() { + LogLevelConfig config = LogLevelConfig.newBuilder().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES_CLIENT).build(); + props.set("sonar.log.level", "DEBUG"); + props.set("sonar.log.level.web.es", "TRACE"); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger("foo").getLevel()).isEqualTo(Level.TRACE); + } + + @Test + public void apply_fails_with_IAE_if_domain_property_has_unsupported_level() { + LogLevelConfig config = LogLevelConfig.newBuilder().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.JMX).build(); + + props.set("sonar.log.level.web.jmx", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web.jmx is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.apply(config, props); + } + + @Test + @UseDataProvider("logbackLevels") + public void apply_accepts_any_level_as_hardcoded_level(Level level) { + LogLevelConfig config = LogLevelConfig.newBuilder().immutableLevel("bar", level).build(); + + LoggerContext context = underTest.apply(config, props); + + assertThat(context.getLogger("bar").getLevel()).isEqualTo(level); + } + + @Test + public void changeRoot_sets_level_of_ROOT_and_all_loggers_with_a_config_but_the_hardcoded_one() { + LogLevelConfig config = LogLevelConfig.newBuilder() + .rootLevelFor(ProcessId.WEB_SERVER) + .levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.JMX) + .levelByDomain("bar", ProcessId.COMPUTE_ENGINE, LogDomain.ES_CLIENT) + .immutableLevel("doh", Level.ERROR) + .immutableLevel("pif", Level.TRACE) + .build(); + LoggerContext context = underTest.apply(config, props); + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.INFO); + assertThat(context.getLogger("foo").getLevel()).isEqualTo(Level.INFO); + assertThat(context.getLogger("bar").getLevel()).isEqualTo(Level.INFO); + assertThat(context.getLogger("doh").getLevel()).isEqualTo(Level.ERROR); + assertThat(context.getLogger("pif").getLevel()).isEqualTo(Level.TRACE); + + underTest.changeRoot(config, Level.DEBUG); + + assertThat(context.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.DEBUG); + assertThat(context.getLogger("foo").getLevel()).isEqualTo(Level.DEBUG); + assertThat(context.getLogger("bar").getLevel()).isEqualTo(Level.DEBUG); + assertThat(context.getLogger("doh").getLevel()).isEqualTo(Level.ERROR); + assertThat(context.getLogger("pif").getLevel()).isEqualTo(Level.TRACE); + } + + @DataProvider + public static Object[][] logbackLevels() { + return new Object[][] { + {Level.OFF}, + {Level.ERROR}, + {Level.WARN}, + {Level.INFO}, + {Level.DEBUG}, + {Level.TRACE}, + {Level.ALL} + }; + } } |