diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-02-13 14:56:28 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-02-19 12:01:20 +0100 |
commit | c063afcb3a908e4205f4f3b605f5137489ae2eba (patch) | |
tree | 57192e99495589cb415e451c250124a4a77ce1e5 | |
parent | 776b67bdfafe3602802e3c76f14b0a5fd3871d12 (diff) | |
download | sonarqube-c063afcb3a908e4205f4f3b605f5137489ae2eba.tar.gz sonarqube-c063afcb3a908e4205f4f3b605f5137489ae2eba.zip |
SONAR-6057 Make log rotation more configurable for production
39 files changed, 1214 insertions, 459 deletions
@@ -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 index 298193e01fa..00000000000 --- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml +++ /dev/null @@ -1 +0,0 @@ -<configuration/> diff --git a/server/sonar-process/pom.xml b/server/sonar-process/pom.xml index f4da18c6757..03bbf3d7c81 100644 --- a/server/sonar-process/pom.xml +++ b/server/sonar-process/pom.xml @@ -12,16 +12,10 @@ <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 index 00000000000..862fc22c7a4 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/LogbackHelper.java @@ -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 index fe7f647d2e9..00000000000 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java +++ /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 index 00000000000..d761a2dca76 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/LogbackHelperTest.java @@ -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 index 00000000000..f2d5a6a466f --- /dev/null +++ b/server/sonar-process/src/test/resources/logback-test.xml @@ -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 index 298193e01fa..00000000000 --- a/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml +++ /dev/null @@ -1 +0,0 @@ -<configuration/> diff --git a/server/sonar-search/pom.xml b/server/sonar-search/pom.xml index 26d3b355560..9f836ef23ba 100644 --- a/server/sonar-search/pom.xml +++ b/server/sonar-search/pom.xml @@ -37,7 +37,12 @@ <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 index 00000000000..2c2d45ad39b --- /dev/null +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchLogging.java @@ -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; + } +} diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java index 7689b91f2ef..f5b601ce21c 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java @@ -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 index 3c24d95cdc0..00000000000 --- a/server/sonar-search/src/main/resources/org/sonar/search/logback.xml +++ /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 index 00000000000..f85772f7340 --- /dev/null +++ b/server/sonar-search/src/test/java/org/sonar/search/SearchLoggingTest.java @@ -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/EmbeddedTomcat.java b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java index ea651f54e01..89b3a09377a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java @@ -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 index d83e49501bb..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Logging.java +++ /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 index 00000000000..63d18c937ab --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/app/ProgrammaticLogbackValve.java @@ -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); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java b/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java index d2e7f4e736a..7fcf0295552 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java @@ -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 index 00000000000..69b80a51d69 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatAccessLog.java @@ -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/Connectors.java b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java index 1518117eafe..61f4718bfdc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/TomcatConnectors.java @@ -35,7 +35,7 @@ import java.util.Set; /** * Configuration of Tomcat connectors */ -class Connectors { +class TomcatConnectors { public static final String PROP_HTTPS_CIPHERS = "sonar.web.https.ciphers"; @@ -43,7 +43,7 @@ class Connectors { public static final String HTTP_PROTOCOL = "HTTP/1.1"; public static final String AJP_PROTOCOL = "AJP/1.3"; - private Connectors() { + private TomcatConnectors() { // only static stuff } 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 index 00000000000..fa1d251f5c9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebLogging.java @@ -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); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java index 24919d9d6e2..724a5cdc5ea 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java @@ -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 index 57b87e84938..00000000000 --- a/server/sonar-server/src/main/resources/org/sonar/server/platform/logback.xml +++ /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/EmbeddedTomcatTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java index 3dd916816e6..71c2fcc6f74 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java @@ -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/ProgrammaticLogbackValveTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java new file mode 100644 index 00000000000..ee436f7f9f3 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/app/ProgrammaticLogbackValveTest.java @@ -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(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java index ea80502c785..b4906a5ca14 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java @@ -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/LoggingTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/TomcatAccessLogTest.java index da9ee50d4af..7e081214eb3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/LoggingTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/TomcatAccessLogTest.java @@ -19,16 +19,13 @@ */ 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; @@ -38,12 +35,12 @@ 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; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; -public class LoggingTest { +public class TomcatAccessLogTest { + + TomcatAccessLog sut = new TomcatAccessLog(); @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -58,23 +55,16 @@ public class LoggingTest { 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); + props.set(ProcessConstants.PATH_LOGS, temp.newFolder().getAbsolutePath()); + sut.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"); - } - })); + verify(tomcat.getHost().getPipeline()).addValve(any(ProgrammaticLogbackValve.class)); } @Test public void log_when_started_and_stopped() { Logger logger = mock(Logger.class); - Logging.LifecycleLogger listener = new Logging.LifecycleLogger(logger); + TomcatAccessLog.LifecycleLogger listener = new TomcatAccessLog.LifecycleLogger(logger); LifecycleEvent event = new LifecycleEvent(mock(Lifecycle.class), "before_init", null); listener.lifecycleEvent(event); 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/TomcatConnectorsTest.java index 70ba503943b..6c4c7d49c80 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/app/ConnectorsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java @@ -34,14 +34,11 @@ 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 { +public class TomcatConnectorsTest { Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS); @@ -55,7 +52,7 @@ public class ConnectorsTest { p.setProperty("sonar.web.http.acceptCount", "20"); Props props = new Props(p); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat).setConnector(argThat(new PropertiesMatcher( ImmutableMap.<String, Object>of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20) @@ -66,7 +63,7 @@ public class ConnectorsTest { public void configure_default_thread_pool() throws Exception { Props props = new Props(new Properties()); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat).setConnector(argThat(new PropertiesMatcher( ImmutableMap.<String, Object>of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25) @@ -82,7 +79,7 @@ public class ConnectorsTest { p.setProperty("sonar.web.https.minThreads", "5"); Props props = new Props(p); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() { @Override @@ -108,7 +105,7 @@ public class ConnectorsTest { Props props = new Props(p); try { - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); fail(); } catch (IllegalStateException e) { assertThat(e.getMessage()).isEqualTo("HTTP connectors are disabled"); @@ -122,7 +119,7 @@ public class ConnectorsTest { p.setProperty("sonar.web.https.port", "9443"); Props props = new Props(p); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() { @Override @@ -142,27 +139,27 @@ public class ConnectorsTest { p.setProperty("sonar.web.https.port", "9443"); Props props = new Props(p); - Connectors.configure(tomcat, props); + 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(Connectors.HTTP_PROTOCOL); + 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(Connectors.AJP_PROTOCOL); + 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(Connectors.HTTP_PROTOCOL); + return c.getScheme().equals("https") && c.getPort() == 9443 && c.getProtocol().equals(TomcatConnectors.HTTP_PROTOCOL); } })); } @@ -175,7 +172,7 @@ public class ConnectorsTest { p.setProperty("sonar.web.https.port", "9000"); try { - Connectors.configure(tomcat, new Props(p)); + TomcatConnectors.configure(tomcat, new Props(p)); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessage("HTTP, AJP and HTTPS must not use the same port 9000"); @@ -189,7 +186,7 @@ public class ConnectorsTest { p.setProperty("sonar.ajp.port", "9009"); p.setProperty("sonar.web.https.port", "9443"); - Connectors.configure(tomcat, new Props(p)); + TomcatConnectors.configure(tomcat, new Props(p)); verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() { @Override @@ -221,7 +218,7 @@ public class ConnectorsTest { p.setProperty("sonar.web.https.port", "9443"); p.setProperty("sonar.web.host", "1.2.3.4"); - Connectors.configure(tomcat, new Props(p)); + TomcatConnectors.configure(tomcat, new Props(p)); verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() { @Override @@ -250,7 +247,7 @@ public class ConnectorsTest { Props props = new Props(p); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() { @Override @@ -272,7 +269,7 @@ public class ConnectorsTest { Props props = new Props(p); - Connectors.configure(tomcat, props); + TomcatConnectors.configure(tomcat, props); verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() { @Override 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 index 00000000000..467b58be54d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/app/WebLoggingTest.java @@ -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 index 504f5fc6797..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/config/logback-access.xml +++ /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> diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index 53fb2cfb207..c2a1b29cf57 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -189,11 +189,6 @@ #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 @@ -258,6 +253,27 @@ # 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 diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java index af325f1d370..e70c34df4fe 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -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 index 00000000000..6c8e56553da --- /dev/null +++ b/sonar-application/src/main/java/org/sonar/application/AppLogging.java @@ -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 index cbcd991ded9..00000000000 --- a/sonar-application/src/main/resources/org/sonar/application/logback.xml +++ /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 index 00000000000..e2c902aaf39 --- /dev/null +++ b/sonar-application/src/test/java/org/sonar/application/AppLoggingTest.java @@ -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(); + } +} diff --git a/sonar-application/src/test/resources/logback-test.xml b/sonar-application/src/test/resources/logback-test.xml index ff2270cc122..f62e5c2b41d 100644 --- a/sonar-application/src/test/resources/logback-test.xml +++ b/sonar-application/src/test/resources/logback-test.xml @@ -1,28 +1,8 @@ <?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> %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n 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 index 00000000000..dab1503bce8 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Logger.java @@ -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 index 00000000000..6a9f278e112 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/Loggers.java @@ -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 index 00000000000..b7f2fa061de --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/InMemoryLogger.java @@ -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 index 00000000000..d1cfadd2bf5 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/log/internal/Slf4jLoggers.java @@ -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)); + } +} |