diff options
32 files changed, 1283 insertions, 457 deletions
diff --git a/it/it-tests/src/test/java/it/serverSystem/LogsTest.java b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java index dc976fc78d5..4252df014ba 100644 --- a/it/it-tests/src/test/java/it/serverSystem/LogsTest.java +++ b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java @@ -20,20 +20,28 @@ package it.serverSystem; import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; import it.Category4Suite; import java.io.File; import java.io.IOException; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.apache.commons.io.input.ReversedLinesFileReader; +import org.assertj.core.util.Files; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.issue.SearchWsRequest; import util.ItUtils; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.projectDir; public class LogsTest { @@ -43,6 +51,11 @@ public class LogsTest { @ClassRule public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR; + @Before + public void cleanDatabase() { + orchestrator.resetData(); + } + /** * SONAR-7581 */ @@ -75,6 +88,46 @@ public class LogsTest { assertThat(logs.get(logs.size() - 1)).describedAs("message is the last line of logs").contains(sqIsUpMessage); } + @Test + public void test_ws_change_log_level() throws IOException { + generateSqlAndEsLogsInWebAndCe(); + + assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).doesNotContain("DEBUG", "TRACE"); + assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).doesNotContain("DEBUG", "TRACE"); + + orchestrator.getServer().adminWsClient().post("api/system/change_log_level", "level", "TRACE"); + + generateSqlAndEsLogsInWebAndCe(); + + // there is hardly DEBUG logs, but we are sure there must be TRACE logs for SQL and ES requests + assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).contains("TRACE"); + assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).contains("TRACE"); + + // reset log files to empty and level to INFO + orchestrator.getServer().adminWsClient().post("api/system/change_log_level", "level", "INFO"); + FileUtils.write(orchestrator.getServer().getWebLogs(), ""); + FileUtils.write(orchestrator.getServer().getCeLogs(), ""); + + generateSqlAndEsLogsInWebAndCe(); + + assertThat(logLevelsOf(orchestrator.getServer().getWebLogs())).doesNotContain("DEBUG", "TRACE"); + assertThat(logLevelsOf(orchestrator.getServer().getCeLogs())).doesNotContain("DEBUG", "TRACE"); + } + + private void generateSqlAndEsLogsInWebAndCe() { + orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))); + ItUtils.newAdminWsClient(orchestrator).issues().search(new SearchWsRequest() + .setProjectKeys(Collections.singletonList("sample"))); + } + + private Collection<String> logLevelsOf(File webLogs) { + return Files.linesOf(webLogs, "UTF-8").stream() + .filter(str -> str.length() >= 25) + .map(str -> str.substring(20, 25)) + .map(String::trim) + .collect(Collectors.toSet()); + } + private void verifyLastAccessLogLine(String login, String path, int status) throws IOException { assertThat(readLastAccessLog()).endsWith(format("\"%s\" \"GET %s HTTP/1.1\" %d", login, path, status)); } diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index fe082dbbc7a..c671a6a2f87 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -44,6 +44,7 @@ import org.sonar.ce.CeHttpModule; import org.sonar.ce.CeQueueModule; import org.sonar.ce.CeTaskCommonsModule; import org.sonar.ce.db.ReadOnlyPropertiesDao; +import org.sonar.ce.log.CeProcessLogging; import org.sonar.ce.platform.ComputeEngineExtensionInstaller; import org.sonar.ce.settings.ProjectSettingsFactory; import org.sonar.ce.user.CeUserSession; @@ -63,7 +64,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DefaultDatabase; import org.sonar.db.purge.PurgeProfiler; import org.sonar.db.version.DatabaseVersion; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.Props; import org.sonar.server.component.ComponentCleanerService; import org.sonar.server.component.ComponentFinder; @@ -212,6 +213,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { ThreadLocalSettings.class, new SonarQubeVersion(apiVersion), SonarRuntimeImpl.forSonarQube(ApiVersion.load(System2.INSTANCE), SonarQubeSide.COMPUTE_ENGINE), + CeProcessLogging.class, UuidFactoryImpl.INSTANCE, ClusterImpl.class, LogbackHelper.class, diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/logging/ChangeLogLevelHttpAction.java b/server/sonar-ce/src/main/java/org/sonar/ce/logging/ChangeLogLevelHttpAction.java index 626eb20c4a6..a84ccc23b85 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/logging/ChangeLogLevelHttpAction.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/logging/ChangeLogLevelHttpAction.java @@ -23,6 +23,7 @@ import fi.iki.elonen.NanoHTTPD; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.api.utils.log.Loggers; import org.sonar.ce.httpd.HttpAction; +import org.sonar.ce.log.CeProcessLogging; import org.sonar.db.Database; import org.sonar.server.platform.ServerLogging; @@ -40,10 +41,12 @@ public class ChangeLogLevelHttpAction implements HttpAction { private final ServerLogging logging; private final Database db; + private final CeProcessLogging ceProcessLogging; - public ChangeLogLevelHttpAction(ServerLogging logging, Database db) { + public ChangeLogLevelHttpAction(ServerLogging logging, Database db, CeProcessLogging ceProcessLogging) { this.logging = logging; this.db = db; + this.ceProcessLogging = ceProcessLogging; } @Override @@ -64,7 +67,7 @@ public class ChangeLogLevelHttpAction implements HttpAction { try { LoggerLevel level = LoggerLevel.valueOf(levelStr); db.enableSqlLogging(level.equals(LoggerLevel.TRACE)); - logging.changeLevel(level); + logging.changeLevel(ceProcessLogging, level); return newFixedLengthResponse(OK, MIME_PLAINTEXT, null); } catch (IllegalArgumentException e) { Loggers.get(ChangeLogLevelHttpAction.class).debug("Value '{}' for parameter '{}' is invalid", levelStr, PARAM_LEVEL, e); diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index a31db40f3ee..25c7fa6fe55 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -105,7 +105,7 @@ public class ComputeEngineContainerImplTest { ); assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION - + 26 // level 1 + + 27 // level 1 + 47 // content of DaoModule + 2 // content of EsSearchModule + 63 // content of CorePropertyDefinitions diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/logging/ChangeLogLevelHttpActionTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/logging/ChangeLogLevelHttpActionTest.java index cce5313c2f5..dc7b79977e8 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/logging/ChangeLogLevelHttpActionTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/logging/ChangeLogLevelHttpActionTest.java @@ -26,6 +26,7 @@ import org.apache.commons.io.IOUtils; import org.junit.Test; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.ce.httpd.HttpAction; +import org.sonar.ce.log.CeProcessLogging; import org.sonar.db.Database; import org.sonar.server.platform.ServerLogging; @@ -42,7 +43,8 @@ import static org.sonar.ce.httpd.CeHttpUtils.createHttpSession; public class ChangeLogLevelHttpActionTest { private ServerLogging serverLogging = mock(ServerLogging.class); private Database database = mock(Database.class); - private ChangeLogLevelHttpAction underTest = new ChangeLogLevelHttpAction(serverLogging, database); + private CeProcessLogging ceProcessLogging = new CeProcessLogging(); + private ChangeLogLevelHttpAction underTest = new ChangeLogLevelHttpAction(serverLogging, database, ceProcessLogging); @Test public void register_to_path_systemInfo() { @@ -81,7 +83,7 @@ public class ChangeLogLevelHttpActionTest { assertThat(response.getStatus()).isEqualTo(OK); - verify(serverLogging).changeLevel(LoggerLevel.ERROR); + verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.ERROR); verify(database).enableSqlLogging(false); } @@ -91,7 +93,7 @@ public class ChangeLogLevelHttpActionTest { assertThat(response.getStatus()).isEqualTo(OK); - verify(serverLogging).changeLevel(LoggerLevel.INFO); + verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.INFO); verify(database).enableSqlLogging(false); } @@ -101,7 +103,7 @@ public class ChangeLogLevelHttpActionTest { assertThat(response.getStatus()).isEqualTo(OK); - verify(serverLogging).changeLevel(LoggerLevel.DEBUG); + verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.DEBUG); verify(database).enableSqlLogging(false); } @@ -111,7 +113,7 @@ public class ChangeLogLevelHttpActionTest { assertThat(response.getStatus()).isEqualTo(OK); - verify(serverLogging).changeLevel(LoggerLevel.TRACE); + verify(serverLogging).changeLevel(ceProcessLogging, LoggerLevel.TRACE); verify(database).enableSqlLogging(true); } } 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..5a4198363b8 --- /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("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/LogbackHelper.java b/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java index ac9351d8ad2..b9342e4f70f 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java +++ b/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java @@ -17,7 +17,7 @@ * 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; +package org.sonar.process.logging; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; @@ -38,14 +38,15 @@ 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.ProcessProperties; +import org.sonar.process.Props; import static java.lang.String.format; -import static java.util.Objects.requireNonNull; import static org.slf4j.Logger.ROOT_LOGGER_NAME; /** @@ -56,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"; @@ -93,53 +92,61 @@ public class LogbackHelper { return propagator; } - public static final class RootLoggerConfig { - private final ProcessId processId; - private final String threadIdFieldPattern; - - private RootLoggerConfig(Builder builder) { - this.processId = requireNonNull(builder.processId); - this.threadIdFieldPattern = builder.threadIdFieldPattern; - } - - public static Builder newRootLoggerConfigBuilder() { - return new Builder(); - } - - ProcessId getProcessId() { - return processId; - } - - String getThreadIdFieldPattern() { - return threadIdFieldPattern; - } + /** + * 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; + } - public static final class Builder { - @CheckForNull - private ProcessId processId; - private String threadIdFieldPattern = ""; + private void applyLevelByProperty(Props props, Logger logger, List<String> properties) { + logger.setLevel(resolveLevel(props, properties.stream().toArray(String[]::new))); + } - private Builder() { - // prevents instantiation outside RootLoggerConfig, use static factory method + /** + * 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; + } - public Builder setProcessId(ProcessId processId) { - this.processId = processId; - return this; - } + private static void applyHardcodedLevel(LoggerContext rootContext, String loggerName, Level newLevel) { + rootContext.getLogger(loggerName).setLevel(newLevel); + } - public Builder setThreadIdFieldPattern(String threadIdFieldPattern) { - this.threadIdFieldPattern = requireNonNull(threadIdFieldPattern, "threadIdFieldPattern can't be null"); - return this; - } + 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)); + } - public RootLoggerConfig build() { - return new RootLoggerConfig(this); - } + 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(LogbackHelper.RootLoggerConfig config) { + public String buildLogPattern(RootLoggerConfig config) { return LOG_FORMAT .replace(PROCESS_NAME_PLACEHOLDER, config.getProcessId().getKey()) .replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern()); @@ -185,7 +192,7 @@ public class LogbackHelper { return fileAppender; } - public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, LogbackHelper.RootLoggerConfig config, String logPattern) { + public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, RootLoggerConfig config, String logPattern) { RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, config.getProcessId().getLogFilenamePrefix()); FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + config.getProcessId().getLogFilenamePrefix()); fileAppender.setContext(ctx); @@ -219,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); @@ -268,57 +243,13 @@ public class LogbackHelper { private static boolean isAllowed(Level level) { for (Level allowedRootLogLevel : ALLOWED_ROOT_LOG_LEVELS) { - if (level == allowedRootLogLevel) { + if (level.equals(allowedRootLogLevel)) { return true; } } 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(); } @@ -457,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/main/java/org/sonar/process/logging/RootLoggerConfig.java b/server/sonar-process/src/main/java/org/sonar/process/logging/RootLoggerConfig.java new file mode 100644 index 00000000000..d1773e8ab78 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/logging/RootLoggerConfig.java @@ -0,0 +1,71 @@ +/* + * 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 javax.annotation.CheckForNull; +import org.sonar.process.ProcessId; + +import static java.util.Objects.requireNonNull; + +public final class RootLoggerConfig { + private final ProcessId processId; + private final String threadIdFieldPattern; + + private RootLoggerConfig(Builder builder) { + this.processId = requireNonNull(builder.processId); + this.threadIdFieldPattern = builder.threadIdFieldPattern; + } + + public static Builder newRootLoggerConfigBuilder() { + return new Builder(); + } + + ProcessId getProcessId() { + return processId; + } + + String getThreadIdFieldPattern() { + return threadIdFieldPattern; + } + + public static final class Builder { + @CheckForNull + private ProcessId processId; + private String threadIdFieldPattern = ""; + + private Builder() { + // prevents instantiation outside RootLoggerConfig, use static factory method + } + + public Builder setProcessId(ProcessId processId) { + this.processId = processId; + return this; + } + + public Builder setThreadIdFieldPattern(String threadIdFieldPattern) { + this.threadIdFieldPattern = requireNonNull(threadIdFieldPattern, "threadIdFieldPattern can't be null"); + return this; + } + + public RootLoggerConfig build() { + return new RootLoggerConfig(this); + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java b/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java deleted file mode 100644 index 3ffc86c64e9..00000000000 --- a/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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; - -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; -import ch.qos.logback.core.Appender; -import ch.qos.logback.core.ConsoleAppender; -import ch.qos.logback.core.FileAppender; -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 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.TemporaryFolder; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; - -public class LogbackHelperTest { - - @Rule - public TemporaryFolder temp = new TemporaryFolder(); - - private Props props = new Props(new Properties()); - private LogbackHelper underTest = new LogbackHelper(); - - @Before - public void setUp() throws Exception { - File dir = temp.newFolder(); - props.set(ProcessProperties.PATH_LOGS, dir.getAbsolutePath()); - } - - @AfterClass - public static void resetLogback() throws Exception { - new LogbackHelper().resetFromXml("/logback-test.xml"); - } - - @Test - public void getRootContext() { - assertThat(underTest.getRootContext()).isNotNull(); - } - - @Test - public void enableJulChangePropagation() { - LoggerContext ctx = underTest.getRootContext(); - int countListeners = ctx.getCopyOfListenerList().size(); - - LoggerContextListener propagator = underTest.enableJulChangePropagation(ctx); - assertThat(ctx.getCopyOfListenerList().size()).isEqualTo(countListeners + 1); - - ctx.removeListener(propagator); - } - - @Test - public void newConsoleAppender() { - LoggerContext ctx = underTest.getRootContext(); - ConsoleAppender<?> appender = underTest.newConsoleAppender(ctx, "MY_APPENDER", "%msg%n"); - - assertThat(appender.getName()).isEqualTo("MY_APPENDER"); - assertThat(appender.getContext()).isSameAs(ctx); - assertThat(appender.isStarted()).isTrue(); - assertThat(((PatternLayoutEncoder) appender.getEncoder()).getPattern()).isEqualTo("%msg%n"); - assertThat(appender.getCopyOfAttachedFiltersList()).isEmpty(); - } - - @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"); - FileAppender appender = policy.createAppender("SONAR_FILE"); - assertThat(appender).isInstanceOf(RollingFileAppender.class); - - // max 5 daily files - RollingFileAppender fileAppender = (RollingFileAppender) appender; - TimeBasedRollingPolicy triggeringPolicy = (TimeBasedRollingPolicy) fileAppender.getTriggeringPolicy(); - assertThat(triggeringPolicy.getMaxHistory()).isEqualTo(7); - assertThat(triggeringPolicy.getFileNamePattern()).endsWith("sonar.%d{yyyy-MM-dd}.log"); - } - - @Test - public void createRollingPolicy_none() { - props.set("sonar.log.rollingPolicy", "none"); - LoggerContext ctx = underTest.getRootContext(); - LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); - - Appender appender = policy.createAppender("SONAR_FILE"); - assertThat(appender).isNotInstanceOf(RollingFileAppender.class).isInstanceOf(FileAppender.class); - } - - @Test - public void createRollingPolicy_size() { - props.set("sonar.log.rollingPolicy", "size:1MB"); - props.set("sonar.log.maxFiles", "20"); - LoggerContext ctx = underTest.getRootContext(); - LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); - - Appender appender = policy.createAppender("SONAR_FILE"); - assertThat(appender).isInstanceOf(RollingFileAppender.class); - - // max 20 files of 1Mb - RollingFileAppender fileAppender = (RollingFileAppender) appender; - FixedWindowRollingPolicy rollingPolicy = (FixedWindowRollingPolicy) fileAppender.getRollingPolicy(); - assertThat(rollingPolicy.getMaxIndex()).isEqualTo(20); - assertThat(rollingPolicy.getFileNamePattern()).endsWith("sonar.%i.log"); - SizeBasedTriggeringPolicy triggeringPolicy = (SizeBasedTriggeringPolicy) fileAppender.getTriggeringPolicy(); - assertThat(triggeringPolicy.getMaxFileSize()).isEqualTo("1MB"); - } - - @Test - public void createRollingPolicy_time() { - props.set("sonar.log.rollingPolicy", "time:yyyy-MM"); - props.set("sonar.log.maxFiles", "20"); - - LoggerContext ctx = underTest.getRootContext(); - LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); - - RollingFileAppender appender = (RollingFileAppender) policy.createAppender("SONAR_FILE"); - - // max 5 monthly files - TimeBasedRollingPolicy triggeringPolicy = (TimeBasedRollingPolicy) appender.getTriggeringPolicy(); - assertThat(triggeringPolicy.getMaxHistory()).isEqualTo(20); - assertThat(triggeringPolicy.getFileNamePattern()).endsWith("sonar.%d{yyyy-MM}.log"); - } - - @Test - public void createRollingPolicy_fail_if_unknown_policy() { - props.set("sonar.log.rollingPolicy", "unknown:foo"); - try { - LoggerContext ctx = underTest.getRootContext(); - underTest.createRollingPolicy(ctx, props, "sonar"); - fail(); - } catch (MessageException e) { - assertThat(e).hasMessage("Unsupported value for property sonar.log.rollingPolicy: unknown:foo"); - } - } -} 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 new file mode 100644 index 00000000000..66d9912de1e --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java @@ -0,0 +1,334 @@ +/* + * 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 ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.FileAppender; +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(); + + @Before + public void setUp() throws Exception { + File dir = temp.newFolder(); + props.set(ProcessProperties.PATH_LOGS, dir.getAbsolutePath()); + } + + @AfterClass + public static void resetLogback() throws Exception { + new LogbackHelper().resetFromXml("/logback-test.xml"); + } + + @Test + public void getRootContext() { + assertThat(underTest.getRootContext()).isNotNull(); + } + + @Test + public void enableJulChangePropagation() { + LoggerContext ctx = underTest.getRootContext(); + int countListeners = ctx.getCopyOfListenerList().size(); + + LoggerContextListener propagator = underTest.enableJulChangePropagation(ctx); + assertThat(ctx.getCopyOfListenerList().size()).isEqualTo(countListeners + 1); + + ctx.removeListener(propagator); + } + + @Test + public void newConsoleAppender() { + LoggerContext ctx = underTest.getRootContext(); + ConsoleAppender<?> appender = underTest.newConsoleAppender(ctx, "MY_APPENDER", "%msg%n"); + + assertThat(appender.getName()).isEqualTo("MY_APPENDER"); + assertThat(appender.getContext()).isSameAs(ctx); + assertThat(appender.isStarted()).isTrue(); + assertThat(((PatternLayoutEncoder) appender.getEncoder()).getPattern()).isEqualTo("%msg%n"); + assertThat(appender.getCopyOfAttachedFiltersList()).isEmpty(); + } + + @Test + public void createRollingPolicy_defaults() { + LoggerContext ctx = underTest.getRootContext(); + LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); + FileAppender appender = policy.createAppender("SONAR_FILE"); + assertThat(appender).isInstanceOf(RollingFileAppender.class); + + // max 5 daily files + RollingFileAppender fileAppender = (RollingFileAppender) appender; + TimeBasedRollingPolicy triggeringPolicy = (TimeBasedRollingPolicy) fileAppender.getTriggeringPolicy(); + assertThat(triggeringPolicy.getMaxHistory()).isEqualTo(7); + assertThat(triggeringPolicy.getFileNamePattern()).endsWith("sonar.%d{yyyy-MM-dd}.log"); + } + + @Test + public void createRollingPolicy_none() { + props.set("sonar.log.rollingPolicy", "none"); + LoggerContext ctx = underTest.getRootContext(); + LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); + + Appender appender = policy.createAppender("SONAR_FILE"); + assertThat(appender).isNotInstanceOf(RollingFileAppender.class).isInstanceOf(FileAppender.class); + } + + @Test + public void createRollingPolicy_size() { + props.set("sonar.log.rollingPolicy", "size:1MB"); + props.set("sonar.log.maxFiles", "20"); + LoggerContext ctx = underTest.getRootContext(); + LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); + + Appender appender = policy.createAppender("SONAR_FILE"); + assertThat(appender).isInstanceOf(RollingFileAppender.class); + + // max 20 files of 1Mb + RollingFileAppender fileAppender = (RollingFileAppender) appender; + FixedWindowRollingPolicy rollingPolicy = (FixedWindowRollingPolicy) fileAppender.getRollingPolicy(); + assertThat(rollingPolicy.getMaxIndex()).isEqualTo(20); + assertThat(rollingPolicy.getFileNamePattern()).endsWith("sonar.%i.log"); + SizeBasedTriggeringPolicy triggeringPolicy = (SizeBasedTriggeringPolicy) fileAppender.getTriggeringPolicy(); + assertThat(triggeringPolicy.getMaxFileSize()).isEqualTo("1MB"); + } + + @Test + public void createRollingPolicy_time() { + props.set("sonar.log.rollingPolicy", "time:yyyy-MM"); + props.set("sonar.log.maxFiles", "20"); + + LoggerContext ctx = underTest.getRootContext(); + LogbackHelper.RollingPolicy policy = underTest.createRollingPolicy(ctx, props, "sonar"); + + RollingFileAppender appender = (RollingFileAppender) policy.createAppender("SONAR_FILE"); + + // max 5 monthly files + TimeBasedRollingPolicy triggeringPolicy = (TimeBasedRollingPolicy) appender.getTriggeringPolicy(); + assertThat(triggeringPolicy.getMaxHistory()).isEqualTo(20); + assertThat(triggeringPolicy.getFileNamePattern()).endsWith("sonar.%d{yyyy-MM}.log"); + } + + @Test + public void createRollingPolicy_fail_if_unknown_policy() { + props.set("sonar.log.rollingPolicy", "unknown:foo"); + try { + LoggerContext ctx = underTest.getRootContext(); + underTest.createRollingPolicy(ctx, props, "sonar"); + fail(); + } catch (MessageException e) { + 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).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).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).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) + .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} + }; + } +} diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java b/server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java index d0216cc89f6..d4ef440abd4 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java @@ -20,11 +20,13 @@ package org.sonar.search; import ch.qos.logback.classic.LoggerContext; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.ProcessId; import org.sonar.process.Props; +import org.sonar.process.logging.RootLoggerConfig; -import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder; +import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; public class SearchLogging { @@ -34,12 +36,13 @@ public class SearchLogging { LoggerContext ctx = helper.getRootContext(); ctx.reset(); - LogbackHelper.RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(ProcessId.ELASTICSEARCH).build(); + RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(ProcessId.ELASTICSEARCH).build(); String logPattern = helper.buildLogPattern(config); helper.configureGlobalFileLog(props, config, logPattern); helper.configureForSubprocessGobbler(props, logPattern); - helper.configureRootLogLevel(props, ProcessId.ELASTICSEARCH); + + helper.apply(LogLevelConfig.newBuilder().rootLevelFor(ProcessId.ELASTICSEARCH).build(), props); return ctx; } diff --git a/server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java b/server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java index c1a18209704..2017d61a19d 100644 --- a/server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java +++ b/server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java @@ -35,7 +35,7 @@ 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.logging.LogbackHelper; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; diff --git a/server/sonar-server/src/main/java/org/sonar/ce/log/CeProcessLogging.java b/server/sonar-server/src/main/java/org/sonar/ce/log/CeProcessLogging.java index 566aa1be7d0..5729b41fa5b 100644 --- a/server/sonar-server/src/main/java/org/sonar/ce/log/CeProcessLogging.java +++ b/server/sonar-server/src/main/java/org/sonar/ce/log/CeProcessLogging.java @@ -19,13 +19,12 @@ */ package org.sonar.ce.log; -import org.sonar.process.LogbackHelper; import org.sonar.process.ProcessId; -import org.sonar.process.Props; +import org.sonar.process.logging.LogDomain; +import org.sonar.process.logging.LogLevelConfig; import org.sonar.server.app.ServerProcessLogging; import static org.sonar.ce.log.CeLogging.MDC_CE_TASK_UUID; -import static org.sonar.process.LogbackHelper.LogDomain; /** * Configure logback for the Compute Engine process. Logs are written to file "ce.log" in SQ's log directory. @@ -37,9 +36,14 @@ public class CeProcessLogging extends ServerProcessLogging { } @Override - protected void extendConfiguration(LogbackHelper helper, Props props) { - helper.configureLoggerLogLevelFromDomain("sql", props, ProcessId.COMPUTE_ENGINE, LogDomain.SQL); - helper.configureLoggerLogLevelFromDomain("es", props, ProcessId.COMPUTE_ENGINE, LogDomain.ES_CLIENT); - helper.configureLoggersLogLevelFromDomain(JMX_RMI_LOGGER_NAMES, props, ProcessId.COMPUTE_ENGINE, LogDomain.JMX); + protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) { + logLevelConfigBuilder.levelByDomain("sql", ProcessId.COMPUTE_ENGINE, LogDomain.SQL); + logLevelConfigBuilder.levelByDomain("es", ProcessId.COMPUTE_ENGINE, LogDomain.ES); + JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, ProcessId.COMPUTE_ENGINE, LogDomain.JMX)); + } + + @Override + protected void extendConfigure() { + // nothing to do } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcessLogging.java b/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcessLogging.java index c8c0d9bc6b4..baeaa88d318 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcessLogging.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcessLogging.java @@ -19,34 +19,60 @@ */ package org.sonar.server.app; +import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import com.google.common.collect.ImmutableSet; import java.util.Set; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.ProcessId; import org.sonar.process.Props; -import org.sonar.server.platform.ServerLogging; +import org.sonar.process.logging.RootLoggerConfig; -import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder; +import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; public abstract class ServerProcessLogging { protected static final Set<String> JMX_RMI_LOGGER_NAMES = ImmutableSet.of( - "javax.management.remote.timeout", - "javax.management.remote.misc", - "javax.management.remote.rmi", - "javax.management.mbeanserver", - "sun.rmi.loader", - "sun.rmi.transport.tcp", - "sun.rmi.transport.misc", - "sun.rmi.server.call", - "sun.rmi.dgc"); + "javax.management.remote.timeout", + "javax.management.remote.misc", + "javax.management.remote.rmi", + "javax.management.mbeanserver", + "sun.rmi.loader", + "sun.rmi.transport.tcp", + "sun.rmi.transport.misc", + "sun.rmi.server.call", + "sun.rmi.dgc"); private final ProcessId processId; private final String threadIdFieldPattern; private final LogbackHelper helper = new LogbackHelper(); + private final LogLevelConfig logLevelConfig; protected ServerProcessLogging(ProcessId processId, String threadIdFieldPattern) { this.processId = processId; this.threadIdFieldPattern = threadIdFieldPattern; + this.logLevelConfig = createLogLevelConfiguration(processId); + } + + private LogLevelConfig createLogLevelConfiguration(ProcessId processId) { + LogLevelConfig.Builder builder = LogLevelConfig.newBuilder(); + builder.rootLevelFor(processId); + builder.immutableLevel("rails", Level.WARN); + builder.immutableLevel("org.apache.ibatis", Level.WARN); + builder.immutableLevel("java.sql", Level.WARN); + builder.immutableLevel("java.sql.ResultSet", Level.WARN); + builder.immutableLevel("org.sonar.MEASURE_FILTER", Level.WARN); + builder.immutableLevel("org.elasticsearch", Level.INFO); + builder.immutableLevel("org.elasticsearch.node", Level.INFO); + builder.immutableLevel("org.elasticsearch.http", Level.INFO); + builder.immutableLevel("ch.qos.logback", Level.WARN); + builder.immutableLevel("org.apache.catalina", Level.INFO); + builder.immutableLevel("org.apache.coyote", Level.INFO); + builder.immutableLevel("org.apache.jasper", Level.INFO); + builder.immutableLevel("org.apache.tomcat", Level.INFO); + + extendLogLevelConfiguration(builder); + + return builder.build(); } public LoggerContext configure(Props props) { @@ -55,16 +81,23 @@ public abstract class ServerProcessLogging { helper.enableJulChangePropagation(ctx); configureRootLogger(props); + helper.apply(logLevelConfig, props); - extendConfiguration(helper, props); + extendConfigure(); return ctx; } - protected abstract void extendConfiguration(LogbackHelper helper, Props props); + public LogLevelConfig getLogLevelConfig() { + return this.logLevelConfig; + } + + protected abstract void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder); + + protected abstract void extendConfigure(); private void configureRootLogger(Props props) { - LogbackHelper.RootLoggerConfig config = newRootLoggerConfigBuilder() + RootLoggerConfig config = newRootLoggerConfigBuilder() .setProcessId(processId) .setThreadIdFieldPattern(threadIdFieldPattern) .build(); @@ -72,9 +105,6 @@ public abstract class ServerProcessLogging { helper.configureGlobalFileLog(props, config, logPattern); helper.configureForSubprocessGobbler(props, logPattern); - - helper.configureRootLogLevel(props, processId); - ServerLogging.configureHardcodedLevels(helper); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java index 18792c1676f..cb938e59f0e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java @@ -26,7 +26,7 @@ import org.apache.catalina.LifecycleListener; import org.apache.catalina.startup.Tomcat; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.Props; class TomcatAccessLog { diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebServerProcessLogging.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServerProcessLogging.java index 6b101065be5..149d94be91a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/WebServerProcessLogging.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServerProcessLogging.java @@ -21,11 +21,10 @@ package org.sonar.server.app; import java.util.logging.LogManager; import org.slf4j.bridge.SLF4JBridgeHandler; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogLevelConfig; import org.sonar.process.ProcessId; -import org.sonar.process.Props; -import static org.sonar.process.LogbackHelper.LogDomain; +import org.sonar.process.logging.LogDomain; import static org.sonar.server.platform.web.requestid.RequestIdMDCStorage.HTTP_REQUEST_ID_MDC_KEY; /** @@ -38,13 +37,16 @@ public class WebServerProcessLogging extends ServerProcessLogging { } @Override - protected void extendConfiguration(LogbackHelper helper, Props props) { + protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) { + logLevelConfigBuilder.levelByDomain("sql", ProcessId.WEB_SERVER, LogDomain.SQL); + logLevelConfigBuilder.levelByDomain("es", ProcessId.WEB_SERVER, LogDomain.ES); + JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, ProcessId.WEB_SERVER, LogDomain.JMX)); + } + + @Override + protected void extendConfigure() { // Configure java.util.logging, used by Tomcat, in order to forward to slf4j LogManager.getLogManager().reset(); SLF4JBridgeHandler.install(); - - helper.configureLoggerLogLevelFromDomain("sql", props, ProcessId.WEB_SERVER, LogDomain.SQL); - helper.configureLoggerLogLevelFromDomain("es", props, ProcessId.WEB_SERVER, LogDomain.ES_CLIENT); - helper.configureLoggersLogLevelFromDomain(JMX_RMI_LOGGER_NAMES, props, ProcessId.WEB_SERVER, LogDomain.JMX); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java index 5fe0cbb7d3b..08feeea802c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java @@ -29,8 +29,9 @@ import org.sonar.api.config.Settings; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.api.utils.log.Loggers; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.ProcessProperties; +import org.sonar.server.app.ServerProcessLogging; @ServerSide @ComputeEngineSide @@ -49,10 +50,9 @@ public class ServerLogging { this.settings = settings; } - public void changeLevel(LoggerLevel level) { + public void changeLevel(ServerProcessLogging serverProcessLogging, LoggerLevel level) { Level logbackLevel = Level.toLevel(level.name()); - helper.configureRootLogLevel(logbackLevel); - configureHardcodedLevels(helper); + helper.changeRoot(serverProcessLogging.getLogLevelConfig(), logbackLevel); LoggerFactory.getLogger(ServerLogging.class).info("Level of logs changed to {}", level); } @@ -60,22 +60,6 @@ public class ServerLogging { return Loggers.get(Logger.ROOT_LOGGER_NAME).getLevel(); } - public static void configureHardcodedLevels(LogbackHelper helper) { - helper.configureLogger("rails", Level.WARN); - helper.configureLogger("org.apache.ibatis", Level.WARN); - helper.configureLogger("java.sql", Level.WARN); - helper.configureLogger("java.sql.ResultSet", Level.WARN); - helper.configureLogger("org.sonar.MEASURE_FILTER", Level.WARN); - helper.configureLogger("org.elasticsearch", Level.INFO); - helper.configureLogger("org.elasticsearch.node", Level.INFO); - helper.configureLogger("org.elasticsearch.http", Level.INFO); - helper.configureLogger("ch.qos.logback", Level.WARN); - helper.configureLogger("org.apache.catalina", Level.INFO); - helper.configureLogger("org.apache.coyote", Level.INFO); - helper.configureLogger("org.apache.jasper", Level.INFO); - helper.configureLogger("org.apache.tomcat", Level.INFO); - } - /** * The directory that contains log files. May not exist. */ @@ -83,11 +67,4 @@ public class ServerLogging { return new File(settings.getString(ProcessProperties.PATH_LOGS)); } - /** - * The file sonar.log, may not exist. - */ - public File getCurrentLogFile() { - return new File(getLogsDir(), "sonar.log"); - } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index d2543fb2b93..7bc4c362646 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -37,9 +37,10 @@ import org.sonar.db.DefaultDatabase; import org.sonar.db.purge.PurgeProfiler; import org.sonar.db.semaphore.SemaphoresImpl; import org.sonar.db.version.DatabaseVersion; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import org.sonar.server.app.ProcessCommandWrapperImpl; import org.sonar.server.app.RestartFlagHolderImpl; +import org.sonar.server.app.WebServerProcessLogging; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.platform.DatabaseServerCompatibility; import org.sonar.server.platform.LogServerVersion; @@ -87,6 +88,7 @@ public class PlatformLevel1 extends PlatformLevel { UrlSettings.class, EmbeddedDatabaseFactory.class, LogbackHelper.class, + WebServerProcessLogging.class, DefaultDatabase.class, DatabaseChecker.class, // must instantiate deprecated class in 5.2 and only this one (and not its replacement) diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java index 74917769dab..1ccb94afe40 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java @@ -25,10 +25,11 @@ import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.ce.http.CeHttpClient; import org.sonar.db.Database; +import org.sonar.server.app.WebServerProcessLogging; import org.sonar.server.platform.ServerLogging; import org.sonar.server.user.UserSession; -import static org.sonar.process.LogbackHelper.allowedLogLevels; +import static org.sonar.process.logging.LogbackHelper.allowedLogLevels; public class ChangeLogLevelAction implements SystemWsAction { @@ -38,12 +39,14 @@ public class ChangeLogLevelAction implements SystemWsAction { private final ServerLogging logging; private final Database db; private final CeHttpClient ceHttpClient; + private final WebServerProcessLogging webServerProcessLogging; - public ChangeLogLevelAction(UserSession userSession, ServerLogging logging, Database db, CeHttpClient ceHttpClient) { + public ChangeLogLevelAction(UserSession userSession, ServerLogging logging, Database db, CeHttpClient ceHttpClient, WebServerProcessLogging webServerProcessLogging) { this.userSession = userSession; this.logging = logging; this.db = db; this.ceHttpClient = ceHttpClient; + this.webServerProcessLogging = webServerProcessLogging; } @Override @@ -67,7 +70,7 @@ public class ChangeLogLevelAction implements SystemWsAction { LoggerLevel level = LoggerLevel.valueOf(wsRequest.mandatoryParam(PARAM_LEVEL)); db.enableSqlLogging(level.equals(LoggerLevel.TRACE)); - logging.changeLevel(level); + logging.changeLevel(webServerProcessLogging, level); ceHttpClient.changeLogLevel(level); wsResponse.noContent(); } diff --git a/server/sonar-server/src/test/java/org/sonar/ce/log/CeLoggingTest.java b/server/sonar-server/src/test/java/org/sonar/ce/log/CeLoggingTest.java index c251892a284..53c28708c2e 100644 --- a/server/sonar-server/src/test/java/org/sonar/ce/log/CeLoggingTest.java +++ b/server/sonar-server/src/test/java/org/sonar/ce/log/CeLoggingTest.java @@ -27,7 +27,7 @@ import org.junit.rules.ExpectedException; import org.mockito.Mockito; import org.slf4j.MDC; import org.sonar.ce.queue.CeTask; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; diff --git a/server/sonar-server/src/test/java/org/sonar/ce/log/CeProcessLoggingTest.java b/server/sonar-server/src/test/java/org/sonar/ce/log/CeProcessLoggingTest.java index 41474bed5a9..0058688f056 100644 --- a/server/sonar-server/src/test/java/org/sonar/ce/log/CeProcessLoggingTest.java +++ b/server/sonar-server/src/test/java/org/sonar/ce/log/CeProcessLoggingTest.java @@ -36,9 +36,9 @@ 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 org.sonar.process.logging.LogbackHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.slf4j.Logger.ROOT_LOGGER_NAME; @@ -247,6 +247,63 @@ public class CeProcessLoggingTest { } @Test + public void jmx_logger_level_changes_with_global_property_and_is_case_insensitive() { + props.set("sonar.log.level", "InFO"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test + public void jmx_logger_level_changes_with_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.ce", "TrACe"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.TRACE); + } + + @Test + public void jmx_logger_level_changes_with_ce_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.ce.jmx", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_ce_jmx_property_over_ce_property() { + props.set("sonar.log.level.ce.jmx", "debug"); + props.set("sonar.log.level.ce", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_ce_jmx_property_over_global_property() { + props.set("sonar.log.level.ce.jmx", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_ce_property_over_global_property() { + props.set("sonar.log.level.ce", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test public void root_logger_level_defaults_to_INFO_if_ce_property_has_invalid_value() { props.set("sonar.log.level.ce", "DodoDouh!"); @@ -271,6 +328,14 @@ public class CeProcessLoggingTest { } @Test + public void jmx_loggers_level_defaults_to_INFO_if_ce_jmx_property_has_invalid_value() { + props.set("sonar.log.level.ce.jmx", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test public void fail_with_IAE_if_global_property_unsupported_level() { props.set("sonar.log.level", "ERROR"); @@ -310,6 +375,41 @@ public class CeProcessLoggingTest { underTest.configure(props); } + @Test + public void fail_with_IAE_if_ce_jmx_property_unsupported_level() { + props.set("sonar.log.level.ce.jmx", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.ce.jmx is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void configure_defines_hardcoded_levels() { + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_global_property() { + props.set("sonar.log.level", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_ce_property() { + props.set("sonar.log.level.ce", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + private void verifyRootLogLevel(LoggerContext ctx, Level expected) { assertThat(ctx.getLogger(ROOT_LOGGER_NAME).getLevel()).isEqualTo(expected); } @@ -321,4 +421,32 @@ public class CeProcessLoggingTest { private void verifyEsLogLevel(LoggerContext ctx, Level expected) { assertThat(ctx.getLogger("es").getLevel()).isEqualTo(expected); } + + private void verifyJmxLogLevel(LoggerContext ctx, Level expected) { + assertThat(ctx.getLogger("javax.management.remote.timeout").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.rmi").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.mbeanserver").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.loader").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.tcp").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.server.call").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.dgc").getLevel()).isEqualTo(expected); + } + + private void verifyImmutableLogLevels(LoggerContext ctx) { + assertThat(ctx.getLogger("rails").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.apache.ibatis").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql.ResultSet").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.sonar.MEASURE_FILTER").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.elasticsearch").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.node").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.http").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("ch.qos.logback").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.apache.catalina").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.coyote").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.jasper").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.tomcat").getLevel()).isEqualTo(Level.INFO); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java index 3099b2bc34e..01f919098d3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java @@ -22,7 +22,7 @@ package org.sonar.server.app; import org.apache.catalina.Container; import org.junit.AfterClass; import org.junit.Test; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java index a81077b6793..02dba16937a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java @@ -36,7 +36,7 @@ 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.logging.LogbackHelper; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; @@ -247,6 +247,63 @@ public class WebServerProcessLoggingTest { } @Test + public void jmx_logger_level_changes_with_global_property_and_is_case_insensitive() { + props.set("sonar.log.level", "InFO"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test + public void jmx_logger_level_changes_with_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.web", "TrACe"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.TRACE); + } + + @Test + public void jmx_logger_level_changes_with_web_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.web.jmx", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_jmx_property_over_web_property() { + props.set("sonar.log.level.web.jmx", "debug"); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_jmx_property_over_global_property() { + props.set("sonar.log.level.web.jmx", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_property_over_global_property() { + props.set("sonar.log.level.web", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test public void root_logger_level_defaults_to_INFO_if_web_property_has_invalid_value() { props.set("sonar.log.level.web", "DodoDouh!"); @@ -271,6 +328,14 @@ public class WebServerProcessLoggingTest { } @Test + public void jmx_loggers_level_defaults_to_INFO_if_wedb_jmx_property_has_invalid_value() { + props.set("sonar.log.level.web.jmx", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test public void fail_with_IAE_if_global_property_unsupported_level() { props.set("sonar.log.level", "ERROR"); @@ -310,6 +375,41 @@ public class WebServerProcessLoggingTest { underTest.configure(props); } + @Test + public void fail_with_IAE_if_web_jmx_property_unsupported_level() { + 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.configure(props); + } + + @Test + public void configure_defines_hardcoded_levels() { + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_global_property() { + props.set("sonar.log.level", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_ce_property() { + props.set("sonar.log.level.ce", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + private void verifyRootLogLevel(LoggerContext ctx, Level expected) { Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); assertThat(rootLogger.getLevel()).isEqualTo(expected); @@ -323,4 +423,32 @@ public class WebServerProcessLoggingTest { assertThat(ctx.getLogger("es").getLevel()).isEqualTo(expected); } + private void verifyJmxLogLevel(LoggerContext ctx, Level expected) { + assertThat(ctx.getLogger("javax.management.remote.timeout").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.rmi").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.mbeanserver").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.loader").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.tcp").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.server.call").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.dgc").getLevel()).isEqualTo(expected); + } + + private void verifyImmutableLogLevels(LoggerContext ctx) { + assertThat(ctx.getLogger("rails").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.apache.ibatis").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql.ResultSet").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.sonar.MEASURE_FILTER").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.elasticsearch").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.node").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.http").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("ch.qos.logback").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.apache.catalina").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.coyote").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.jasper").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.tomcat").getLevel()).isEqualTo(Level.INFO); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java index c3b6f9b8d68..9dc813ddaf5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java @@ -20,25 +20,32 @@ package org.sonar.server.platform; import ch.qos.logback.classic.Level; +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.io.IOException; -import org.apache.commons.io.FileUtils; 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.api.config.MapSettings; import org.sonar.api.config.Settings; import org.sonar.api.utils.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; -import org.sonar.process.LogbackHelper; import org.sonar.process.ProcessProperties; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.process.logging.LogbackHelper; +import org.sonar.server.app.ServerProcessLogging; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +@RunWith(DataProviderRunner.class) public class ServerLoggingTest { @Rule @@ -68,76 +75,39 @@ public class ServerLoggingTest { } @Test - public void getCurrentLogFile() throws IOException { - File dir = temp.newFolder(); - File logFile = new File(dir, "sonar.log"); - FileUtils.touch(logFile); - settings.setProperty(ProcessProperties.PATH_LOGS, dir.getAbsolutePath()); + @UseDataProvider("supportedSonarApiLevels") + public void changeLevel_calls_changeRoot_with_LogLevelConfig_and_level_converted_to_logback_class_then_log_INFO_message(LoggerLevel level) { + ServerProcessLogging serverProcessLogging = mock(ServerProcessLogging.class); + LogLevelConfig logLevelConfig = LogLevelConfig.newBuilder().build(); + when(serverProcessLogging.getLogLevelConfig()).thenReturn(logLevelConfig); - assertThat(underTest.getCurrentLogFile()).isEqualTo(logFile); - } - - @Test - public void configureHardcodedLevels() { - LogbackHelper logbackHelper = mock(LogbackHelper.class); - ServerLogging.configureHardcodedLevels(logbackHelper); + underTest.changeLevel(serverProcessLogging, level); - verifyHardcodedLevels(logbackHelper); + verify(logbackHelper).changeRoot(logLevelConfig, Level.valueOf(level.name())); } - @Test - public void changeLevel_throws_IAE_if_level_is_WARN() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("WARN log level is not supported (allowed levels are ["); - - underTest.changeLevel(LoggerLevel.WARN); + @DataProvider + public static Object[][] supportedSonarApiLevels() { + return new Object[][] { + {LoggerLevel.INFO}, + {LoggerLevel.DEBUG}, + {LoggerLevel.TRACE} + }; } @Test - public void changeLevel_throws_IAE_if_level_is_ERROR() { + public void changeLevel_fails_with_IAE_when_level_is_ERROR() { expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("ERROR log level is not supported (allowed levels are ["); + expectedException.expectMessage("ERROR log level is not supported (allowed levels are [TRACE, DEBUG, INFO])"); - underTest.changeLevel(LoggerLevel.ERROR); + underTest.changeLevel(mock(ServerProcessLogging.class), LoggerLevel.ERROR); } @Test - public void changeLevel_changes_root_logger_level_to_INFO() { - underTest.changeLevel(LoggerLevel.INFO); - - verify(logbackHelper).configureRootLogLevel(Level.INFO); - verifyHardcodedLevels(logbackHelper); - } - - @Test - public void changeLevel_changes_root_logger_level_to_DEBUG() { - underTest.changeLevel(LoggerLevel.DEBUG); - - verify(logbackHelper).configureRootLogLevel(Level.DEBUG); - verifyHardcodedLevels(logbackHelper); - } - - @Test - public void changeLevel_changes_root_logger_level_to_TRACE() { - underTest.changeLevel(LoggerLevel.TRACE); - - verify(logbackHelper).configureRootLogLevel(Level.TRACE); - verifyHardcodedLevels(logbackHelper); - } + public void changeLevel_fails_with_IAE_when_level_is_WARN() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("WARN log level is not supported (allowed levels are [TRACE, DEBUG, INFO])"); - private void verifyHardcodedLevels(LogbackHelper logbackHelper) { - verify(logbackHelper).configureLogger("rails", Level.WARN); - verify(logbackHelper).configureLogger("org.apache.ibatis", Level.WARN); - verify(logbackHelper).configureLogger("java.sql", Level.WARN); - verify(logbackHelper).configureLogger("java.sql.ResultSet", Level.WARN); - verify(logbackHelper).configureLogger("org.sonar.MEASURE_FILTER", Level.WARN); - verify(logbackHelper).configureLogger("org.elasticsearch", Level.INFO); - verify(logbackHelper).configureLogger("org.elasticsearch.node", Level.INFO); - verify(logbackHelper).configureLogger("org.elasticsearch.http", Level.INFO); - verify(logbackHelper).configureLogger("ch.qos.logback", Level.WARN); - verify(logbackHelper).configureLogger("org.apache.catalina", Level.INFO); - verify(logbackHelper).configureLogger("org.apache.coyote", Level.INFO); - verify(logbackHelper).configureLogger("org.apache.jasper", Level.INFO); - verify(logbackHelper).configureLogger("org.apache.tomcat", Level.INFO); + underTest.changeLevel(mock(ServerProcessLogging.class), LoggerLevel.WARN); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/ChangeLogLevelActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/ChangeLogLevelActionTest.java index bdf1f6abf0f..ca30fa204e7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/ChangeLogLevelActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/ChangeLogLevelActionTest.java @@ -25,6 +25,7 @@ import org.junit.rules.ExpectedException; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.ce.http.CeHttpClient; import org.sonar.db.Database; +import org.sonar.server.app.WebServerProcessLogging; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.platform.ServerLogging; import org.sonar.server.tester.UserSessionRule; @@ -43,7 +44,8 @@ public class ChangeLogLevelActionTest { private ServerLogging serverLogging = mock(ServerLogging.class); private Database db = mock(Database.class); private CeHttpClient ceHttpClient = mock(CeHttpClient.class); - private ChangeLogLevelAction underTest = new ChangeLogLevelAction(userSession, serverLogging, db, ceHttpClient); + private WebServerProcessLogging webServerProcessLogging = new WebServerProcessLogging(); + private ChangeLogLevelAction underTest = new ChangeLogLevelAction(userSession, serverLogging, db, ceHttpClient, webServerProcessLogging); private WsActionTester actionTester = new WsActionTester(underTest); @Test @@ -71,7 +73,7 @@ public class ChangeLogLevelActionTest { .setMethod("POST") .execute(); - verify(serverLogging).changeLevel(LoggerLevel.DEBUG); + verify(serverLogging).changeLevel(webServerProcessLogging, LoggerLevel.DEBUG); verify(ceHttpClient).changeLogLevel(LoggerLevel.DEBUG); verify(db).enableSqlLogging(false); } @@ -85,7 +87,7 @@ public class ChangeLogLevelActionTest { .setMethod("POST") .execute(); - verify(serverLogging).changeLevel(LoggerLevel.TRACE); + verify(serverLogging).changeLevel(webServerProcessLogging, LoggerLevel.TRACE); verify(ceHttpClient).changeLogLevel(LoggerLevel.TRACE); verify(db).enableSqlLogging(true); } diff --git a/sonar-application/src/main/java/org/sonar/application/AppLogging.java b/sonar-application/src/main/java/org/sonar/application/AppLogging.java index 69ba954e4f1..284fef1466d 100644 --- a/sonar-application/src/main/java/org/sonar/application/AppLogging.java +++ b/sonar-application/src/main/java/org/sonar/application/AppLogging.java @@ -24,12 +24,14 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.FileAppender; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.process.logging.LogbackHelper; import org.sonar.process.ProcessId; import org.sonar.process.Props; +import org.sonar.process.logging.RootLoggerConfig; import static org.slf4j.Logger.ROOT_LOGGER_NAME; -import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder; +import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; import static org.sonar.process.monitor.StreamGobbler.LOGGER_GOBBLER; /** @@ -108,7 +110,7 @@ class AppLogging { private static final String CONSOLE_PLAIN_APPENDER = "CONSOLE"; private static final String APP_CONSOLE_APPENDER = "APP_CONSOLE"; private static final String GOBBLER_PLAIN_CONSOLE = "GOBBLER_CONSOLE"; - private static final LogbackHelper.RootLoggerConfig APP_ROOT_LOGGER_CONFIG = newRootLoggerConfigBuilder() + private static final RootLoggerConfig APP_ROOT_LOGGER_CONFIG = newRootLoggerConfigBuilder() .setProcessId(ProcessId.APP) .build(); @@ -126,7 +128,7 @@ class AppLogging { } else { configureWithWrapperWritingToFile(ctx); } - helper.configureRootLogLevel(props, ProcessId.APP); + helper.apply(LogLevelConfig.newBuilder().rootLevelFor(ProcessId.APP).build(), props); return ctx; } diff --git a/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java b/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java index c58854b8032..c382d61058f 100644 --- a/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java +++ b/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java @@ -39,7 +39,7 @@ 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.logging.LogbackHelper; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; diff --git a/sonar-db/src/main/java/org/sonar/db/DefaultDatabase.java b/sonar-db/src/main/java/org/sonar/db/DefaultDatabase.java index 5a1995161d9..8520edd25a8 100644 --- a/sonar-db/src/main/java/org/sonar/db/DefaultDatabase.java +++ b/sonar-db/src/main/java/org/sonar/db/DefaultDatabase.java @@ -40,7 +40,7 @@ import org.sonar.db.dialect.DialectUtils; import org.sonar.db.profiling.NullConnectionInterceptor; import org.sonar.db.profiling.ProfiledConnectionInterceptor; import org.sonar.db.profiling.ProfiledDataSource; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import static java.lang.String.format; diff --git a/sonar-db/src/test/java/org/sonar/db/DefaultDatabaseTest.java b/sonar-db/src/test/java/org/sonar/db/DefaultDatabaseTest.java index 454dcbac3ac..c130cfa5799 100644 --- a/sonar-db/src/test/java/org/sonar/db/DefaultDatabaseTest.java +++ b/sonar-db/src/test/java/org/sonar/db/DefaultDatabaseTest.java @@ -25,7 +25,7 @@ import org.junit.Test; import org.sonar.api.config.Settings; import org.sonar.api.config.MapSettings; import org.sonar.db.dialect.PostgreSql; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/sonar-db/src/test/java/org/sonar/db/TestDb.java b/sonar-db/src/test/java/org/sonar/db/TestDb.java index 4f6d0c0c0b0..c1a99c0e896 100644 --- a/sonar-db/src/test/java/org/sonar/db/TestDb.java +++ b/sonar-db/src/test/java/org/sonar/db/TestDb.java @@ -41,7 +41,7 @@ import org.sonar.api.config.MapSettings; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.db.dialect.H2; -import org.sonar.process.LogbackHelper; +import org.sonar.process.logging.LogbackHelper; /** * This class should be call using @ClassRule in order to create the schema once (ft @Rule is used |