]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8335 move LogBackHelper to subpackage logging
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 28 Nov 2016 15:54:28 +0000 (16:54 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 1 Dec 2016 11:29:39 +0000 (12:29 +0100)
24 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java [new file with mode: 0644]
server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java [deleted file]
server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java [new file with mode: 0644]
server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java
server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java
server/sonar-server/src/main/java/org/sonar/ce/log/CeProcessLogging.java
server/sonar-server/src/main/java/org/sonar/server/app/ServerProcessLogging.java
server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java
server/sonar-server/src/main/java/org/sonar/server/app/WebServerProcessLogging.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerLogging.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ChangeLogLevelAction.java
server/sonar-server/src/test/java/org/sonar/ce/log/CeLoggingTest.java
server/sonar-server/src/test/java/org/sonar/ce/log/CeProcessLoggingTest.java
server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java
server/sonar-server/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/ServerLoggingTest.java
sonar-application/src/main/java/org/sonar/application/AppLogging.java
sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java
sonar-db/src/main/java/org/sonar/db/DefaultDatabase.java
sonar-db/src/test/java/org/sonar/db/DefaultDatabaseTest.java
sonar-db/src/test/java/org/sonar/db/TestDb.java

index fe082dbbc7a4b8d6d2eafd5ca1bb8aa266928fbe..c9711c9de2ec74e70d8a5c7c9f5d407caf947d2d 100644 (file)
@@ -63,7 +63,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;
diff --git a/server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java b/server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java
deleted file mode 100644 (file)
index ac9351d..0000000
+++ /dev/null
@@ -1,471 +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.joran.JoranConfigurator;
-import ch.qos.logback.classic.jul.LevelChangePropagator;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.spi.LoggerContextListener;
-import ch.qos.logback.core.ConsoleAppender;
-import ch.qos.logback.core.Context;
-import ch.qos.logback.core.FileAppender;
-import ch.qos.logback.core.joran.spi.JoranException;
-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.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-import javax.annotation.CheckForNull;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.LoggerFactory;
-
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-import static org.slf4j.Logger.ROOT_LOGGER_NAME;
-
-/**
- * Helps to configure Logback in a programmatic way, without using XML.
- */
-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";
-  private static final Level[] ALLOWED_ROOT_LOG_LEVELS = new Level[] {Level.TRACE, Level.DEBUG, Level.INFO};
-
-  public static Collection<Level> allowedLogLevels() {
-    return Arrays.asList(ALLOWED_ROOT_LOG_LEVELS);
-  }
-
-  public LoggerContext getRootContext() {
-    org.slf4j.Logger logger;
-    while (!((logger = LoggerFactory.getLogger(ROOT_LOGGER_NAME)) instanceof Logger)) {
-      // It occurs when the initialization step is still not finished because of a race condition
-      // on ILoggerFactory.getILoggerFactory
-      // http://jira.qos.ch/browse/SLF4J-167
-      // Substitute loggers are used.
-      // http://www.slf4j.org/codes.html#substituteLogger
-      // Bug is not fixed in SLF4J 1.7.14.
-      try {
-        Thread.sleep(10);
-      } catch (InterruptedException e) {
-        Thread.currentThread().interrupt();
-      }
-    }
-    return ((Logger) logger).getLoggerContext();
-  }
-
-  public LoggerContextListener enableJulChangePropagation(LoggerContext loggerContext) {
-    LevelChangePropagator propagator = new LevelChangePropagator();
-    propagator.setContext(loggerContext);
-    propagator.start();
-    loggerContext.addListener(propagator);
-    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;
-    }
-
-    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);
-      }
-    }
-  }
-
-  public String buildLogPattern(LogbackHelper.RootLoggerConfig config) {
-    return LOG_FORMAT
-      .replace(PROCESS_NAME_PLACEHOLDER, config.getProcessId().getKey())
-      .replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern());
-  }
-
-  /**
-   * Creates a new {@link ConsoleAppender} to {@code System.out} with the specified name and log pattern.
-   *
-   * @see #buildLogPattern(RootLoggerConfig)
-   */
-  public ConsoleAppender<ILoggingEvent> newConsoleAppender(Context loggerContext, String name, String logPattern) {
-    PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
-    consoleEncoder.setContext(loggerContext);
-    consoleEncoder.setPattern(logPattern);
-    consoleEncoder.start();
-    ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
-    consoleAppender.setContext(loggerContext);
-    consoleAppender.setEncoder(consoleEncoder);
-    consoleAppender.setName(name);
-    consoleAppender.setTarget("System.out");
-    consoleAppender.start();
-    return consoleAppender;
-  }
-
-  /**
-   * Make logback configuration for a process to push all its logs to a log file.
-   * <p>
-   * <ul>
-   * <li>the file's name will use the prefix defined in {@link RootLoggerConfig#getProcessId()#getLogFilenamePrefix()}.</li>
-   * <li>the file will follow the rotation policy defined in property {@link #ROLLING_POLICY_PROPERTY} and
-   * the max number of files defined in property {@link #MAX_FILES_PROPERTY}</li>
-   * <li>the logs will follow the specified log pattern</li>
-   * </ul>
-   * </p>
-   *
-   * @see #buildLogPattern(RootLoggerConfig)
-   */
-  public FileAppender<ILoggingEvent> configureGlobalFileLog(Props props, RootLoggerConfig config, String logPattern) {
-    LoggerContext ctx = getRootContext();
-    Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
-    FileAppender<ILoggingEvent> fileAppender = newFileAppender(ctx, props, config, logPattern);
-    rootLogger.addAppender(fileAppender);
-    return fileAppender;
-  }
-
-  public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, LogbackHelper.RootLoggerConfig config, String logPattern) {
-    RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, config.getProcessId().getLogFilenamePrefix());
-    FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + config.getProcessId().getLogFilenamePrefix());
-    fileAppender.setContext(ctx);
-    PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
-    fileEncoder.setContext(ctx);
-    fileEncoder.setPattern(logPattern);
-    fileEncoder.start();
-    fileAppender.setEncoder(fileEncoder);
-    fileAppender.start();
-    return fileAppender;
-  }
-
-  /**
-   * Make the logback configuration for a sub process to correctly push all its logs to be read by a stream gobbler
-   * on the sub process's System.out.
-   *
-   * @see #buildLogPattern(RootLoggerConfig)
-   */
-  public void configureForSubprocessGobbler(Props props, String logPattern) {
-    if (isAllLogsToConsoleEnabled(props)) {
-      LoggerContext ctx = getRootContext();
-      ctx.getLogger(ROOT_LOGGER_NAME).addAppender(newConsoleAppender(ctx, "root_console", logPattern));
-    }
-  }
-
-  /**
-   * Finds out whether we are in testing environment (usually ITs) and logs of all processes must be forward to
-   * App's System.out. This is specified by the value of property {@link #ALL_LOGS_TO_CONSOLE_PROPERTY}.
-   */
-  public boolean isAllLogsToConsoleEnabled(Props props) {
-    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);
-    if (value == null) {
-      return null;
-    }
-
-    Level level = Level.toLevel(value, Level.INFO);
-    if (!isAllowed(level)) {
-      throw new IllegalArgumentException(format("log level %s in property %s is not a supported value (allowed levels are %s)",
-        level, propertyKey, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS)));
-    }
-    return level;
-  }
-
-  private static boolean isAllowed(Level level) {
-    for (Level allowedRootLogLevel : ALLOWED_ROOT_LOG_LEVELS) {
-      if (level == allowedRootLogLevel) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * 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();
-  }
-
-  /**
-   * Generally used to reset logback in logging tests
-   */
-  public void resetFromXml(String xmlResourcePath) throws JoranException {
-    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
-    JoranConfigurator configurator = new JoranConfigurator();
-    configurator.setContext(context);
-    context.reset();
-    configurator.doConfigure(LogbackHelper.class.getResource(xmlResourcePath));
-  }
-
-  public RollingPolicy createRollingPolicy(Context ctx, Props props, String filenamePrefix) {
-    String rollingPolicy = props.value(ROLLING_POLICY_PROPERTY, "time:yyyy-MM-dd");
-    int maxFiles = props.valueAsInt(MAX_FILES_PROPERTY, 7);
-    File logsDir = props.nonNullValueAsFile(ProcessProperties.PATH_LOGS);
-
-    if (rollingPolicy.startsWith("time:")) {
-      return new TimeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "time:"));
-
-    } else if (rollingPolicy.startsWith("size:")) {
-      return new SizeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "size:"));
-
-    } else if ("none".equals(rollingPolicy)) {
-      return new NoRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles);
-
-    } else {
-      throw new MessageException(format("Unsupported value for property %s: %s", ROLLING_POLICY_PROPERTY, rollingPolicy));
-    }
-  }
-
-  public abstract static class RollingPolicy {
-    protected final Context context;
-    final String filenamePrefix;
-    final File logsDir;
-    final int maxFiles;
-
-    RollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
-      this.context = context;
-      this.filenamePrefix = filenamePrefix;
-      this.logsDir = logsDir;
-      this.maxFiles = maxFiles;
-    }
-
-    public abstract FileAppender<ILoggingEvent> createAppender(String appenderName);
-  }
-
-  /**
-   * Log files are not rotated, for example for unix command logrotate is in place.
-   */
-  private static class NoRollingPolicy extends RollingPolicy {
-    private NoRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
-      super(context, filenamePrefix, logsDir, maxFiles);
-    }
-
-    @Override
-    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
-      FileAppender<ILoggingEvent> appender = new FileAppender<>();
-      appender.setContext(context);
-      appender.setFile(new File(logsDir, filenamePrefix + ".log").getAbsolutePath());
-      appender.setName(appenderName);
-      return appender;
-    }
-  }
-
-  /**
-   * Log files are rotated according to time (one file per day, month or year).
-   * See http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy
-   */
-  private static class TimeRollingPolicy extends RollingPolicy {
-    private final String datePattern;
-
-    private TimeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String datePattern) {
-      super(context, filenamePrefix, logsDir, maxFiles);
-      this.datePattern = datePattern;
-    }
-
-    @Override
-    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
-      RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
-      appender.setContext(context);
-      appender.setName(appenderName);
-      String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath();
-      appender.setFile(filePath);
-
-      TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();
-      rollingPolicy.setContext(context);
-      rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%d{" + datePattern + "}.log"));
-      rollingPolicy.setMaxHistory(maxFiles);
-      rollingPolicy.setParent(appender);
-      rollingPolicy.start();
-      appender.setRollingPolicy(rollingPolicy);
-
-      return appender;
-    }
-  }
-
-  /**
-   * Log files are rotated according to their size.
-   * See http://logback.qos.ch/manual/appenders.html#FixedWindowRollingPolicy
-   */
-  private static class SizeRollingPolicy extends RollingPolicy {
-    private final String size;
-
-    private SizeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String parameter) {
-      super(context, filenamePrefix, logsDir, maxFiles);
-      this.size = parameter;
-    }
-
-    @Override
-    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
-      RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
-      appender.setContext(context);
-      appender.setName(appenderName);
-      String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath();
-      appender.setFile(filePath);
-
-      SizeBasedTriggeringPolicy<ILoggingEvent> trigger = new SizeBasedTriggeringPolicy<>(size);
-      trigger.setContext(context);
-      trigger.start();
-      appender.setTriggeringPolicy(trigger);
-
-      FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
-      rollingPolicy.setContext(context);
-      rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%i.log"));
-      rollingPolicy.setMinIndex(1);
-      rollingPolicy.setMaxIndex(maxFiles);
-      rollingPolicy.setParent(appender);
-      rollingPolicy.start();
-      appender.setRollingPolicy(rollingPolicy);
-
-      return appender;
-    }
-  }
-
-  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/LogbackHelper.java b/server/sonar-process/src/main/java/org/sonar/process/logging/LogbackHelper.java
new file mode 100644 (file)
index 0000000..15b129c
--- /dev/null
@@ -0,0 +1,475 @@
+/*
+ * 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.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.classic.jul.LevelChangePropagator;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggerContextListener;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.Context;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.joran.spi.JoranException;
+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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.slf4j.Logger.ROOT_LOGGER_NAME;
+
+/**
+ * Helps to configure Logback in a programmatic way, without using XML.
+ */
+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";
+  private static final Level[] ALLOWED_ROOT_LOG_LEVELS = new Level[] {Level.TRACE, Level.DEBUG, Level.INFO};
+
+  public static Collection<Level> allowedLogLevels() {
+    return Arrays.asList(ALLOWED_ROOT_LOG_LEVELS);
+  }
+
+  public LoggerContext getRootContext() {
+    org.slf4j.Logger logger;
+    while (!((logger = LoggerFactory.getLogger(ROOT_LOGGER_NAME)) instanceof Logger)) {
+      // It occurs when the initialization step is still not finished because of a race condition
+      // on ILoggerFactory.getILoggerFactory
+      // http://jira.qos.ch/browse/SLF4J-167
+      // Substitute loggers are used.
+      // http://www.slf4j.org/codes.html#substituteLogger
+      // Bug is not fixed in SLF4J 1.7.14.
+      try {
+        Thread.sleep(10);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+    return ((Logger) logger).getLoggerContext();
+  }
+
+  public LoggerContextListener enableJulChangePropagation(LoggerContext loggerContext) {
+    LevelChangePropagator propagator = new LevelChangePropagator();
+    propagator.setContext(loggerContext);
+    propagator.start();
+    loggerContext.addListener(propagator);
+    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;
+    }
+
+    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);
+      }
+    }
+  }
+
+  public String buildLogPattern(LogbackHelper.RootLoggerConfig config) {
+    return LOG_FORMAT
+      .replace(PROCESS_NAME_PLACEHOLDER, config.getProcessId().getKey())
+      .replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern());
+  }
+
+  /**
+   * Creates a new {@link ConsoleAppender} to {@code System.out} with the specified name and log pattern.
+   *
+   * @see #buildLogPattern(RootLoggerConfig)
+   */
+  public ConsoleAppender<ILoggingEvent> newConsoleAppender(Context loggerContext, String name, String logPattern) {
+    PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
+    consoleEncoder.setContext(loggerContext);
+    consoleEncoder.setPattern(logPattern);
+    consoleEncoder.start();
+    ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
+    consoleAppender.setContext(loggerContext);
+    consoleAppender.setEncoder(consoleEncoder);
+    consoleAppender.setName(name);
+    consoleAppender.setTarget("System.out");
+    consoleAppender.start();
+    return consoleAppender;
+  }
+
+  /**
+   * Make logback configuration for a process to push all its logs to a log file.
+   * <p>
+   * <ul>
+   * <li>the file's name will use the prefix defined in {@link RootLoggerConfig#getProcessId()#getLogFilenamePrefix()}.</li>
+   * <li>the file will follow the rotation policy defined in property {@link #ROLLING_POLICY_PROPERTY} and
+   * the max number of files defined in property {@link #MAX_FILES_PROPERTY}</li>
+   * <li>the logs will follow the specified log pattern</li>
+   * </ul>
+   * </p>
+   *
+   * @see #buildLogPattern(RootLoggerConfig)
+   */
+  public FileAppender<ILoggingEvent> configureGlobalFileLog(Props props, RootLoggerConfig config, String logPattern) {
+    LoggerContext ctx = getRootContext();
+    Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+    FileAppender<ILoggingEvent> fileAppender = newFileAppender(ctx, props, config, logPattern);
+    rootLogger.addAppender(fileAppender);
+    return fileAppender;
+  }
+
+  public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, LogbackHelper.RootLoggerConfig config, String logPattern) {
+    RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, config.getProcessId().getLogFilenamePrefix());
+    FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + config.getProcessId().getLogFilenamePrefix());
+    fileAppender.setContext(ctx);
+    PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
+    fileEncoder.setContext(ctx);
+    fileEncoder.setPattern(logPattern);
+    fileEncoder.start();
+    fileAppender.setEncoder(fileEncoder);
+    fileAppender.start();
+    return fileAppender;
+  }
+
+  /**
+   * Make the logback configuration for a sub process to correctly push all its logs to be read by a stream gobbler
+   * on the sub process's System.out.
+   *
+   * @see #buildLogPattern(RootLoggerConfig)
+   */
+  public void configureForSubprocessGobbler(Props props, String logPattern) {
+    if (isAllLogsToConsoleEnabled(props)) {
+      LoggerContext ctx = getRootContext();
+      ctx.getLogger(ROOT_LOGGER_NAME).addAppender(newConsoleAppender(ctx, "root_console", logPattern));
+    }
+  }
+
+  /**
+   * Finds out whether we are in testing environment (usually ITs) and logs of all processes must be forward to
+   * App's System.out. This is specified by the value of property {@link #ALL_LOGS_TO_CONSOLE_PROPERTY}.
+   */
+  public boolean isAllLogsToConsoleEnabled(Props props) {
+    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);
+    if (value == null) {
+      return null;
+    }
+
+    Level level = Level.toLevel(value, Level.INFO);
+    if (!isAllowed(level)) {
+      throw new IllegalArgumentException(format("log level %s in property %s is not a supported value (allowed levels are %s)",
+        level, propertyKey, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS)));
+    }
+    return level;
+  }
+
+  private static boolean isAllowed(Level level) {
+    for (Level allowedRootLogLevel : ALLOWED_ROOT_LOG_LEVELS) {
+      if (level == allowedRootLogLevel) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * 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();
+  }
+
+  /**
+   * Generally used to reset logback in logging tests
+   */
+  public void resetFromXml(String xmlResourcePath) throws JoranException {
+    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+    JoranConfigurator configurator = new JoranConfigurator();
+    configurator.setContext(context);
+    context.reset();
+    configurator.doConfigure(LogbackHelper.class.getResource(xmlResourcePath));
+  }
+
+  public RollingPolicy createRollingPolicy(Context ctx, Props props, String filenamePrefix) {
+    String rollingPolicy = props.value(ROLLING_POLICY_PROPERTY, "time:yyyy-MM-dd");
+    int maxFiles = props.valueAsInt(MAX_FILES_PROPERTY, 7);
+    File logsDir = props.nonNullValueAsFile(ProcessProperties.PATH_LOGS);
+
+    if (rollingPolicy.startsWith("time:")) {
+      return new TimeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "time:"));
+
+    } else if (rollingPolicy.startsWith("size:")) {
+      return new SizeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "size:"));
+
+    } else if ("none".equals(rollingPolicy)) {
+      return new NoRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles);
+
+    } else {
+      throw new MessageException(format("Unsupported value for property %s: %s", ROLLING_POLICY_PROPERTY, rollingPolicy));
+    }
+  }
+
+  public abstract static class RollingPolicy {
+    protected final Context context;
+    final String filenamePrefix;
+    final File logsDir;
+    final int maxFiles;
+
+    RollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
+      this.context = context;
+      this.filenamePrefix = filenamePrefix;
+      this.logsDir = logsDir;
+      this.maxFiles = maxFiles;
+    }
+
+    public abstract FileAppender<ILoggingEvent> createAppender(String appenderName);
+  }
+
+  /**
+   * Log files are not rotated, for example for unix command logrotate is in place.
+   */
+  private static class NoRollingPolicy extends RollingPolicy {
+    private NoRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+    }
+
+    @Override
+    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
+      FileAppender<ILoggingEvent> appender = new FileAppender<>();
+      appender.setContext(context);
+      appender.setFile(new File(logsDir, filenamePrefix + ".log").getAbsolutePath());
+      appender.setName(appenderName);
+      return appender;
+    }
+  }
+
+  /**
+   * Log files are rotated according to time (one file per day, month or year).
+   * See http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy
+   */
+  private static class TimeRollingPolicy extends RollingPolicy {
+    private final String datePattern;
+
+    private TimeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String datePattern) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+      this.datePattern = datePattern;
+    }
+
+    @Override
+    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
+      RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
+      appender.setContext(context);
+      appender.setName(appenderName);
+      String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath();
+      appender.setFile(filePath);
+
+      TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();
+      rollingPolicy.setContext(context);
+      rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%d{" + datePattern + "}.log"));
+      rollingPolicy.setMaxHistory(maxFiles);
+      rollingPolicy.setParent(appender);
+      rollingPolicy.start();
+      appender.setRollingPolicy(rollingPolicy);
+
+      return appender;
+    }
+  }
+
+  /**
+   * Log files are rotated according to their size.
+   * See http://logback.qos.ch/manual/appenders.html#FixedWindowRollingPolicy
+   */
+  private static class SizeRollingPolicy extends RollingPolicy {
+    private final String size;
+
+    private SizeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String parameter) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+      this.size = parameter;
+    }
+
+    @Override
+    public FileAppender<ILoggingEvent> createAppender(String appenderName) {
+      RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
+      appender.setContext(context);
+      appender.setName(appenderName);
+      String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath();
+      appender.setFile(filePath);
+
+      SizeBasedTriggeringPolicy<ILoggingEvent> trigger = new SizeBasedTriggeringPolicy<>(size);
+      trigger.setContext(context);
+      trigger.start();
+      appender.setTriggeringPolicy(trigger);
+
+      FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
+      rollingPolicy.setContext(context);
+      rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%i.log"));
+      rollingPolicy.setMinIndex(1);
+      rollingPolicy.setMaxIndex(maxFiles);
+      rollingPolicy.setParent(appender);
+      rollingPolicy.start();
+      appender.setRollingPolicy(rollingPolicy);
+
+      return appender;
+    }
+  }
+
+  public enum LogDomain {
+    SQL("sql"),
+    ES_CLIENT("es"),
+    JMX("jmx");
+
+    private final String key;
+
+    LogDomain(String key) {
+      this.key = key;
+    }
+  }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java b/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java
deleted file mode 100644 (file)
index 3ffc86c..0000000
+++ /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/LogbackHelperTest.java b/server/sonar-process/src/test/java/org/sonar/process/logging/LogbackHelperTest.java
new file mode 100644 (file)
index 0000000..1037de3
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * 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.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 org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+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");
+    }
+  }
+}
index d0216cc89f691f7a1592046076a92a5bf1d7d155..5d3e857bedecc66cfc13526e67dec51bebdca856 100644 (file)
 package org.sonar.search;
 
 import ch.qos.logback.classic.LoggerContext;
-import org.sonar.process.LogbackHelper;
+import org.sonar.process.logging.LogbackHelper;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
 
-import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
+import static org.sonar.process.logging.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
 
 public class SearchLogging {
 
index c1a182097045f9c137679eaa44ec9e7024b14ee4..2017d61a19db05e2954911b0011febfeef5d21c6 100644 (file)
@@ -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;
 
index 566aa1be7d0056a5700a45622750059452242542..76fdafff71410d83c5c079e411ab3d916d7b41e8 100644 (file)
  */
 package org.sonar.ce.log;
 
-import org.sonar.process.LogbackHelper;
+import org.sonar.process.logging.LogbackHelper;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
 import org.sonar.server.app.ServerProcessLogging;
 
 import static org.sonar.ce.log.CeLogging.MDC_CE_TASK_UUID;
-import static org.sonar.process.LogbackHelper.LogDomain;
+import static org.sonar.process.logging.LogbackHelper.LogDomain;
 
 /**
  * Configure logback for the Compute Engine process. Logs are written to file "ce.log" in SQ's log directory.
index c8c0d9bc6b44b3385de1a5ffacf407e786c1fc07..4692e71401c13f21f6899bc38fa5180d35e4c3ae 100644 (file)
@@ -22,12 +22,12 @@ package org.sonar.server.app;
 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.LogbackHelper;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
 import org.sonar.server.platform.ServerLogging;
 
-import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
+import static org.sonar.process.logging.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
 
 public abstract class ServerProcessLogging {
   protected static final Set<String> JMX_RMI_LOGGER_NAMES = ImmutableSet.of(
index 18792c1676f3e7facb1d08502e2f2541a2878376..cb938e59f0e6e146485aef4df77b2d188b068583 100644 (file)
@@ -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 {
index 6b101065be5f42267dfda2be067c61231c090610..202b9e1f5ce46a5aba6c2a6c837f15310fce7c22 100644 (file)
@@ -21,11 +21,11 @@ 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.LogbackHelper;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
 
-import static org.sonar.process.LogbackHelper.LogDomain;
+import static org.sonar.process.logging.LogbackHelper.LogDomain;
 import static org.sonar.server.platform.web.requestid.RequestIdMDCStorage.HTTP_REQUEST_ID_MDC_KEY;
 
 /**
index 5fe0cbb7d3b89625515e76d213ddfe79b7993d23..2f996b7bca15b954753a3905075e7868e7026058 100644 (file)
@@ -29,7 +29,7 @@ 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;
 
 @ServerSide
index d2543fb2b938f5a61bc44f4f31f1f415ca713d8b..6fecd6991dc9a8848e6de2231566d47ee56c39ba 100644 (file)
@@ -37,7 +37,7 @@ 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.issue.index.IssueIndex;
index 74917769dab9de39ac3b0de00a9e36428eb66dbe..be2d3602580c408c42619ea4cf778b3dcaeff9cd 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.db.Database;
 import org.sonar.server.platform.ServerLogging;
 import org.sonar.server.user.UserSession;
 
-import static org.sonar.process.LogbackHelper.allowedLogLevels;
+import static org.sonar.process.logging.LogbackHelper.allowedLogLevels;
 
 public class ChangeLogLevelAction implements SystemWsAction {
 
index c251892a2840a3628e7eb6917176afb0a9bebf4b..53c28708c2e2887296f7824c21e9d409a4c000b7 100644 (file)
@@ -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;
index 41474bed5a954e34ebf9fca096266419b6cf548b..d2bc08de935693b88606c290bd9765e5a5d46ada 100644 (file)
@@ -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;
 
index 3099b2bc34e1cff1f7df7d60f1b7b148f9466cec..01f919098d3f25b23d314f2bd89eeb14652639ed 100644 (file)
@@ -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;
index a81077b67934f70f1f141f9aab401954326adf25..5a7d401f02708bb5ae04648a6b666ca3e0fe2a9b 100644 (file)
@@ -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;
 
index c3b6f9b8d68045bf819289f82e7a7220a95d26d0..04ed09576185d5a702530795a879af6e4f04e222 100644 (file)
@@ -31,7 +31,7 @@ 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.logging.LogbackHelper;
 import org.sonar.process.ProcessProperties;
 
 import static org.assertj.core.api.Assertions.assertThat;
index 69ba954e4f1fdeac0332578a069b3302724ff8ec..992cca0ec65f18b0f4060c080048aebc77de8d0b 100644 (file)
@@ -24,12 +24,12 @@ 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.LogbackHelper;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
 
 import static org.slf4j.Logger.ROOT_LOGGER_NAME;
-import static org.sonar.process.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
+import static org.sonar.process.logging.LogbackHelper.RootLoggerConfig.newRootLoggerConfigBuilder;
 import static org.sonar.process.monitor.StreamGobbler.LOGGER_GOBBLER;
 
 /**
index c58854b8032fe8cb6578ee782147e718a27fc499..c382d61058f2db86219fa7dad560c89fe4ea594e 100644 (file)
@@ -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;
 
index 5a1995161d9bcce46bee00e104ba1fc69f8e3e43..8520edd25a8a5c7548bdbf302f197207b2663890 100644 (file)
@@ -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;
 
index 454dcbac3aca891be3b3e7e7c63080cdb2927408..c130cfa57995eda79cbc483ef3c19e95ba2d19ed 100644 (file)
@@ -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;
index 4f6d0c0c0b025b696fe592a0885a2f6e385ce898..c1a99c0e8969da7b742814366a3fedcae58f883c 100644 (file)
@@ -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