@@ -37,13 +37,13 @@ import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; | |||
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; | |||
import java.io.File; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.slf4j.LoggerFactory; | |||
import static java.util.Collections.unmodifiableSet; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
@@ -60,7 +60,11 @@ public class LogbackHelper { | |||
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"; | |||
public static final Set<Level> ALLOWED_ROOT_LOG_LEVELS = unmodifiableSet(setOf(Level.TRACE, Level.DEBUG, Level.INFO)); | |||
private static final Level[] ALLOWED_ROOT_LOG_LEVELS = new Level[] {Level.TRACE, Level.DEBUG, Level.INFO}; | |||
public static Collection<Level> allowedLogLevels() { | |||
return Arrays.asList(ALLOWED_ROOT_LOG_LEVELS); | |||
} | |||
public LoggerContext getRootContext() { | |||
org.slf4j.Logger logger; | |||
@@ -165,8 +169,8 @@ public class LogbackHelper { | |||
public String buildLogPattern(LogbackHelper.RootLoggerConfig config) { | |||
return LOG_FORMAT | |||
.replace(PROCESS_NAME_PLACEHOLDER, config.getProcessName()) | |||
.replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern()); | |||
.replace(PROCESS_NAME_PLACEHOLDER, config.getProcessName()) | |||
.replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern()); | |||
} | |||
/** | |||
@@ -257,8 +261,8 @@ public class LogbackHelper { | |||
*/ | |||
public Level configureRootLogLevel(Props props, ProcessId processId) { | |||
return configureRootLogLevel(props, | |||
SONAR_LOG_LEVEL_PROPERTY, | |||
SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey())); | |||
SONAR_LOG_LEVEL_PROPERTY, | |||
SONAR_PROCESS_LOG_LEVEL_PROPERTY.replace(PROCESS_NAME_PLACEHOLDER, processId.getKey())); | |||
} | |||
/** | |||
@@ -268,14 +272,37 @@ public class LogbackHelper { | |||
* @throws IllegalArgumentException if the value of the specified property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} | |||
*/ | |||
public Level configureRootLogLevel(Props props, String... propertyKeys) { | |||
return configureLoggerLogLevel(getRootContext().getLogger(ROOT_LOGGER_NAME), props, propertyKeys); | |||
} | |||
private Level configureLoggerLogLevel(Logger logger, Props props, String... propertyKeys) { | |||
Level newLevel = Level.INFO; | |||
for (String propertyKey : propertyKeys) { | |||
Level level = Level.toLevel(props.value(propertyKey, Level.INFO.toString()), Level.INFO); | |||
Level level = getPropertyValueAsLevel(props, propertyKey); | |||
if (!level.isGreaterOrEqual(newLevel)) { | |||
newLevel = level; | |||
} | |||
} | |||
return configureRootLogLevel(newLevel); | |||
logger.setLevel(newLevel); | |||
return newLevel; | |||
} | |||
private static Level getPropertyValueAsLevel(Props props, String propertyKey) { | |||
Level level = Level.toLevel(props.value(propertyKey, Level.INFO.toString()), Level.INFO); | |||
if (!isAllowed(level)) { | |||
throw new IllegalArgumentException(String.format("log level %s in property %s is not a supported value (allowed levels are %s)", | |||
level, propertyKey, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); | |||
} | |||
return level; | |||
} | |||
private static boolean isAllowed(Level level) { | |||
for (Level allowedRootLogLevel : ALLOWED_ROOT_LOG_LEVELS) { | |||
if (level == allowedRootLogLevel) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
/** | |||
@@ -285,13 +312,17 @@ public class LogbackHelper { | |||
*/ | |||
public Level configureRootLogLevel(Level newLevel) { | |||
Logger rootLogger = getRootContext().getLogger(ROOT_LOGGER_NAME); | |||
if (!ALLOWED_ROOT_LOG_LEVELS.contains(newLevel)) { | |||
throw new IllegalArgumentException(String.format("%s log level is not supported (allowed levels are %s)", newLevel, ALLOWED_ROOT_LOG_LEVELS)); | |||
} | |||
ensureSupportedLevel(newLevel); | |||
rootLogger.setLevel(newLevel); | |||
return newLevel; | |||
} | |||
private static void ensureSupportedLevel(Level newLevel) { | |||
if (!isAllowed(newLevel)) { | |||
throw new IllegalArgumentException(String.format("%s log level is not supported (allowed levels are %s)", newLevel, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); | |||
} | |||
} | |||
/** | |||
* Configure the log level of the specified logger to specified level. | |||
* <p> |
@@ -39,7 +39,7 @@ public class SearchLogging { | |||
helper.configureGlobalFileLog(props, config, logPattern); | |||
helper.configureForSubprocessGobbler(props, logPattern); | |||
// SQ's global log level must not change ES's log level | |||
helper.configureRootLogLevel(props, "sonar.log.es.level"); | |||
helper.configureRootLogLevel(props, "sonar.log.level.es"); | |||
return ctx; | |||
} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.search; | |||
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; | |||
@@ -38,6 +39,7 @@ import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
public class SearchLoggingTest { | |||
@Rule | |||
@@ -63,7 +65,7 @@ public class SearchLoggingTest { | |||
public void do_not_log_to_console() { | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME); | |||
Logger root = ctx.getLogger(ROOT_LOGGER_NAME); | |||
Appender appender = root.getAppender("CONSOLE"); | |||
assertThat(appender).isNull(); | |||
} | |||
@@ -72,7 +74,7 @@ public class SearchLoggingTest { | |||
public void log_to_es_file() { | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME); | |||
Logger root = ctx.getLogger(ROOT_LOGGER_NAME); | |||
Appender<ILoggingEvent> appender = root.getAppender("file_es"); | |||
assertThat(appender).isInstanceOf(FileAppender.class); | |||
FileAppender fileAppender = (FileAppender) appender; | |||
@@ -81,4 +83,24 @@ public class SearchLoggingTest { | |||
PatternLayoutEncoder encoder = (PatternLayoutEncoder) fileAppender.getEncoder(); | |||
assertThat(encoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level es[][%logger{20}] %msg%n"); | |||
} | |||
@Test | |||
public void root_log_level_does_not_change_with_global_property_value() { | |||
props.set("sonar.log.level", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.INFO); | |||
} | |||
@Test | |||
public void root_log_level_change_with_es_property_value() { | |||
props.set("sonar.log.level.es", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.TRACE); | |||
} | |||
} |
@@ -28,7 +28,7 @@ import org.sonar.db.Database; | |||
import org.sonar.server.platform.ServerLogging; | |||
import org.sonar.server.user.UserSession; | |||
import static org.sonar.process.LogbackHelper.ALLOWED_ROOT_LOG_LEVELS; | |||
import static org.sonar.process.LogbackHelper.allowedLogLevels; | |||
public class ChangeLogLevelAction implements SystemWsAction { | |||
@@ -57,7 +57,7 @@ public class ChangeLogLevelAction implements SystemWsAction { | |||
newAction.createParam(PARAM_LEVEL) | |||
.setDescription("The new level. Be cautious: DEBUG, and even more TRACE, may have performance impacts.") | |||
.setPossibleValues(ALLOWED_ROOT_LOG_LEVELS) | |||
.setPossibleValues(allowedLogLevels()) | |||
.setRequired(true); | |||
} | |||
@@ -34,19 +34,21 @@ 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.sonar.process.LogbackHelper; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
public class CeProcessLoggingTest { | |||
private static final String LOG_LEVEL_PROPERTY = "sonar.log.level"; | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private File logDir; | |||
private Props props = new Props(new Properties()); | |||
@@ -87,16 +89,69 @@ public class CeProcessLoggingTest { | |||
} | |||
@Test | |||
public void enable_debug_logs() { | |||
props.set(LOG_LEVEL_PROPERTY, "DEBUG"); | |||
public void default_level_for_root_logger_is_INFO() { | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_global_property() { | |||
props.set("sonar.log.level", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_app_property() { | |||
props.set("sonar.log.level.ce", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_app_property_and_is_case_insensitive() { | |||
props.set("sonar.log.level.ce", "debug"); | |||
LoggerContext ctx = underTest.configure(props); | |||
assertThat(ctx.getLogger(Logger.ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.DEBUG); | |||
verifyRootLogLevel(ctx, Level.DEBUG); | |||
} | |||
@Test | |||
public void enable_trace_logs() { | |||
props.set(LOG_LEVEL_PROPERTY, "TRACE"); | |||
public void default_to_INFO_if_app_property_has_invalid_value() { | |||
props.set("sonar.log.level.ce", "DodoDouh!"); | |||
LoggerContext ctx = underTest.configure(props); | |||
assertThat(ctx.getLogger(Logger.ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.TRACE); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_global_property_unsupported_level() { | |||
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.configure(props); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_app_property_unsupported_level() { | |||
props.set("sonar.log.level.ce", "ERROR"); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("log level ERROR in property sonar.log.level.ce is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); | |||
underTest.configure(props); | |||
} | |||
private void verifyRootLogLevel(LoggerContext ctx, Level expected) { | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(expected); | |||
} | |||
} |
@@ -34,19 +34,21 @@ 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.sonar.process.LogbackHelper; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
public class WebServerProcessLoggingTest { | |||
private static final String LOG_LEVEL_PROPERTY = "sonar.log.level"; | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private File logDir; | |||
private Props props = new Props(new Properties()); | |||
@@ -87,17 +89,70 @@ public class WebServerProcessLoggingTest { | |||
} | |||
@Test | |||
public void enable_debug_logs() { | |||
props.set(LOG_LEVEL_PROPERTY, "DEBUG"); | |||
public void default_level_for_root_logger_is_INFO() { | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_global_property() { | |||
props.set("sonar.log.level", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_app_property() { | |||
props.set("sonar.log.level.web", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_app_property_and_is_case_insensitive() { | |||
props.set("sonar.log.level.web", "debug"); | |||
LoggerContext ctx = underTest.configure(props); | |||
assertThat(ctx.getLogger(Logger.ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.DEBUG); | |||
verifyRootLogLevel(ctx, Level.DEBUG); | |||
} | |||
@Test | |||
public void enable_trace_logs() { | |||
props.set(LOG_LEVEL_PROPERTY, "TRACE"); | |||
public void default_to_INFO_if_app_property_has_invalid_value() { | |||
props.set("sonar.log.level.web", "DodoDouh!"); | |||
LoggerContext ctx = underTest.configure(props); | |||
assertThat(ctx.getLogger(Logger.ROOT_LOGGER_NAME).getLevel()).isEqualTo(Level.TRACE); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_global_property_unsupported_level() { | |||
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.configure(props); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_app_property_unsupported_level() { | |||
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.configure(props); | |||
} | |||
private void verifyRootLogLevel(LoggerContext ctx, Level expected) { | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(expected); | |||
} | |||
} |
@@ -37,6 +37,7 @@ 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.sonar.process.LogbackHelper; | |||
import org.sonar.process.ProcessProperties; | |||
@@ -50,6 +51,8 @@ public class AppLoggingTest { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private File logDir; | |||
@@ -172,35 +175,63 @@ public class AppLoggingTest { | |||
@Test | |||
public void default_level_for_root_logger_is_INFO() { | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.INFO); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void root_logger_level_can_be_changed_with_a_property() { | |||
public void root_logger_level_changes_with_global_property() { | |||
props.set("sonar.log.level", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void root_logger_level_changes_with_app_property() { | |||
props.set("sonar.log.level.app", "TRACE"); | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.TRACE); | |||
verifyRootLogLevel(ctx, Level.TRACE); | |||
} | |||
@Test | |||
public void property_changing_root_logger_level_is_case_insensitive() { | |||
props.set("sonar.log.level.app", "trace"); | |||
public void root_logger_level_changes_with_app_property_and_is_case_insensitive() { | |||
props.set("sonar.log.level.app", "debug"); | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.TRACE); | |||
verifyRootLogLevel(ctx, Level.DEBUG); | |||
} | |||
@Test | |||
public void default_to_INFO_if_property_changing_root_logger_level_has_invalid_value() { | |||
public void default_to_INFO_if_app_property_has_invalid_value() { | |||
props.set("sonar.log.level.app", "DodoDouh!"); | |||
LoggerContext ctx = underTest.configure(props); | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(Level.INFO); | |||
verifyRootLogLevel(ctx, Level.INFO); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_global_property_unsupported_level() { | |||
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.configure(props); | |||
} | |||
@Test | |||
public void fail_with_IAE_if_app_property_unsupported_level() { | |||
props.set("sonar.log.level.app", "ERROR"); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("log level ERROR in property sonar.log.level.app is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); | |||
underTest.configure(props); | |||
} | |||
private void emulateRunFromSonarScript() { | |||
@@ -251,4 +282,9 @@ public class AppLoggingTest { | |||
PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) encoder; | |||
assertThat(patternEncoder.getPattern()).isEqualTo(logPattern); | |||
} | |||
private void verifyRootLogLevel(LoggerContext ctx, Level expected) { | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(expected); | |||
} | |||
} |