/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.application; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.FileAppender; import org.sonar.application.config.AppSettings; import org.sonar.application.process.StreamGobbler; import org.sonar.process.ProcessId; import org.sonar.process.ProcessProperties; import org.sonar.process.logging.LogLevelConfig; import org.sonar.process.logging.LogbackHelper; import org.sonar.process.logging.RootLoggerConfig; import static org.slf4j.Logger.ROOT_LOGGER_NAME; import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER; import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; /** * Configure logback for the APP process. * * <p> * SonarQube's logging use cases: * <ol> * <li> * SQ started as a background process (with {@code sonar.sh start}): * <ul> * <li> * logs produced by the JVM before logback is setup in the APP JVM or which can't be caught by logback * (such as JVM crash) must be written to sonar.log * </li> * <li> * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught * by logback (such as JVM crash) must be written to sonar.log * </li> * <li>each JVM writes its own logs into its dedicated file</li> * </ul> * </li> * <li> * SQ started in console with wrapper (ie. with {@code sonar.sh console}): * <ul> * <li> * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback * (such as JVM crash) must be written to sonar.log * </li> * <li> * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught * by logback (such as JVM crash) must be written to sonar.log * </li> * <li>each JVM writes its own logs into its dedicated file</li> * <li>APP JVM logs are written to the APP JVM {@code System.out}</li> * </ul> * </li> * <li> * SQ started from command line (ie. {@code java -jar sonar-application-X.Y.jar}): * <ul> * <li> * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback * (such as JVM crash) are the responsibility of the user to be dealt with * </li> * <li> * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught * by logback (such as JVM crash) must be written to APP's {@code System.out} * </li> * <li>each JVM writes its own logs into its dedicated file</li> * <li>APP JVM logs are written to the APP JVM {@code System.out}</li> * </ul> * </li> * <li> * SQ started from an IT (ie. from command line with {@code option -Dsonar.log.console=true}): * <ul> * <li> * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback * (such as JVM crash) are the responsibility of the developer or maven to be dealt with * </li> * <li> * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught * by logback (such as JVM crash) must be written to APP's {@code System.out} and are the responsibility of the * developer or maven to be dealt with * </li> * <li>each JVM writes its own logs into its dedicated file</li> * <li>logs of all 4 JVMs are also written to the APP JVM {@code System.out}</li> * </ul> * </li> * </ol> * </p> * */ public class AppLogging { private static final String CONSOLE_LOGGER = "console"; private static final String CONSOLE_PLAIN_APPENDER = "CONSOLE"; private static final String APP_CONSOLE_APPENDER = "APP_CONSOLE"; private static final String GOBBLER_PLAIN_CONSOLE = "GOBBLER_CONSOLE"; private static final RootLoggerConfig APP_ROOT_LOGGER_CONFIG = newRootLoggerConfigBuilder() .setProcessId(ProcessId.APP) .build(); private final LogbackHelper helper = new LogbackHelper(); private final AppSettings appSettings; public AppLogging(AppSettings appSettings) { this.appSettings = appSettings; } public LoggerContext configure() { LoggerContext ctx = helper.getRootContext(); ctx.reset(); helper.enableJulChangePropagation(ctx); configureConsole(ctx); if (helper.isAllLogsToConsoleEnabled(appSettings.getProps()) || !appSettings.getProps().valueAsBoolean("sonar.wrapped", false)) { configureWithLogbackWritingToFile(ctx); } else { configureWithWrapperWritingToFile(ctx); } helper.apply( LogLevelConfig.newBuilder() .rootLevelFor(ProcessId.APP) .immutableLevel("com.hazelcast", Level.toLevel( appSettings.getProps().nonNullValue(ProcessProperties.HAZELCAST_LOG_LEVEL))) .build(), appSettings.getProps()); return ctx; } /** * Creates a non additive logger dedicated to printing message as is (ie. assuming they are already formatted). * * It creates a dedicated appender to the System.out which applies no formatting the logs it receives. */ private void configureConsole(LoggerContext loggerContext) { ConsoleAppender<ILoggingEvent> consoleAppender = helper.newConsoleAppender(loggerContext, CONSOLE_PLAIN_APPENDER, "%msg%n"); Logger consoleLogger = loggerContext.getLogger(CONSOLE_LOGGER); consoleLogger.setAdditive(false); consoleLogger.addAppender(consoleAppender); } /** * The process has been started by orchestrator (ie. via {@code java -jar} and optionally passing the option {@code -Dsonar.log.console=true}). * Therefor, APP's System.out (and System.err) are <strong>not</strong> copied to sonar.log by the wrapper and * printing to sonar.log must be done at logback level. */ private void configureWithLogbackWritingToFile(LoggerContext ctx) { // configure all logs (ie. root logger) to be written to sonar.log and also to the console with formatting // in practice, this will be only APP's own logs as logs from sub processes LOGGER_GOBBLER and LOGGER_GOBBLER // is configured below to be detached from root // so, this will make all APP's log to be both written to sonar.log and visible in the console configureRootWithLogbackWritingToFile(ctx); // if option -Dsonar.log.console=true has been set, sub processes will write their logs to their own files but also // copy them to their System.out. // otherwise, the only logs to be expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or // when their JVM crashes // they must be printed to App's System.out as is (as they are already formatted) // logger is configured to be non additive as we don't want these logs to be written to sonar.log and duplicated in // the console (with an incorrect formatting) configureGobbler(ctx); } /** * SQ has been started by the wrapper (ie. with sonar.sh) therefor, APP's System.out (and System.err) are written to * sonar.log by the wrapper. */ private void configureWithWrapperWritingToFile(LoggerContext ctx) { // configure all logs (ie. root logger) to be written to console with formatting // in practice, this will be only APP's own logs as logs from sub processes are written to LOGGER_GOBBLER and // LOGGER_GOBBLER is configured below to be detached from root // logs are written to the console because we want them to be in sonar.log and the wrapper will write any log // from APP's System.out and System.err to sonar.log Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); rootLogger.addAppender(createAppConsoleAppender(ctx, helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG))); // in regular configuration, sub processes are not copying their logs to their System.out, so, the only logs to be // expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or when JVM crashes // so, they must be printed to App's System.out as is (as they are already formatted) and the wrapper will write // them to sonar.log // logger is configured to be non additive as we don't want these logs written to sonar.log and duplicated in the // console with an incorrect formatting configureGobbler(ctx); } private void configureRootWithLogbackWritingToFile(LoggerContext ctx) { Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); String appLogPattern = helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG); FileAppender<ILoggingEvent> fileAppender = helper.newFileAppender(ctx, appSettings.getProps(), APP_ROOT_LOGGER_CONFIG, appLogPattern); rootLogger.addAppender(fileAppender); rootLogger.addAppender(createAppConsoleAppender(ctx, appLogPattern)); } /** * Configure the logger to which logs from sub processes are written to * (called {@link StreamGobbler#LOGGER_GOBBLER}) by {@link StreamGobbler}, * to be: * <ol> * <li>non additive (ie. these logs will be output by the appender of {@link StreamGobbler#LOGGER_GOBBLER} and only this one)</li> * <li>write logs as is (ie. without any extra formatting)</li> * <li>write exclusively to App's System.out</li> * </ol> */ private void configureGobbler(LoggerContext ctx) { Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER); gobblerLogger.setAdditive(false); gobblerLogger.addAppender(helper.newConsoleAppender(ctx, GOBBLER_PLAIN_CONSOLE, "%msg%n")); } private ConsoleAppender<ILoggingEvent> createAppConsoleAppender(LoggerContext ctx, String appLogPattern) { return helper.newConsoleAppender(ctx, APP_CONSOLE_APPENDER, appLogPattern); } }