]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6057 Make log rotation more configurable for production
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 13 Feb 2015 13:56:28 +0000 (14:56 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 19 Feb 2015 11:01:20 +0000 (12:01 +0100)
42 files changed:
pom.xml
server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml [deleted file]
server/sonar-process/pom.xml
server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java [deleted file]
server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java [new file with mode: 0644]
server/sonar-process/src/test/resources/logback-test.xml [new file with mode: 0644]
server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml [deleted file]
server/sonar-search/pom.xml
server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java [new file with mode: 0644]
server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
server/sonar-search/src/main/resources/org/sonar/search/logback.xml [deleted file]
server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
server/sonar-server/src/main/java/org/sonar/server/app/Logging.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/app/ProgrammaticLogbackValve.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java
server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/app/WebLogging.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java
server/sonar-server/src/main/resources/org/sonar/server/platform/logback.xml [deleted file]
server/sonar-server/src/test/java/org/sonar/server/app/ConnectorsTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java
server/sonar-server/src/test/java/org/sonar/server/app/LoggingTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java
server/sonar-server/src/test/java/org/sonar/server/app/TomcatAccessLogTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/app/WebLoggingTest.java [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/config/logback-access.xml [deleted file]
sonar-application/src/main/assembly/conf/sonar.properties
sonar-application/src/main/java/org/sonar/application/App.java
sonar-application/src/main/java/org/sonar/application/AppLogging.java [new file with mode: 0644]
sonar-application/src/main/resources/org/sonar/application/logback.xml [deleted file]
sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java [new file with mode: 0644]
sonar-application/src/test/resources/logback-test.xml
sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Logger.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Loggers.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/InMemoryLogger.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/Slf4jLoggers.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index cfd7c40779ce6bd912e734869aaded8b264c9c3b..b4b19ca6356559c0b911798383da9c8f933d7981 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -71,8 +71,8 @@
     <sonarSvn.version>1.0-RC1</sonarSvn.version>
     <h2.version>1.3.176</h2.version>
     <jetty.version>8.1.12.v20130726</jetty.version>
-    <logback.version>1.0.13</logback.version>
-    <slf4j.version>1.7.5</slf4j.version>
+    <logback.version>1.1.2</logback.version>
+    <slf4j.version>1.7.10</slf4j.version>
     <tomcat.version>8.0.18</tomcat.version>
     <elasticsearch.version>1.4.2</elasticsearch.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
deleted file mode 100644 (file)
index 298193e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<configuration/>
index f4da18c6757c25e9ae381fa2527c3a3dd4ed7625..03bbf3d7c816bc9b20d877b290e4b15fe57acf53 100644 (file)
   <name>SonarQube :: Process</name>
 
   <dependencies>
-
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-    </dependency>
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
     </dependency>
-
     <dependency>
       <groupId>commons-codec</groupId>
       <artifactId>commons-codec</artifactId>
@@ -34,6 +28,7 @@
       <groupId>commons-lang</groupId>
       <artifactId>commons-lang</artifactId>
     </dependency>
+
     <dependency>
       <groupId>com.google.code.findbugs</groupId>
       <artifactId>jsr305</artifactId>
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
new file mode 100644 (file)
index 0000000..862fc22
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.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 org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+/**
+ * Helps to configure Logback in a programmatic way, without using XML.
+ */
+public class LogbackHelper {
+
+  public static final String ROLLING_POLICY_PROPERTY = "sonar.log.rollingPolicy";
+  public static final String MAX_FILES_PROPERTY = "sonar.log.maxFiles";
+
+  public LoggerContext getRootContext() {
+    Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+    return rootLogger.getLoggerContext();
+  }
+
+  public LoggerContextListener enableJulChangePropagation(LoggerContext loggerContext) {
+    LevelChangePropagator propagator = new LevelChangePropagator();
+    propagator.setContext(loggerContext);
+    propagator.start();
+    loggerContext.addListener(propagator);
+    return propagator;
+  }
+
+  public ConsoleAppender newConsoleAppender(Context loggerContext, String name, String pattern) {
+    PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
+    consoleEncoder.setContext(loggerContext);
+    consoleEncoder.setPattern(pattern);
+    consoleEncoder.start();
+    ConsoleAppender consoleAppender = new ConsoleAppender();
+    consoleAppender.setContext(loggerContext);
+    consoleAppender.setEncoder(consoleEncoder);
+    consoleAppender.setName(name);
+    consoleAppender.setTarget("System.out");
+    consoleAppender.start();
+    return consoleAppender;
+  }
+
+  public Logger configureLogger(LoggerContext loggerContext, String loggerName, Level level) {
+    Logger logger = loggerContext.getLogger(loggerName);
+    logger.setLevel(level);
+    return logger;
+  }
+
+  /**
+   * 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(ProcessConstants.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 (rollingPolicy.equals("none")) {
+      return new NoRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles);
+
+    } else {
+      throw new MessageException(String.format("Unsupported value for property %s: %s", ROLLING_POLICY_PROPERTY, rollingPolicy));
+    }
+  }
+
+  public static abstract class RollingPolicy {
+    protected final Context context;
+    protected final String filenamePrefix;
+    protected final File logsDir;
+    protected final int maxFiles;
+
+    public RollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
+      this.context = context;
+      this.filenamePrefix = filenamePrefix;
+      this.logsDir = logsDir;
+      this.maxFiles = maxFiles;
+    }
+
+    public abstract FileAppender createAppender(String appenderName);
+  }
+
+  /**
+   * Log files are not rotated, for example for unix command logrotate is in place.
+   */
+  static class NoRollingPolicy extends RollingPolicy {
+    NoRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+    }
+
+    @Override
+    public FileAppender createAppender(String appenderName) {
+      FileAppender 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
+   */
+  static class TimeRollingPolicy extends RollingPolicy {
+    private final String datePattern;
+
+    public TimeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String datePattern) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+      this.datePattern = datePattern;
+    }
+
+    @Override
+    public FileAppender createAppender(String appenderName) {
+      RollingFileAppender appender = new RollingFileAppender();
+      appender.setContext(context);
+      appender.setName(appenderName);
+      appender.setFile(new File(logsDir, filenamePrefix + ".log").getAbsolutePath());
+
+      TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();
+      rollingPolicy.setContext(context);
+      rollingPolicy.setFileNamePattern(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
+   */
+  static class SizeRollingPolicy extends RollingPolicy {
+    private final String size;
+
+    SizeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String parameter) {
+      super(context, filenamePrefix, logsDir, maxFiles);
+      this.size = parameter;
+    }
+
+    @Override
+    public FileAppender createAppender(String appenderName) {
+      RollingFileAppender appender = new RollingFileAppender();
+      appender.setContext(context);
+      appender.setName(appenderName);
+      appender.setFile(new File(logsDir, filenamePrefix + ".log").getAbsolutePath());
+
+      SizeBasedTriggeringPolicy trigger = new SizeBasedTriggeringPolicy(size);
+      trigger.setContext(context);
+      trigger.start();
+      appender.setTriggeringPolicy(trigger);
+
+      FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
+      rollingPolicy.setContext(context);
+      rollingPolicy.setFileNamePattern(filenamePrefix + ".%i.log");
+      rollingPolicy.setMaxIndex(1);
+      rollingPolicy.setMaxIndex(maxFiles);
+      rollingPolicy.setMaxIndex(maxFiles);
+      rollingPolicy.setParent(appender);
+      rollingPolicy.start();
+      appender.setRollingPolicy(rollingPolicy);
+
+      return appender;
+    }
+  }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
deleted file mode 100644 (file)
index fe7f647..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.joran.JoranConfigurator;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.Appender;
-import ch.qos.logback.core.joran.spi.JoranException;
-import ch.qos.logback.core.util.StatusPrinter;
-import org.slf4j.LoggerFactory;
-
-public class ProcessLogging {
-
-  public void configure(Props props, String logbackXmlResource) {
-    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
-    try {
-      JoranConfigurator configurator = new JoranConfigurator();
-      configurator.setContext(context);
-      context.reset();
-      context.putProperty(ProcessConstants.PATH_LOGS, props.nonNullValue(ProcessConstants.PATH_LOGS));
-      doConfigure(configurator, logbackXmlResource);
-    } catch (JoranException ignored) {
-      // StatusPrinter will handle this
-    }
-    StatusPrinter.printInCaseOfErrorsOrWarnings(context);
-  }
-
-  public void addConsoleAppender() {
-    Logger consoleLogger = (Logger) LoggerFactory.getLogger("console");
-    Appender<ILoggingEvent> consoleAppender = consoleLogger.getAppender("CONSOLE");
-
-    Logger gobblerLogger = (Logger) LoggerFactory.getLogger("gobbler");
-    gobblerLogger.addAppender(consoleAppender);
-  }
-
-  /**
-   * Extracted only for unit testing
-   */
-  void doConfigure(JoranConfigurator configurator, String logbackXmlResource) throws JoranException {
-    configurator.doConfigure(getClass().getResource(logbackXmlResource));
-  }
-}
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
new file mode 100644 (file)
index 0000000..d761a2d
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class LogbackHelperTest {
+
+  Props props = new Props(new Properties());
+  LogbackHelper sut = new LogbackHelper();
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Before
+  public void setUp() throws Exception {
+    File dir = temp.newFolder();
+    props.set(ProcessConstants.PATH_LOGS, dir.getAbsolutePath());
+  }
+
+  @AfterClass
+  public static void resetLogback() throws Exception {
+    new LogbackHelper().resetFromXml("/logback-test.xml");
+  }
+
+  @Test
+  public void getRootContext() {
+    assertThat(sut.getRootContext()).isNotNull();
+  }
+
+  @Test
+  public void enableJulChangePropagation() {
+    LoggerContext ctx = sut.getRootContext();
+    int countListeners = ctx.getCopyOfListenerList().size();
+
+    LoggerContextListener propagator = sut.enableJulChangePropagation(ctx);
+    assertThat(ctx.getCopyOfListenerList().size()).isEqualTo(countListeners + 1);
+
+    ctx.removeListener(propagator);
+  }
+
+  @Test
+  public void newConsoleAppender() throws Exception {
+    LoggerContext ctx = sut.getRootContext();
+    ConsoleAppender appender = sut.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");
+  }
+
+  @Test
+  public void configureLogger() throws Exception {
+    LoggerContext ctx = sut.getRootContext();
+
+    Logger logger = sut.configureLogger(ctx, "my_logger", Level.WARN);
+
+    assertThat(logger.getLevel()).isEqualTo(Level.WARN);
+    assertThat(logger.getName()).isEqualTo("my_logger");
+  }
+
+  @Test
+  public void createRollingPolicy_defaults() throws Exception {
+    LoggerContext ctx = sut.getRootContext();
+    LogbackHelper.RollingPolicy policy = sut.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()).isEqualTo("sonar.%d{yyyy-MM-dd}.log");
+  }
+
+  @Test
+  public void createRollingPolicy_none() throws Exception {
+    props.set("sonar.log.rollingPolicy", "none");
+    LoggerContext ctx = sut.getRootContext();
+    LogbackHelper.RollingPolicy policy = sut.createRollingPolicy(ctx, props, "sonar");
+
+    Appender appender = policy.createAppender("SONAR_FILE");
+    assertThat(appender).isNotInstanceOf(RollingFileAppender.class).isInstanceOf(FileAppender.class);
+  }
+
+  @Test
+  public void createRollingPolicy_size() throws Exception {
+    props.set("sonar.log.rollingPolicy", "size:1MB");
+    props.set("sonar.log.maxFiles", "20");
+    LoggerContext ctx = sut.getRootContext();
+    LogbackHelper.RollingPolicy policy = sut.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()).isEqualTo("sonar.%i.log");
+    SizeBasedTriggeringPolicy triggeringPolicy = (SizeBasedTriggeringPolicy) fileAppender.getTriggeringPolicy();
+    assertThat(triggeringPolicy.getMaxFileSize()).isEqualTo("1MB");
+  }
+
+  @Test
+  public void createRollingPolicy_time() throws Exception {
+    props.set("sonar.log.rollingPolicy", "time:yyyy-MM");
+    props.set("sonar.log.maxFiles", "20");
+
+    LoggerContext ctx = sut.getRootContext();
+    LogbackHelper.RollingPolicy policy = sut.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()).isEqualTo("sonar.%d{yyyy-MM}.log");
+  }
+
+  @Test
+  public void createRollingPolicy_fail_if_unknown_policy() throws Exception {
+    props.set("sonar.log.rollingPolicy", "unknown:foo");
+    try {
+      LoggerContext ctx = sut.getRootContext();
+      sut.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/resources/logback-test.xml b/server/sonar-process/src/test/resources/logback-test.xml
new file mode 100644 (file)
index 0000000..f2d5a6a
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="debug">
+    <appender-ref ref="STDOUT"/>
+  </root>
+</configuration>
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
deleted file mode 100644 (file)
index 298193e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<configuration/>
index 26d3b35556010e2b4f34356bd6b7f5416076061d..9f836ef23bade14bc04e02ce270982236caaf4ef 100644 (file)
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
-      <scope>runtime</scope>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-core</artifactId>
+      <scope>provided</scope>
     </dependency>
 
     <!-- testing -->
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
new file mode 100644 (file)
index 0000000..2c2d45a
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.search;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.core.ConsoleAppender;
+import org.sonar.process.LogbackHelper;
+
+public class SearchLogging {
+
+  private static final String LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level  es[%logger{20}] %X %msg%n";
+
+  private LogbackHelper helper = new LogbackHelper();
+
+  public LoggerContext configure() {
+    LoggerContext ctx = helper.getRootContext();
+    ctx.reset();
+
+    ConsoleAppender consoleAppender = helper.newConsoleAppender(ctx, "CONSOLE", LOG_FORMAT);
+    Logger rootLogger = helper.configureLogger(ctx, Logger.ROOT_LOGGER_NAME, Level.INFO);
+    rootLogger.addAppender(consoleAppender);
+    return ctx;
+  }
+}
index 7689b91f2ef13d1fb5e2bd9e8fae980fca642957..f5b601ce21c7495e431adff674bdd13995fa667c 100644 (file)
@@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory;
 import org.sonar.process.MinimumViableSystem;
 import org.sonar.process.Monitored;
 import org.sonar.process.ProcessEntryPoint;
-import org.sonar.process.ProcessLogging;
 import org.sonar.process.Props;
 
 public class SearchServer implements Monitored {
@@ -76,7 +75,7 @@ public class SearchServer implements Monitored {
 
   public static void main(String... args) {
     ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
-    new ProcessLogging().configure(entryPoint.getProps(), "/org/sonar/search/logback.xml");
+    new SearchLogging().configure();
     SearchServer searchServer = new SearchServer(entryPoint.getProps());
     entryPoint.launch(searchServer);
   }
diff --git a/server/sonar-search/src/main/resources/org/sonar/search/logback.xml b/server/sonar-search/src/main/resources/org/sonar/search/logback.xml
deleted file mode 100644 (file)
index 3c24d95..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<!--
-  Configuration for default logger. Only used while embedded server is starting,
-  before proper logging configuration is loaded.
-
-  See http://logback.qos.ch/manual/configuration.html
--->
-<configuration debug="false">
-  <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
-  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>
-        %d{yyyy.MM.dd HH:mm:ss} %-5level sea[%logger{20}] %X %msg%n
-      </pattern>
-    </encoder>
-  </appender>
-
-  <root>
-    <level value="INFO"/>
-    <appender-ref ref="CONSOLE"/>
-  </root>
-
-</configuration>
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
new file mode 100644 (file)
index 0000000..f85772f
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.search;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.sonar.process.LogbackHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SearchLoggingTest {
+
+  SearchLogging sut = new SearchLogging();
+
+  @AfterClass
+  public static void resetLogback() throws Exception {
+    new LogbackHelper().resetFromXml("/logback-test.xml");
+  }
+
+  @Test
+  public void log_to_console() throws Exception {
+    LoggerContext ctx = sut.configure();
+
+    Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME);
+    Appender appender = root.getAppender("CONSOLE");
+    assertThat(appender).isInstanceOf(ConsoleAppender.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java b/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java
deleted file mode 100644 (file)
index 1518117..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.server.app;
-
-import org.apache.catalina.connector.Connector;
-import org.apache.catalina.startup.Tomcat;
-import org.sonar.process.Props;
-
-import javax.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Configuration of Tomcat connectors
- */
-class Connectors {
-
-  public static final String PROP_HTTPS_CIPHERS = "sonar.web.https.ciphers";
-
-  public static final int DISABLED_PORT = -1;
-  public static final String HTTP_PROTOCOL = "HTTP/1.1";
-  public static final String AJP_PROTOCOL = "AJP/1.3";
-
-  private Connectors() {
-    // only static stuff
-  }
-
-  static void configure(Tomcat tomcat, Props props) {
-    List<Connector> connectors = new ArrayList<>();
-    connectors.addAll(Arrays.asList(newHttpConnector(props), newAjpConnector(props), newHttpsConnector(props)));
-    connectors.removeAll(Collections.singleton(null));
-
-    verify(connectors);
-
-    tomcat.setConnector(connectors.get(0));
-    for (Connector connector : connectors) {
-      tomcat.getService().addConnector(connector);
-    }
-  }
-
-  private static void verify(List<Connector> connectors) {
-    if (connectors.isEmpty()) {
-      throw new IllegalStateException("HTTP connectors are disabled");
-    }
-    Set<Integer> ports = new HashSet<>();
-    for (Connector connector : connectors) {
-      int port = connector.getPort();
-      if (ports.contains(port)) {
-        throw new IllegalStateException(String.format("HTTP, AJP and HTTPS must not use the same port %d", port));
-      }
-      ports.add(port);
-    }
-  }
-
-  @Nullable
-  private static Connector newHttpConnector(Props props) {
-    Connector connector = null;
-    // Not named "sonar.web.http.port" to keep backward-compatibility
-    int port = props.valueAsInt("sonar.web.port", 9000);
-    if (port > DISABLED_PORT) {
-      connector = newConnector(props, HTTP_PROTOCOL, "http");
-      connector.setPort(port);
-    }
-    return connector;
-  }
-
-  @Nullable
-  private static Connector newAjpConnector(Props props) {
-    Connector connector = null;
-    int port = props.valueAsInt("sonar.ajp.port", DISABLED_PORT);
-    if (port > DISABLED_PORT) {
-      connector = newConnector(props, AJP_PROTOCOL, "http");
-      connector.setPort(port);
-    }
-    return connector;
-  }
-
-  @Nullable
-  private static Connector newHttpsConnector(Props props) {
-    Connector connector = null;
-    int port = props.valueAsInt("sonar.web.https.port", DISABLED_PORT);
-    if (port > DISABLED_PORT) {
-      connector = newConnector(props, HTTP_PROTOCOL, "https");
-      connector.setPort(port);
-      connector.setSecure(true);
-      connector.setScheme("https");
-      setConnectorAttribute(connector, "keyAlias", props.value("sonar.web.https.keyAlias"));
-      String keyPassword = props.value("sonar.web.https.keyPass", "changeit");
-      setConnectorAttribute(connector, "keyPass", keyPassword);
-      setConnectorAttribute(connector, "keystorePass", props.value("sonar.web.https.keystorePass", keyPassword));
-      setConnectorAttribute(connector, "keystoreFile", props.value("sonar.web.https.keystoreFile"));
-      setConnectorAttribute(connector, "keystoreType", props.value("sonar.web.https.keystoreType", "JKS"));
-      setConnectorAttribute(connector, "keystoreProvider", props.value("sonar.web.https.keystoreProvider"));
-      setConnectorAttribute(connector, "truststorePass", props.value("sonar.web.https.truststorePass", "changeit"));
-      setConnectorAttribute(connector, "truststoreFile", props.value("sonar.web.https.truststoreFile"));
-      setConnectorAttribute(connector, "truststoreType", props.value("sonar.web.https.truststoreType", "JKS"));
-      setConnectorAttribute(connector, "truststoreProvider", props.value("sonar.web.https.truststoreProvider"));
-      setConnectorAttribute(connector, "clientAuth", props.value("sonar.web.https.clientAuth", "false"));
-      setConnectorAttribute(connector, "ciphers", props.value(PROP_HTTPS_CIPHERS));
-      // SSLv3 must not be enable because of Poodle vulnerability
-      // See https://jira.codehaus.org/browse/SONAR-5860
-      setConnectorAttribute(connector, "sslEnabledProtocols", "TLSv1,TLSv1.1,TLSv1.2");
-      setConnectorAttribute(connector, "sslProtocol", "TLS");
-      setConnectorAttribute(connector, "SSLEnabled", true);
-    }
-    return connector;
-  }
-
-  private static Connector newConnector(Props props, String protocol, String scheme) {
-    Connector connector = new Connector(protocol);
-    connector.setURIEncoding("UTF-8");
-    connector.setProperty("address", props.value("sonar.web.host", "0.0.0.0"));
-    connector.setProperty("socket.soReuseAddress", "true");
-    configurePool(props, connector, scheme);
-    configureCompression(connector);
-    return connector;
-  }
-
-  private static void configurePool(Props props, Connector connector, String scheme) {
-    connector.setProperty("acceptorThreadCount", String.valueOf(2));
-    connector.setProperty("minSpareThreads", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".minThreads", 5)));
-    connector.setProperty("maxThreads", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".maxThreads", 50)));
-    connector.setProperty("acceptCount", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".acceptCount", 25)));
-  }
-
-  private static void configureCompression(Connector connector) {
-    connector.setProperty("compression", "on");
-    connector.setProperty("compressionMinSize", "1024");
-    connector.setProperty("compressableMimeType", "text/html,text/xml,text/plain,text/css,application/json,application/javascript");
-  }
-
-  private static void setConnectorAttribute(Connector c, String key, @Nullable Object value) {
-    if (value != null) {
-      c.setAttribute(key, value);
-    }
-  }
-}
index ea651f54e01b94c793b02dc0b14b46f264ecca1c..89b3a09377a621b6def7bbc3e155bb09173bdd71 100644 (file)
@@ -55,8 +55,8 @@ class EmbeddedTomcat {
     tomcat.getHost().setAutoDeploy(false);
     tomcat.getHost().setCreateDirs(false);
     tomcat.getHost().setDeployOnStartup(true);
-    Logging.configure(tomcat, props);
-    Connectors.configure(tomcat, props);
+    new TomcatAccessLog().configure(tomcat, props);
+    TomcatConnectors.configure(tomcat, props);
     webappContext = Webapp.configure(tomcat, props);
     try {
       tomcat.start();
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Logging.java b/server/sonar-server/src/main/java/org/sonar/server/app/Logging.java
deleted file mode 100644 (file)
index d83e495..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.server.app;
-
-import ch.qos.logback.access.tomcat.LogbackValve;
-import com.google.common.collect.ImmutableMap;
-import org.apache.catalina.LifecycleEvent;
-import org.apache.catalina.LifecycleListener;
-import org.apache.catalina.startup.Tomcat;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.slf4j.bridge.SLF4JBridgeHandler;
-import org.sonar.core.config.Logback;
-import org.sonar.core.profiling.Profiling;
-import org.sonar.process.ProcessConstants;
-import org.sonar.process.Props;
-
-import java.io.File;
-import java.util.Map;
-import java.util.logging.LogManager;
-
-class Logging {
-
-  private static final String LOG_COMMON_PREFIX = "%d{yyyy.MM.dd HH:mm:ss} %-5level ";
-  private static final String LOG_COMMON_SUFFIX = "%msg%n";
-
-  private static final String LOG_LOGFILE_SPECIFIC_PART = "web[%logger{20}] %X ";
-  private static final String LOG_FULL_SPECIFIC_PART = "%thread ";
-
-  private static final String LOGFILE_STANDARD_LOGGING_FORMAT = LOG_COMMON_PREFIX + LOG_LOGFILE_SPECIFIC_PART + LOG_COMMON_SUFFIX;
-  private static final String LOGFILE_FULL_LOGGING_FORMAT = LOG_COMMON_PREFIX + LOG_FULL_SPECIFIC_PART + LOG_LOGFILE_SPECIFIC_PART + LOG_COMMON_SUFFIX;
-
-  static final String ACCESS_RELATIVE_PATH = "WEB-INF/config/logback-access.xml";
-  static final String PROPERTY_ENABLE_ACCESS_LOGS = "sonar.web.accessLogs.enable";
-
-  private Logging() {
-  }
-
-  static void init(Props props) {
-    configureLogback(props);
-
-    // Configure java.util.logging, used by Tomcat, in order to forward to slf4j
-    LogManager.getLogManager().reset();
-    SLF4JBridgeHandler.install();
-  }
-
-  /**
-   * Configure Logback from classpath, with configuration from sonar.properties
-   */
-  private static void configureLogback(Props props) {
-    String configProfilingLevel = props.value(Profiling.CONFIG_PROFILING_LEVEL, "NONE");
-    Profiling.Level profilingLevel = Profiling.Level.fromConfigString(configProfilingLevel);
-    Map<String, String> variables = ImmutableMap.of(
-      ProcessConstants.PATH_LOGS, props.nonNullValue(ProcessConstants.PATH_LOGS),
-      "LOGGING_FORMAT", profilingLevel == Profiling.Level.FULL ? LOGFILE_FULL_LOGGING_FORMAT : LOGFILE_STANDARD_LOGGING_FORMAT);
-    Logback.configure("/org/sonar/server/platform/logback.xml", variables);
-  }
-
-  static void configure(Tomcat tomcat, Props props) {
-    tomcat.setSilent(true);
-    tomcat.getService().addLifecycleListener(new LifecycleLogger(LoggerFactory.getLogger(Logging.class)));
-    configureLogbackAccess(tomcat, props);
-  }
-
-  private static void configureLogbackAccess(Tomcat tomcat, Props props) {
-    if (props.valueAsBoolean(PROPERTY_ENABLE_ACCESS_LOGS, true)) {
-      LogbackValve valve = new LogbackValve();
-      valve.setQuiet(true);
-      valve.setFilename(new File(props.nonNullValue(ProcessConstants.PATH_WEB), ACCESS_RELATIVE_PATH).getAbsolutePath());
-      tomcat.getHost().getPipeline().addValve(valve);
-    }
-  }
-
-  static class LifecycleLogger implements LifecycleListener {
-    private Logger logger;
-
-    LifecycleLogger(Logger logger) {
-      this.logger = logger;
-    }
-
-    @Override
-    public void lifecycleEvent(LifecycleEvent event) {
-      if ("after_start".equals(event.getType())) {
-        logger.info("Web server is started");
-
-      } else if ("after_destroy".equals(event.getType())) {
-        logger.info("Web server is stopped");
-      }
-    }
-  }
-
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/ProgrammaticLogbackValve.java b/server/sonar-server/src/main/java/org/sonar/server/app/ProgrammaticLogbackValve.java
new file mode 100644 (file)
index 0000000..63d18c9
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import ch.qos.logback.access.tomcat.LogbackValve;
+import ch.qos.logback.core.util.ExecutorServiceUtil;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleState;
+import org.apache.commons.lang.reflect.FieldUtils;
+
+/**
+ * Implementation of {@link ch.qos.logback.access.tomcat.LogbackValve} that does not
+ * rely on the required file logback-access.xml. It allows to be configured
+ * programmatically.
+ */
+public class ProgrammaticLogbackValve extends LogbackValve {
+
+  @Override
+  public void startInternal() throws LifecycleException {
+    try {
+      // direct coupling with LogbackValve implementation
+      FieldUtils.writeField(this, "executorService", ExecutorServiceUtil.newExecutorService(), true);
+      FieldUtils.writeField(this, "started", true, true);
+      setState(LifecycleState.STARTING);
+    } catch (IllegalAccessException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
index d2e7f4e736a9df61f051d0e7effc3149abfe71b7..7fcf02955523356887b54eb2af790b3ca55e36b5 100644 (file)
@@ -65,7 +65,7 @@ class StartupLogs {
 
     AbstractHttp11JsseProtocol protocol = (AbstractHttp11JsseProtocol) connector.getProtocolHandler();
     sb.append(" | ciphers=");
-    if (props.contains(Connectors.PROP_HTTPS_CIPHERS)) {
+    if (props.contains(TomcatConnectors.PROP_HTTPS_CIPHERS)) {
       sb.append(StringUtils.join(protocol.getCiphersUsed(), ","));
     } else {
       sb.append("JVM defaults");
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
new file mode 100644 (file)
index 0000000..69b80a5
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import ch.qos.logback.access.PatternLayoutEncoder;
+import ch.qos.logback.core.FileAppender;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.startup.Tomcat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.LogbackHelper;
+import org.sonar.process.Props;
+
+class TomcatAccessLog {
+
+  public static final String PROPERTY_ENABLE = "sonar.web.accessLogs.enable";
+  public static final String PROPERTY_PATTERN = "sonar.web.accessLogs.pattern";
+
+  void configure(Tomcat tomcat, Props props) {
+    tomcat.setSilent(true);
+    tomcat.getService().addLifecycleListener(new LifecycleLogger(LoggerFactory.getLogger(TomcatAccessLog.class)));
+    configureLogbackAccess(tomcat, props);
+  }
+
+  private void configureLogbackAccess(Tomcat tomcat, Props props) {
+    if (props.valueAsBoolean(PROPERTY_ENABLE, true)) {
+      ProgrammaticLogbackValve valve = new ProgrammaticLogbackValve();
+      LogbackHelper helper = new LogbackHelper();
+      LogbackHelper.RollingPolicy policy = helper.createRollingPolicy(valve, props, "access");
+      FileAppender appender = policy.createAppender("ACCESS_LOG");
+      PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
+      fileEncoder.setContext(valve);
+      fileEncoder.setPattern(props.value(PROPERTY_PATTERN, "combined"));
+      fileEncoder.start();
+      appender.setEncoder(fileEncoder);
+      appender.start();
+      valve.addAppender(appender);
+      tomcat.getHost().getPipeline().addValve(valve);
+    }
+  }
+
+  static class LifecycleLogger implements LifecycleListener {
+    private Logger logger;
+
+    LifecycleLogger(Logger logger) {
+      this.logger = logger;
+    }
+
+    @Override
+    public void lifecycleEvent(LifecycleEvent event) {
+      if ("after_start".equals(event.getType())) {
+        logger.info("Web server is started");
+
+      } else if ("after_destroy".equals(event.getType())) {
+        logger.info("Web server is stopped");
+      }
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java
new file mode 100644 (file)
index 0000000..61f4718
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.sonar.process.Props;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Configuration of Tomcat connectors
+ */
+class TomcatConnectors {
+
+  public static final String PROP_HTTPS_CIPHERS = "sonar.web.https.ciphers";
+
+  public static final int DISABLED_PORT = -1;
+  public static final String HTTP_PROTOCOL = "HTTP/1.1";
+  public static final String AJP_PROTOCOL = "AJP/1.3";
+
+  private TomcatConnectors() {
+    // only static stuff
+  }
+
+  static void configure(Tomcat tomcat, Props props) {
+    List<Connector> connectors = new ArrayList<>();
+    connectors.addAll(Arrays.asList(newHttpConnector(props), newAjpConnector(props), newHttpsConnector(props)));
+    connectors.removeAll(Collections.singleton(null));
+
+    verify(connectors);
+
+    tomcat.setConnector(connectors.get(0));
+    for (Connector connector : connectors) {
+      tomcat.getService().addConnector(connector);
+    }
+  }
+
+  private static void verify(List<Connector> connectors) {
+    if (connectors.isEmpty()) {
+      throw new IllegalStateException("HTTP connectors are disabled");
+    }
+    Set<Integer> ports = new HashSet<>();
+    for (Connector connector : connectors) {
+      int port = connector.getPort();
+      if (ports.contains(port)) {
+        throw new IllegalStateException(String.format("HTTP, AJP and HTTPS must not use the same port %d", port));
+      }
+      ports.add(port);
+    }
+  }
+
+  @Nullable
+  private static Connector newHttpConnector(Props props) {
+    Connector connector = null;
+    // Not named "sonar.web.http.port" to keep backward-compatibility
+    int port = props.valueAsInt("sonar.web.port", 9000);
+    if (port > DISABLED_PORT) {
+      connector = newConnector(props, HTTP_PROTOCOL, "http");
+      connector.setPort(port);
+    }
+    return connector;
+  }
+
+  @Nullable
+  private static Connector newAjpConnector(Props props) {
+    Connector connector = null;
+    int port = props.valueAsInt("sonar.ajp.port", DISABLED_PORT);
+    if (port > DISABLED_PORT) {
+      connector = newConnector(props, AJP_PROTOCOL, "http");
+      connector.setPort(port);
+    }
+    return connector;
+  }
+
+  @Nullable
+  private static Connector newHttpsConnector(Props props) {
+    Connector connector = null;
+    int port = props.valueAsInt("sonar.web.https.port", DISABLED_PORT);
+    if (port > DISABLED_PORT) {
+      connector = newConnector(props, HTTP_PROTOCOL, "https");
+      connector.setPort(port);
+      connector.setSecure(true);
+      connector.setScheme("https");
+      setConnectorAttribute(connector, "keyAlias", props.value("sonar.web.https.keyAlias"));
+      String keyPassword = props.value("sonar.web.https.keyPass", "changeit");
+      setConnectorAttribute(connector, "keyPass", keyPassword);
+      setConnectorAttribute(connector, "keystorePass", props.value("sonar.web.https.keystorePass", keyPassword));
+      setConnectorAttribute(connector, "keystoreFile", props.value("sonar.web.https.keystoreFile"));
+      setConnectorAttribute(connector, "keystoreType", props.value("sonar.web.https.keystoreType", "JKS"));
+      setConnectorAttribute(connector, "keystoreProvider", props.value("sonar.web.https.keystoreProvider"));
+      setConnectorAttribute(connector, "truststorePass", props.value("sonar.web.https.truststorePass", "changeit"));
+      setConnectorAttribute(connector, "truststoreFile", props.value("sonar.web.https.truststoreFile"));
+      setConnectorAttribute(connector, "truststoreType", props.value("sonar.web.https.truststoreType", "JKS"));
+      setConnectorAttribute(connector, "truststoreProvider", props.value("sonar.web.https.truststoreProvider"));
+      setConnectorAttribute(connector, "clientAuth", props.value("sonar.web.https.clientAuth", "false"));
+      setConnectorAttribute(connector, "ciphers", props.value(PROP_HTTPS_CIPHERS));
+      // SSLv3 must not be enable because of Poodle vulnerability
+      // See https://jira.codehaus.org/browse/SONAR-5860
+      setConnectorAttribute(connector, "sslEnabledProtocols", "TLSv1,TLSv1.1,TLSv1.2");
+      setConnectorAttribute(connector, "sslProtocol", "TLS");
+      setConnectorAttribute(connector, "SSLEnabled", true);
+    }
+    return connector;
+  }
+
+  private static Connector newConnector(Props props, String protocol, String scheme) {
+    Connector connector = new Connector(protocol);
+    connector.setURIEncoding("UTF-8");
+    connector.setProperty("address", props.value("sonar.web.host", "0.0.0.0"));
+    connector.setProperty("socket.soReuseAddress", "true");
+    configurePool(props, connector, scheme);
+    configureCompression(connector);
+    return connector;
+  }
+
+  private static void configurePool(Props props, Connector connector, String scheme) {
+    connector.setProperty("acceptorThreadCount", String.valueOf(2));
+    connector.setProperty("minSpareThreads", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".minThreads", 5)));
+    connector.setProperty("maxThreads", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".maxThreads", 50)));
+    connector.setProperty("acceptCount", String.valueOf(props.valueAsInt("sonar.web." + scheme + ".acceptCount", 25)));
+  }
+
+  private static void configureCompression(Connector connector) {
+    connector.setProperty("compression", "on");
+    connector.setProperty("compressionMinSize", "1024");
+    connector.setProperty("compressableMimeType", "text/html,text/xml,text/plain,text/css,application/json,application/javascript");
+  }
+
+  private static void setConnectorAttribute(Connector c, String key, @Nullable Object value) {
+    if (value != null) {
+      c.setAttribute(key, value);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebLogging.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebLogging.java
new file mode 100644 (file)
index 0000000..fa1d251
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.core.ConsoleAppender;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+import org.sonar.process.LogbackHelper;
+
+import java.util.logging.LogManager;
+
+/**
+ * Configure logback for web server process. Logs must be written to console, which is
+ * forwarded to file logs/sonar.log by the app master process.
+ */
+class WebLogging {
+
+  private static final String LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level web[%logger{20}] %X %msg%n";
+
+  private final LogbackHelper helper = new LogbackHelper();
+
+  LoggerContext configure() {
+    LoggerContext ctx = helper.getRootContext();
+    ctx.reset();
+
+    helper.enableJulChangePropagation(ctx);
+    configureAppender(ctx);
+    configureLevels(ctx);
+
+    // Configure java.util.logging, used by Tomcat, in order to forward to slf4j
+    LogManager.getLogManager().reset();
+    SLF4JBridgeHandler.install();
+    return ctx;
+  }
+
+  private void configureAppender(LoggerContext ctx) {
+    ConsoleAppender consoleAppender = helper.newConsoleAppender(ctx, "CONSOLE", LOG_FORMAT);
+    Logger rootLogger = helper.configureLogger(ctx, Logger.ROOT_LOGGER_NAME, Level.INFO);
+    rootLogger.addAppender(consoleAppender);
+  }
+
+  private void configureLevels(LoggerContext ctx) {
+    // override level of some loggers
+    helper.configureLogger(ctx, "rails", Level.WARN);
+    helper.configureLogger(ctx, "org.hibernate.cache.ReadWriteCache", Level.ERROR);
+    helper.configureLogger(ctx, "org.hibernate", Level.WARN);
+    helper.configureLogger(ctx, "org.hibernate.SQL", Level.WARN);
+    helper.configureLogger(ctx, "org.apache.ibatis", Level.WARN);
+    helper.configureLogger(ctx, "java.sql", Level.WARN);
+    helper.configureLogger(ctx, "java.sql.ResultSet", Level.WARN);
+    helper.configureLogger(ctx, "org.sonar.MEASURE_FILTER", Level.WARN);
+    helper.configureLogger(ctx, "org.elasticsearch", Level.INFO);
+    helper.configureLogger(ctx, "org.elasticsearch.node", Level.INFO);
+    helper.configureLogger(ctx, "org.elasticsearch.http", Level.INFO);
+    helper.configureLogger(ctx, "ch.qos.logback", Level.WARN);
+  }
+}
index 24919d9d6e232b0ccd9b87bed941e9a95db323a9..724a5cdc5eabe8fb278c4f92b90439f0bcacd2e3 100644 (file)
@@ -60,7 +60,7 @@ public class WebServer implements Monitored {
    */
   public static void main(String[] args) throws Exception {
     ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
-    Logging.init(entryPoint.getProps());
+    new WebLogging().configure();
     WebServer server = new WebServer(entryPoint.getProps());
     entryPoint.launch(server);
   }
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/platform/logback.xml b/server/sonar-server/src/main/resources/org/sonar/server/platform/logback.xml
deleted file mode 100644 (file)
index 57b87e8..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<!--
-  Logging configuration.
-  By default logs are generated in logs/sonar.log.
-
-  See http://logback.qos.ch/manual/configuration.html
--->
-<configuration debug="false">
-  <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
-  <!-- do not change appender name. See org.sonar.server.platform.SwitchLogbackAppender -->
-  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>
-        ${LOGGING_FORMAT}
-      </pattern>
-    </encoder>
-  </appender>
-
-  <!-- do not change appender name. See org.sonar.server.platform.SwitchLogbackAppender -->
-  <appender name="ANALYSIS_REPORTS" class="ch.qos.logback.core.rolling.RollingFileAppender">
-    <File>${sonar.path.logs}/analysis_reports.log</File>
-    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
-      <param name="FileNamePattern" value="${sonar.path.logs}/analysis_reports.%i.log"/>
-      <param name="MinIndex" value="1"/>
-      <param name="MaxIndex" value="5"/>
-    </rollingPolicy>
-    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-      <param name="MaxFileSize" value="10MB"/>
-    </triggeringPolicy>
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>%d{yyyy.MM.dd HH:mm:ss} %-5level [%logger{20}] %msg%n</pattern>
-    </encoder>
-  </appender>
-
-  <appender name="SWITCH" class="org.sonar.server.platform.SwitchLogbackAppender">
-    <!-- do not change appender names. See org.sonar.server.platform.SwitchLogbackAppender -->
-    <appender-ref ref="CONSOLE" />
-    <appender-ref ref="ANALYSIS_REPORTS" />
-  </appender>
-
-  <!-- Display Rails warnings and errors  -->
-  <logger name="rails">
-    <level value="WARN"/>
-  </logger>
-
-  <logger name="org.hibernate.cache.ReadWriteCache">
-    <!-- removing "An item was expired by the cache while it was locked (increase your cache timeout)" msg -->
-    <level value="ERROR"/>
-  </logger>
-  <logger name="org.hibernate">
-    <level value="WARN"/>
-  </logger>
-
-  <!-- Display SQL requests and results by setting the following loggers to level DEBUG -->
-  <logger name="org.hibernate.SQL">
-    <level value="WARN"/>
-  </logger>
-  <logger name="org.apache.ibatis">
-    <level value="WARN"/>
-  </logger>
-  <logger name="java.sql">
-    <level value="WARN"/>
-  </logger>
-  <logger name="java.sql.ResultSet">
-    <level value="WARN"/>
-  </logger>
-
-  <!-- Execution of measure filters -->
-  <logger name="org.sonar.MEASURE_FILTER">
-    <level value="WARN"/>
-  </logger>
-
-  <!-- Turn off ElasticSearch logging -->
-  <logger name="org.elasticsearch">
-    <level value="INFO"/>
-  </logger>
-  <logger name="org.elasticsearch.node">
-    <level value="INFO"/>
-  </logger>
-  <logger name="org.elasticsearch.http">
-    <level value="INFO"/>
-  </logger>
-
-  <!-- LOG level for logback itself -->
-  <logger name="ch.qos.logback">
-    <level value="WARN"/>
-  </logger>
-
-  <root>
-    <level value="INFO"/>
-    <appender-ref ref="SWITCH"/>
-  </root>
-
-</configuration>
diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/ConnectorsTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/ConnectorsTest.java
deleted file mode 100644 (file)
index 70ba503..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.server.app;
-
-import com.google.common.collect.ImmutableMap;
-import org.apache.catalina.connector.Connector;
-import org.apache.catalina.startup.Tomcat;
-import org.junit.Test;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-import org.sonar.process.Props;
-
-import java.net.InetAddress;
-import java.util.Map;
-import java.util.Properties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-public class ConnectorsTest {
-
-  Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
-
-  // ---- connectors
-
-  @Test
-  public void configure_thread_pool() throws Exception {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.http.minThreads", "2");
-    p.setProperty("sonar.web.http.maxThreads", "30");
-    p.setProperty("sonar.web.http.acceptCount", "20");
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
-      ImmutableMap.<String, Object>of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20)
-      )));
-  }
-
-  @Test
-  public void configure_default_thread_pool() throws Exception {
-    Props props = new Props(new Properties());
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
-      ImmutableMap.<String, Object>of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25)
-      )));
-  }
-
-  @Test
-  public void different_thread_pools_for_connectors() throws Exception {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "9000");
-    p.setProperty("sonar.web.http.minThreads", "2");
-    p.setProperty("sonar.web.https.port", "9443");
-    p.setProperty("sonar.web.https.minThreads", "5");
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getPort() == 9000 && c.getProperty("minSpareThreads").equals(2);
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getPort() == 9443 && c.getProperty("minSpareThreads").equals(5);
-      }
-    }));
-  }
-
-  @Test
-  public void fail_if_http_connectors_are_disabled() {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "-1");
-    p.setProperty("sonar.web.https.port", "-1");
-    Props props = new Props(p);
-
-    try {
-      Connectors.configure(tomcat, props);
-      fail();
-    } catch (IllegalStateException e) {
-      assertThat(e.getMessage()).isEqualTo("HTTP connectors are disabled");
-    }
-  }
-
-  @Test
-  public void only_https_is_enabled() {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "-1");
-    p.setProperty("sonar.web.https.port", "9443");
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getPort() == 9443
-          && c.getProperty("clientAuth").equals("false");
-      }
-    }));
-  }
-
-  @Test
-  public void all_connectors_are_enabled() {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "9000");
-    p.setProperty("sonar.ajp.port", "9009");
-    p.setProperty("sonar.web.https.port", "9443");
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("http") && c.getPort() == 9000 && c.getProtocol().equals(Connectors.HTTP_PROTOCOL);
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("http") && c.getPort() == 9009 && c.getProtocol().equals(Connectors.AJP_PROTOCOL);
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getPort() == 9443 && c.getProtocol().equals(Connectors.HTTP_PROTOCOL);
-      }
-    }));
-  }
-
-  @Test
-  public void http_and_ajp_and_https_ports_should_be_different() throws Exception {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "9000");
-    p.setProperty("sonar.ajp.port", "9000");
-    p.setProperty("sonar.web.https.port", "9000");
-
-    try {
-      Connectors.configure(tomcat, new Props(p));
-      fail();
-    } catch (IllegalStateException e) {
-      assertThat(e).hasMessage("HTTP, AJP and HTTPS must not use the same port 9000");
-    }
-  }
-
-  @Test
-  public void bind_to_all_addresses_by_default() throws Exception {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "9000");
-    p.setProperty("sonar.ajp.port", "9009");
-    p.setProperty("sonar.web.https.port", "9443");
-
-    Connectors.configure(tomcat, new Props(p));
-
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("http") && c.getPort() == 9009 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
-      }
-    }));
-  }
-
-  @Test
-  public void bind_to_specific_address() throws Exception {
-    Properties p = new Properties();
-    p.setProperty("sonar.web.port", "9000");
-    p.setProperty("sonar.web.https.port", "9443");
-    p.setProperty("sonar.web.host", "1.2.3.4");
-
-    Connectors.configure(tomcat, new Props(p));
-
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
-      }
-    }));
-    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
-      }
-    }));
-  }
-
-  @Test
-  public void enable_client_auth() throws Exception {
-
-    Properties p = new Properties();
-
-    p.setProperty("sonar.web.port", "-1");
-    p.setProperty("sonar.web.https.port", "9443");
-    p.setProperty("sonar.web.https.clientAuth", "want");
-
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("want");
-      }
-    }));
-  }
-
-  @Test
-  public void require_client_auth() throws Exception {
-
-    Properties p = new Properties();
-
-    p.setProperty("sonar.web.port", "-1");
-    p.setProperty("sonar.web.https.port", "9443");
-    p.setProperty("sonar.web.https.clientAuth", "true");
-
-    Props props = new Props(p);
-
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
-      @Override
-      public boolean matches(Object o) {
-        Connector c = (Connector) o;
-        return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("true");
-      }
-    }));
-  }
-
-  private static class PropertiesMatcher extends ArgumentMatcher<Connector> {
-    private final Map<String, Object> expected;
-
-    PropertiesMatcher(Map<String, Object> expected) {
-      this.expected = expected;
-    }
-
-    public boolean matches(Object o) {
-      Connector c = (Connector) o;
-      for (Map.Entry<String, Object> entry : expected.entrySet()) {
-        if (!entry.getValue().equals(c.getProperty(entry.getKey()))) {
-          return false;
-        }
-      }
-      return true;
-    }
-  }
-}
index 3dd916816e62fe598b89fe2d72d931544ec4277d..71c2fcc6f744651cbc4d41fde72946dfda806e1c 100644 (file)
@@ -50,6 +50,7 @@ public class EmbeddedTomcatTest {
     FileUtils.write(new File(home, "web/WEB-INF/web.xml"), "<web-app/>");
     props.set("sonar.path.home", home.getAbsolutePath());
     props.set("sonar.path.web", webDir.getAbsolutePath());
+    props.set("sonar.path.logs", temp.newFolder().getAbsolutePath());
 
     // start server on a random port
     int httpPort = NetworkUtils.freePort();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/LoggingTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/LoggingTest.java
deleted file mode 100644 (file)
index da9ee50..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.server.app;
-
-import ch.qos.logback.access.tomcat.LogbackValve;
-import org.apache.catalina.Lifecycle;
-import org.apache.catalina.LifecycleEvent;
-import org.apache.catalina.Valve;
-import org.apache.catalina.startup.Tomcat;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-import org.slf4j.Logger;
-import org.sonar.process.ProcessConstants;
-import org.sonar.process.Props;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Properties;
-
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-public class LoggingTest {
-
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
-
-  @Before
-  public void setHome() throws IOException {
-    File homeDir = temp.newFolder("home");
-    System.setProperty("SONAR_HOME", homeDir.getAbsolutePath());
-  }
-
-  @Test
-  public void enable_access_logs_by_Default() throws Exception {
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
-    Props props = new Props(new Properties());
-    props.set(ProcessConstants.PATH_WEB, temp.newFolder().getAbsolutePath());
-    Logging.configure(tomcat, props);
-
-    verify(tomcat.getHost().getPipeline()).addValve(argThat(new ArgumentMatcher<Valve>() {
-      @Override
-      public boolean matches(Object o) {
-        LogbackValve v = (LogbackValve) o;
-        String confFile = v.getFilename();
-        return confFile.endsWith("logback-access.xml");
-      }
-    }));
-  }
-
-  @Test
-  public void log_when_started_and_stopped() {
-    Logger logger = mock(Logger.class);
-    Logging.LifecycleLogger listener = new Logging.LifecycleLogger(logger);
-
-    LifecycleEvent event = new LifecycleEvent(mock(Lifecycle.class), "before_init", null);
-    listener.lifecycleEvent(event);
-    verifyZeroInteractions(logger);
-
-    event = new LifecycleEvent(mock(Lifecycle.class), "after_start", null);
-    listener.lifecycleEvent(event);
-    verify(logger).info("Web server is started");
-
-    event = new LifecycleEvent(mock(Lifecycle.class), "after_destroy", null);
-    listener.lifecycleEvent(event);
-    verify(logger).info("Web server is stopped");
-  }
-}
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
new file mode 100644 (file)
index 0000000..ee436f7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import ch.qos.logback.core.ConsoleAppender;
+import org.apache.catalina.Container;
+import org.junit.Test;
+import org.sonar.process.LogbackHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class ProgrammaticLogbackValveTest {
+
+  @Test
+  public void startInternal() throws Exception {
+    ProgrammaticLogbackValve valve = new ProgrammaticLogbackValve();
+    valve.setContainer(mock(Container.class));
+    LogbackHelper helper = new LogbackHelper();
+    ConsoleAppender appender = helper.newConsoleAppender(valve, "CONSOLE", "combined");
+    valve.addAppender(appender);
+    valve.start();
+    assertThat(valve.isStarted()).isTrue();
+    valve.stop();
+    assertThat(valve.isStarted()).isFalse();
+  }
+}
index ea80502c7851df0af4f8ed6ef70c728d7f854bfe..b4906a5ca1410a140d2bbedb60d640323ab0d6d4 100644 (file)
@@ -81,7 +81,7 @@ public class StartupLogsTest {
     when(protocol.getCiphersUsed()).thenReturn(new String[] {"SSL_RSA", "TLS_RSA_WITH_RC4"});
     when(connector.getProtocolHandler()).thenReturn(protocol);
     when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector});
-    props.set(Connectors.PROP_HTTPS_CIPHERS, "SSL_RSA,TLS_RSA_WITH_RC4");
+    props.set(TomcatConnectors.PROP_HTTPS_CIPHERS, "SSL_RSA,TLS_RSA_WITH_RC4");
     sut.log(tomcat);
 
     verify(logger).info("HTTPS connector enabled on port 1234 | ciphers=SSL_RSA,TLS_RSA_WITH_RC4");
diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/TomcatAccessLogTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/TomcatAccessLogTest.java
new file mode 100644 (file)
index 0000000..7e08121
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.sonar.process.ProcessConstants;
+import org.sonar.process.Props;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class TomcatAccessLogTest {
+
+  TomcatAccessLog sut = new TomcatAccessLog();
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Before
+  public void setHome() throws IOException {
+    File homeDir = temp.newFolder("home");
+    System.setProperty("SONAR_HOME", homeDir.getAbsolutePath());
+  }
+
+  @Test
+  public void enable_access_logs_by_Default() throws Exception {
+    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
+    Props props = new Props(new Properties());
+    props.set(ProcessConstants.PATH_LOGS, temp.newFolder().getAbsolutePath());
+    sut.configure(tomcat, props);
+
+    verify(tomcat.getHost().getPipeline()).addValve(any(ProgrammaticLogbackValve.class));
+  }
+
+  @Test
+  public void log_when_started_and_stopped() {
+    Logger logger = mock(Logger.class);
+    TomcatAccessLog.LifecycleLogger listener = new TomcatAccessLog.LifecycleLogger(logger);
+
+    LifecycleEvent event = new LifecycleEvent(mock(Lifecycle.class), "before_init", null);
+    listener.lifecycleEvent(event);
+    verifyZeroInteractions(logger);
+
+    event = new LifecycleEvent(mock(Lifecycle.class), "after_start", null);
+    listener.lifecycleEvent(event);
+    verify(logger).info("Web server is started");
+
+    event = new LifecycleEvent(mock(Lifecycle.class), "after_destroy", null);
+    listener.lifecycleEvent(event);
+    verify(logger).info("Web server is stopped");
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java
new file mode 100644 (file)
index 0000000..6c4c7d4
--- /dev/null
@@ -0,0 +1,300 @@
+
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+import org.sonar.process.Props;
+
+import java.net.InetAddress;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class TomcatConnectorsTest {
+
+  Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
+
+  // ---- connectors
+
+  @Test
+  public void configure_thread_pool() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.http.minThreads", "2");
+    p.setProperty("sonar.web.http.maxThreads", "30");
+    p.setProperty("sonar.web.http.acceptCount", "20");
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+      ImmutableMap.<String, Object>of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20)
+      )));
+  }
+
+  @Test
+  public void configure_default_thread_pool() throws Exception {
+    Props props = new Props(new Properties());
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+      ImmutableMap.<String, Object>of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25)
+      )));
+  }
+
+  @Test
+  public void different_thread_pools_for_connectors() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "9000");
+    p.setProperty("sonar.web.http.minThreads", "2");
+    p.setProperty("sonar.web.https.port", "9443");
+    p.setProperty("sonar.web.https.minThreads", "5");
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getPort() == 9000 && c.getProperty("minSpareThreads").equals(2);
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getPort() == 9443 && c.getProperty("minSpareThreads").equals(5);
+      }
+    }));
+  }
+
+  @Test
+  public void fail_if_http_connectors_are_disabled() {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "-1");
+    p.setProperty("sonar.web.https.port", "-1");
+    Props props = new Props(p);
+
+    try {
+      TomcatConnectors.configure(tomcat, props);
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("HTTP connectors are disabled");
+    }
+  }
+
+  @Test
+  public void only_https_is_enabled() {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "-1");
+    p.setProperty("sonar.web.https.port", "9443");
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getPort() == 9443
+          && c.getProperty("clientAuth").equals("false");
+      }
+    }));
+  }
+
+  @Test
+  public void all_connectors_are_enabled() {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "9000");
+    p.setProperty("sonar.ajp.port", "9009");
+    p.setProperty("sonar.web.https.port", "9443");
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("http") && c.getPort() == 9000 && c.getProtocol().equals(TomcatConnectors.HTTP_PROTOCOL);
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("http") && c.getPort() == 9009 && c.getProtocol().equals(TomcatConnectors.AJP_PROTOCOL);
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getPort() == 9443 && c.getProtocol().equals(TomcatConnectors.HTTP_PROTOCOL);
+      }
+    }));
+  }
+
+  @Test
+  public void http_and_ajp_and_https_ports_should_be_different() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "9000");
+    p.setProperty("sonar.ajp.port", "9000");
+    p.setProperty("sonar.web.https.port", "9000");
+
+    try {
+      TomcatConnectors.configure(tomcat, new Props(p));
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessage("HTTP, AJP and HTTPS must not use the same port 9000");
+    }
+  }
+
+  @Test
+  public void bind_to_all_addresses_by_default() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "9000");
+    p.setProperty("sonar.ajp.port", "9009");
+    p.setProperty("sonar.web.https.port", "9443");
+
+    TomcatConnectors.configure(tomcat, new Props(p));
+
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("http") && c.getPort() == 9009 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0");
+      }
+    }));
+  }
+
+  @Test
+  public void bind_to_specific_address() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.port", "9000");
+    p.setProperty("sonar.web.https.port", "9443");
+    p.setProperty("sonar.web.host", "1.2.3.4");
+
+    TomcatConnectors.configure(tomcat, new Props(p));
+
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
+      }
+    }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getPort() == 9443 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4");
+      }
+    }));
+  }
+
+  @Test
+  public void enable_client_auth() throws Exception {
+
+    Properties p = new Properties();
+
+    p.setProperty("sonar.web.port", "-1");
+    p.setProperty("sonar.web.https.port", "9443");
+    p.setProperty("sonar.web.https.clientAuth", "want");
+
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("want");
+      }
+    }));
+  }
+
+  @Test
+  public void require_client_auth() throws Exception {
+
+    Properties p = new Properties();
+
+    p.setProperty("sonar.web.port", "-1");
+    p.setProperty("sonar.web.https.port", "9443");
+    p.setProperty("sonar.web.https.clientAuth", "true");
+
+    Props props = new Props(p);
+
+    TomcatConnectors.configure(tomcat, props);
+
+    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector) o;
+        return c.getScheme().equals("https") && c.getProperty("clientAuth").equals("true");
+      }
+    }));
+  }
+
+  private static class PropertiesMatcher extends ArgumentMatcher<Connector> {
+    private final Map<String, Object> expected;
+
+    PropertiesMatcher(Map<String, Object> expected) {
+      this.expected = expected;
+    }
+
+    public boolean matches(Object o) {
+      Connector c = (Connector) o;
+      for (Map.Entry<String, Object> entry : expected.entrySet()) {
+        if (!entry.getValue().equals(c.getProperty(entry.getKey()))) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/WebLoggingTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/WebLoggingTest.java
new file mode 100644 (file)
index 0000000..467b58b
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.server.app;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.sonar.process.LogbackHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WebLoggingTest {
+
+  WebLogging sut = new WebLogging();
+
+  @AfterClass
+  public static void resetLogback() throws Exception {
+    new LogbackHelper().resetFromXml("/logback-test.xml");
+  }
+
+  @Test
+  public void log_to_console() throws Exception {
+    LoggerContext ctx = sut.configure();
+
+    Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME);
+    Appender appender = root.getAppender("CONSOLE");
+    assertThat(appender).isInstanceOf(ConsoleAppender.class);
+
+    // change level of some loggers
+    assertThat(ctx.getLogger("java.sql").getLevel()).isEqualTo(Level.WARN);
+  }
+}
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/config/logback-access.xml b/server/sonar-web/src/main/webapp/WEB-INF/config/logback-access.xml
deleted file mode 100644 (file)
index 504f5fc..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<!--
-
-  Log HTTP requests in logs/access.log. For internal use only.
-
-  See http://logback.qos.ch/access.html#configuration and Tomcat configuration in sonar-application.
-
--->
-
-<configuration debug="false">
-
-  <!-- Required for performance reason. See http://www.slf4j.org/legacy.html#jul-to-slf4j -->
-  <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
-  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-    <file>${sonar.path.logs}/access.log</file>
-    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
-      <param name="FileNamePattern" value="${sonar.path.logs}/access.%i.log"/>
-      <param name="MinIndex" value="1"/>
-      <param name="MaxIndex" value="3"/>
-    </rollingPolicy>
-    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-      <param name="MaxFileSize" value="5MB"/>
-    </triggeringPolicy>
-    <encoder>
-      <pattern>combined</pattern>
-    </encoder>
-  </appender>
-
-  <appender-ref ref="FILE" />
-</configuration>
index 53fb2cfb207a1c20e54f9113cdd7e0cebf599e63..c2a1b29cf576b1baf1bf733134633bde45c481c9 100644 (file)
 #sonar.web.http.acceptCount=25
 #sonar.web.https.acceptCount=25
 
-# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb.
-# An archive of 3 files is kept in the same directory.
-# Access logs are enabled by default.
-#sonar.web.accessLogs.enable=true
-
 # TCP port for incoming AJP connections. Disabled if value is -1. Disabled by default.
 #sonar.ajp.port=-1
 
 # Default is <installation home>/logs
 #sonar.path.logs=logs
 
+# Rolling policy of log files
+#    - based on time if value starts with "time:", for example by day ("time:yyyy-MM-dd")
+#      or by month ("time:yyyy-MM")
+#    - based on size if value starts with "size:", for example "size:10MB"
+#    - disabled if value is "none".  That needs logs to be managed by an external system like logrotate.
+#sonar.log.rollingPolicy=time:yyyy-MM-dd
+
+# Maximum number of files to keep if a rolling policy is enabled
+#sonar.log.maxFiles=7
+
+# Access log is the list of all the HTTP requests received by server. If enabled, it is stored
+# in the file {sonar.path.logs}/access.log. This file follows the same rolling policy as for
+# sonar.log (see sonar.log.rollingPolicy and sonar.log.maxFiles).
+#sonar.web.accessLogs.enable=true
+
+# Format of access log. It is ignored if sonar.web.accessLogs.enable=false. Value is:
+#    - "common" is the Common Log Format (shortcut for: %h %l %u %user %date "%r" %s %b)
+#    - "combined" is another format widely recognized (shortcut for: %h %l %u [%t] "%r" %s %b "%i{Referer}" "%i{User-Agent}")
+#    - else a custom pattern. See http://logback.qos.ch/manual/layouts.html#AccessPatternLayout
+#sonar.web.accessLogs.pattern=combined
+
 
 #--------------------------------------------------------------------------------------------------
 # OTHERS
index af325f1d370a233f84dc63bb0105ff0e996ac420..e70c34df4feeb6c2f5c045c75ed5e06c99573bb8 100644 (file)
@@ -24,7 +24,6 @@ import org.apache.commons.lang.StringUtils;
 import org.sonar.process.MinimumViableSystem;
 import org.sonar.process.ProcessCommands;
 import org.sonar.process.ProcessConstants;
-import org.sonar.process.ProcessLogging;
 import org.sonar.process.Props;
 import org.sonar.process.StopWatcher;
 import org.sonar.process.Stoppable;
@@ -114,11 +113,8 @@ public class App implements Stoppable {
     CommandLineParser cli = new CommandLineParser();
     Properties rawProperties = cli.parseArguments(args);
     Props props = new PropsBuilder(rawProperties, new JdbcSettings()).build();
-    ProcessLogging logging = new ProcessLogging();
-    logging.configure(props, "/org/sonar/application/logback.xml");
-    if (props.valueAsBoolean("sonar.log.console", false)) {
-      logging.addConsoleAppender();
-    }
+    AppLogging logging = new AppLogging();
+    logging.configure(props);
 
     App app = new App();
     app.start(props);
diff --git a/sonar-application/src/main/java/org/sonar/application/AppLogging.java b/sonar-application/src/main/java/org/sonar/application/AppLogging.java
new file mode 100644 (file)
index 0000000..6c8e565
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.application;
+
+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.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.LogbackHelper;
+import org.sonar.process.Props;
+
+/**
+ * Configure logback for the master process
+ */
+class AppLogging {
+
+  static final String CONSOLE_LOGGER = "console";
+  static final String CONSOLE_APPENDER = "CONSOLE";
+  static final String GOBBLER_LOGGER = "gobbler";
+  static final String GOBBLER_APPENDER = "GOBBLER";
+  static final String APP_PATTERN = "%d{yyyy.MM.dd HH:mm:ss} %-5level app[%logger{20}] %msg%n";
+
+  private final LogbackHelper helper = new LogbackHelper();
+
+  LoggerContext configure(Props props) {
+    LoggerContext ctx = helper.getRootContext();
+    ctx.reset();
+    helper.enableJulChangePropagation(ctx);
+    configureConsole(ctx);
+    configureGobbler(props, ctx);
+    configureRoot(ctx);
+    if (props.valueAsBoolean("sonar.log.console", false)) {
+      // used by SonarSource testing environment
+      copyGobblerToConsole();
+    }
+    return ctx;
+  }
+
+  /**
+   * Enable the copy in console of the logs written in logs/sonar.log
+   */
+  private void copyGobblerToConsole() {
+    Logger consoleLogger = (Logger) LoggerFactory.getLogger(CONSOLE_LOGGER);
+    Appender consoleAppender = consoleLogger.getAppender(CONSOLE_APPENDER);
+
+    Logger gobblerLogger = (Logger) LoggerFactory.getLogger(GOBBLER_LOGGER);
+    gobblerLogger.addAppender(consoleAppender);
+  }
+
+  private void configureConsole(LoggerContext loggerContext) {
+    ConsoleAppender consoleAppender = helper.newConsoleAppender(loggerContext, CONSOLE_APPENDER, "%msg%n");
+    Logger consoleLogger = loggerContext.getLogger(CONSOLE_LOGGER);
+    consoleLogger.setAdditive(false);
+    consoleLogger.addAppender(consoleAppender);
+  }
+
+  private void configureGobbler(Props props, LoggerContext ctx) {
+    // configure appender
+    LogbackHelper.RollingPolicy rollingPolicy = helper.createRollingPolicy(ctx, props, "sonar");
+    FileAppender fileAppender = rollingPolicy.createAppender(GOBBLER_APPENDER);
+    fileAppender.setContext(ctx);
+    PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
+    fileEncoder.setContext(ctx);
+    fileEncoder.setPattern("%msg%n");
+    fileEncoder.start();
+    fileAppender.setEncoder(fileEncoder);
+    fileAppender.start();
+
+    // configure logger
+    Logger gobblerLogger = ctx.getLogger(GOBBLER_LOGGER);
+    gobblerLogger.setAdditive(false);
+    gobblerLogger.addAppender(fileAppender);
+  }
+
+  private void configureRoot(LoggerContext loggerContext) {
+    ConsoleAppender consoleAppender = helper.newConsoleAppender(loggerContext, "ROOT_CONSOLE", APP_PATTERN);
+    Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
+    rootLogger.setLevel(Level.INFO);
+    rootLogger.addAppender(consoleAppender);
+  }
+}
diff --git a/sonar-application/src/main/resources/org/sonar/application/logback.xml b/sonar-application/src/main/resources/org/sonar/application/logback.xml
deleted file mode 100644 (file)
index cbcd991..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<configuration debug="false">
-  <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
-  <appender name="LOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-    <File>${sonar.path.logs}/sonar.log</File>
-    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
-      <param name="FileNamePattern" value="${sonar.path.logs}/sonar.%i.log"/>
-      <param name="MinIndex" value="1"/>
-      <param name="MaxIndex" value="5"/>
-    </rollingPolicy>
-    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-      <param name="MaxFileSize" value="10MB"/>
-    </triggeringPolicy>
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>%msg%n</pattern>
-    </encoder>
-  </appender>
-
-  <appender name="APP" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>%d{yyyy.MM.dd HH:mm:ss} %-5level app[%logger{20}] %msg%n</pattern>
-    </encoder>
-  </appender>
-
-  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>
-        %msg%n
-      </pattern>
-    </encoder>
-  </appender>
-
-  <logger name="console" additivity="false">
-    <appender-ref ref="CONSOLE"/>
-  </logger>
-
-  <logger name="gobbler" additivity="false">
-    <appender-ref ref="LOGFILE"/>
-  </logger>
-
-  <root>
-    <level value="INFO"/>
-    <appender-ref ref="APP"/>
-  </root>
-
-</configuration>
diff --git a/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java b/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java
new file mode 100644 (file)
index 0000000..e2c902a
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.application;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+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.LogbackHelper;
+import org.sonar.process.ProcessConstants;
+import org.sonar.process.Props;
+
+import java.io.File;
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AppLoggingTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  Props props = new Props(new Properties());
+  AppLogging sut = new AppLogging();
+
+  @Before
+  public void setUp() throws Exception {
+    File dir = temp.newFolder();
+    props.set(ProcessConstants.PATH_LOGS, dir.getAbsolutePath());
+  }
+
+  @AfterClass
+  public static void resetLogback() throws Exception {
+    new LogbackHelper().resetFromXml("/logback-test.xml");
+  }
+
+  @Test
+  public void configure_defaults() throws Exception {
+    LoggerContext ctx = sut.configure(props);
+
+    Logger gobbler = ctx.getLogger(AppLogging.GOBBLER_LOGGER);
+    Appender<ILoggingEvent> appender = gobbler.getAppender(AppLogging.GOBBLER_APPENDER);
+    assertThat(appender).isInstanceOf(RollingFileAppender.class);
+
+    // gobbler is not copied to console
+    assertThat(gobbler.getAppender(AppLogging.GOBBLER_APPENDER)).isNotNull();
+    assertThat(gobbler.getAppender(AppLogging.CONSOLE_APPENDER)).isNull();
+  }
+
+  @Test
+  public void configure_no_rotation() throws Exception {
+    props.set("sonar.log.rollingPolicy", "none");
+
+    LoggerContext ctx = sut.configure(props);
+
+    Logger gobbler = ctx.getLogger(AppLogging.GOBBLER_LOGGER);
+    Appender<ILoggingEvent> appender = gobbler.getAppender(AppLogging.GOBBLER_APPENDER);
+    assertThat(appender).isNotInstanceOf(RollingFileAppender.class).isInstanceOf(FileAppender.class);
+  }
+
+  @Test
+  public void copyGobblerToConsole() throws Exception {
+    props.set("sonar.log.console", "true");
+
+    LoggerContext ctx = sut.configure(props);
+    Logger gobbler = ctx.getLogger(AppLogging.GOBBLER_LOGGER);
+    assertThat(gobbler.getAppender(AppLogging.GOBBLER_APPENDER)).isNotNull();
+    assertThat(gobbler.getAppender(AppLogging.CONSOLE_APPENDER)).isNotNull();
+  }
+}
index ff2270cc122edabd0f127b1b50ef6dd01ea2f678..f62e5c2b41d4dda0931a5c79af25d7113d17aff4 100644 (file)
@@ -1,27 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-
-<!--
-  Configuration for default logger. Only used while embedded server is starting,
-  before proper logging configuration is loaded.
-
-  See http://logback.qos.ch/manual/configuration.html
--->
 <configuration debug="false">
   <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
 
-  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-    <filter class="ch.qos.logback.classic.filter.LevelFilter">
-      <level>INFO</level>
-      <onMatch>ACCEPT</onMatch>
-      <onMismatch>DENY</onMismatch>
-    </filter>
-    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-      <pattern>
-        %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
-      </pattern>
-    </encoder>
-  </appender>
-
   <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
     <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
       <pattern>
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Logger.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Logger.java
new file mode 100644 (file)
index 0000000..dab1503
--- /dev/null
@@ -0,0 +1,16 @@
+package org.sonar.api.utils.log;
+
+public interface Logger {
+
+  boolean isDebugEnabled();
+
+  void debug(String message);
+
+  void info(String message);
+
+  void warn(String message);
+
+  void error(String message);
+
+  void error(String message, Throwable throwable);
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Loggers.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Loggers.java
new file mode 100644 (file)
index 0000000..6a9f278
--- /dev/null
@@ -0,0 +1,6 @@
+package org.sonar.api.utils.log;
+
+public class Loggers {
+
+
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/InMemoryLogger.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/InMemoryLogger.java
new file mode 100644 (file)
index 0000000..b7f2fa0
--- /dev/null
@@ -0,0 +1,88 @@
+package org.sonar.api.utils.log.internal;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import org.sonar.api.utils.log.Logger;
+
+import java.util.List;
+
+/**
+ * Implementation of {@link org.sonar.api.utils.log.Logger} which keeps logs
+ * in memory, so that they can be loaded after writing. It is helpful
+ * for testing.
+ */
+public class InMemoryLogger implements Logger {
+
+  private static enum Level {
+    DEBUG, INFO, WARN, ERROR
+  }
+
+  private boolean debugEnabled = false;
+  private final ArrayListMultimap<Level, String> logs = ArrayListMultimap.create();
+
+  @Override
+  public boolean isDebugEnabled() {
+    return debugEnabled;
+  }
+
+  public InMemoryLogger setDebugEnabled(boolean b) {
+    this.debugEnabled = b;
+    return this;
+  }
+
+  @Override
+  public void debug(String message) {
+    if (isDebugEnabled()) {
+      log(Level.DEBUG, message);
+    }
+  }
+
+  @Override
+  public void info(String message) {
+    log(Level.INFO, message);
+  }
+
+  @Override
+  public void warn(String message) {
+    log(Level.WARN, message);
+  }
+
+  @Override
+  public void error(String message) {
+    log(Level.ERROR, message);
+  }
+
+  @Override
+  public void error(String message, Throwable throwable) {
+    log(Level.ERROR, String.format("%s | %s", message, throwable.getMessage()));
+  }
+
+  public List<String> logs() {
+    return Lists.newArrayList(logs.values());
+  }
+
+  public List<String> debugLogs() {
+    return logs.get(Level.DEBUG);
+  }
+
+  public List<String> infoLogs() {
+    return logs.get(Level.INFO);
+  }
+
+  public List<String> warnLogs() {
+    return logs.get(Level.WARN);
+  }
+
+  public List<String> errorLogs() {
+    return logs.get(Level.ERROR);
+  }
+
+  public InMemoryLogger clear() {
+    logs.clear();
+    return this;
+  }
+
+  private void log(Level level, String message) {
+    logs.put(level, message);
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/Slf4jLoggers.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/Slf4jLoggers.java
new file mode 100644 (file)
index 0000000..d1cfadd
--- /dev/null
@@ -0,0 +1,54 @@
+package org.sonar.api.utils.log.internal;
+
+
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.log.Logger;
+
+public class Slf4jLoggers {
+
+  public static class Slf4jLogger implements Logger {
+    private final org.slf4j.Logger slf4j;
+
+    public Slf4jLogger(org.slf4j.Logger slf4j) {
+      this.slf4j = slf4j;
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+      return slf4j.isDebugEnabled();
+    }
+
+    @Override
+    public void debug(String message) {
+      slf4j.debug(message);
+    }
+
+    @Override
+    public void info(String message) {
+      slf4j.info(message);
+    }
+
+    @Override
+    public void warn(String message) {
+      slf4j.warn(message);
+    }
+
+    @Override
+    public void error(String message) {
+      slf4j.error(message);
+    }
+
+    @Override
+    public void error(String message, Throwable throwable) {
+      slf4j.error(message, throwable);
+    }
+  }
+
+  public Slf4jLogger getLogger(String name) {
+    return new Slf4jLogger(LoggerFactory.getLogger(name));
+  }
+
+  public Slf4jLogger getLogger(Class name) {
+    return new Slf4jLogger(LoggerFactory.getLogger(name));
+  }
+}