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 /server/sonar-process | |
parent | 776b67bdfafe3602802e3c76f14b0a5fd3871d12 (diff) | |
download | sonarqube-c063afcb3a908e4205f4f3b605f5137489ae2eba.tar.gz sonarqube-c063afcb3a908e4205f4f3b605f5137489ae2eba.zip |
SONAR-6057 Make log rotation more configurable for production
Diffstat (limited to 'server/sonar-process')
6 files changed, 401 insertions, 68 deletions
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/> |