/* * 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.process.logging; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.jul.LevelChangePropagator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggerContextListener; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.Context; import ch.qos.logback.core.FileAppender; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.List; import javax.annotation.CheckForNull; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; import org.sonar.process.MessageException; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; import static java.lang.String.format; import static org.slf4j.Logger.ROOT_LOGGER_NAME; /** * Helps to configure Logback in a programmatic way, without using XML. */ public class LogbackHelper { private static final String SONAR_LOG_LEVEL_PROPERTY = "sonar.log.level"; private static final String ALL_LOGS_TO_CONSOLE_PROPERTY = "sonar.log.console"; private static final String PROCESS_NAME_PLACEHOLDER = "XXXX"; private static final String THREAD_ID_PLACEHOLDER = "ZZZZ"; private static final String ROLLING_POLICY_PROPERTY = "sonar.log.rollingPolicy"; private static final String MAX_FILES_PROPERTY = "sonar.log.maxFiles"; private static final String LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level " + PROCESS_NAME_PLACEHOLDER + "[" + THREAD_ID_PLACEHOLDER + "][%logger{20}] %msg%n"; private static final Level[] ALLOWED_ROOT_LOG_LEVELS = new Level[] {Level.TRACE, Level.DEBUG, Level.INFO}; public static Collection<Level> allowedLogLevels() { return Arrays.asList(ALLOWED_ROOT_LOG_LEVELS); } public LoggerContext getRootContext() { org.slf4j.Logger logger; while (!((logger = LoggerFactory.getLogger(ROOT_LOGGER_NAME)) instanceof Logger)) { // It occurs when the initialization step is still not finished because of a race condition // on ILoggerFactory.getILoggerFactory // http://jira.qos.ch/browse/SLF4J-167 // Substitute loggers are used. // http://www.slf4j.org/codes.html#substituteLogger // Bug is not fixed in SLF4J 1.7.14. try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } return ((Logger) logger).getLoggerContext(); } public LoggerContextListener enableJulChangePropagation(LoggerContext loggerContext) { LevelChangePropagator propagator = new LevelChangePropagator(); propagator.setContext(loggerContext); propagator.start(); loggerContext.addListener(propagator); return propagator; } /** * Applies the specified {@link LogLevelConfig} reading the specified {@link Props}. * * @throws IllegalArgumentException if the any level specified in a property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} */ public LoggerContext apply(LogLevelConfig logLevelConfig, Props props) { LoggerContext rootContext = getRootContext(); logLevelConfig.getConfiguredByProperties().forEach((key, value) -> applyLevelByProperty(props, rootContext.getLogger(key), value)); logLevelConfig.getConfiguredByHardcodedLevel().forEach((key, value) -> applyHardcodedLevel(rootContext, key, value)); Level propertyValueAsLevel = getPropertyValueAsLevel(props, SONAR_LOG_LEVEL_PROPERTY); boolean traceGloballyEnabled = propertyValueAsLevel == Level.TRACE; logLevelConfig.getOffUnlessTrace().forEach(logger -> applyHardUnlessTrace(rootContext, logger, traceGloballyEnabled)); return rootContext; } private void applyLevelByProperty(Props props, Logger logger, List<String> properties) { logger.setLevel(resolveLevel(props, properties.stream().toArray(String[]::new))); } /** * Resolve a log level reading the value of specified properties. * <p> * To compute the applied log level the following rules will be followed: * <ul>the last property with a defined and valid value in the order of the {@code propertyKeys} argument will be applied</ul> * <ul>if there is none, {@link Level#INFO INFO} will be returned</ul> * </p> * * @throws IllegalArgumentException if the value of the specified property is not one of {@link #ALLOWED_ROOT_LOG_LEVELS} */ private static Level resolveLevel(Props props, String... propertyKeys) { Level newLevel = Level.INFO; for (String propertyKey : propertyKeys) { Level level = getPropertyValueAsLevel(props, propertyKey); if (level != null) { newLevel = level; } } return newLevel; } private static void applyHardcodedLevel(LoggerContext rootContext, String loggerName, Level newLevel) { rootContext.getLogger(loggerName).setLevel(newLevel); } private static void applyHardUnlessTrace(LoggerContext rootContext, String logger, boolean traceGloballyEnabled) { if (!traceGloballyEnabled) { rootContext.getLogger(logger).setLevel(Level.OFF); } } public void changeRoot(LogLevelConfig logLevelConfig, Level newLevel) { ensureSupportedLevel(newLevel); LoggerContext rootContext = getRootContext(); rootContext.getLogger(ROOT_LOGGER_NAME).setLevel(newLevel); logLevelConfig.getConfiguredByProperties().forEach((key, value) -> rootContext.getLogger(key).setLevel(newLevel)); } private static void ensureSupportedLevel(Level newLevel) { if (!isAllowed(newLevel)) { throw new IllegalArgumentException(format("%s log level is not supported (allowed levels are %s)", newLevel, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); } } public String buildLogPattern(RootLoggerConfig config) { return LOG_FORMAT .replace(PROCESS_NAME_PLACEHOLDER, config.getProcessId().getKey()) .replace(THREAD_ID_PLACEHOLDER, config.getThreadIdFieldPattern()); } /** * Creates a new {@link ConsoleAppender} to {@code System.out} with the specified name and log pattern. * * @see #buildLogPattern(RootLoggerConfig) */ public ConsoleAppender<ILoggingEvent> newConsoleAppender(Context loggerContext, String name, String logPattern) { PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder(); consoleEncoder.setContext(loggerContext); consoleEncoder.setPattern(logPattern); consoleEncoder.start(); ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>(); consoleAppender.setContext(loggerContext); consoleAppender.setEncoder(consoleEncoder); consoleAppender.setName(name); consoleAppender.setTarget("System.out"); consoleAppender.start(); return consoleAppender; } /** * Make logback configuration for a process to push all its logs to a log file. * <p> * <ul> * <li>the file's name will use the prefix defined in {@link RootLoggerConfig#getProcessId()#getLogFilenamePrefix()}.</li> * <li>the file will follow the rotation policy defined in property {@link #ROLLING_POLICY_PROPERTY} and * the max number of files defined in property {@link #MAX_FILES_PROPERTY}</li> * <li>the logs will follow the specified log pattern</li> * </ul> * </p> * * @see #buildLogPattern(RootLoggerConfig) */ public FileAppender<ILoggingEvent> configureGlobalFileLog(Props props, RootLoggerConfig config, String logPattern) { LoggerContext ctx = getRootContext(); Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); FileAppender<ILoggingEvent> fileAppender = newFileAppender(ctx, props, config, logPattern); rootLogger.addAppender(fileAppender); return fileAppender; } public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, RootLoggerConfig config, String logPattern) { RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, config.getProcessId().getLogFilenamePrefix()); FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + config.getProcessId().getLogFilenamePrefix()); fileAppender.setContext(ctx); PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder(); fileEncoder.setContext(ctx); fileEncoder.setPattern(logPattern); fileEncoder.start(); fileAppender.setEncoder(fileEncoder); fileAppender.start(); return fileAppender; } /** * Make the logback configuration for a sub process to correctly push all its logs to be read by a stream gobbler * on the sub process's System.out. * * @see #buildLogPattern(RootLoggerConfig) */ public void configureForSubprocessGobbler(Props props, String logPattern) { if (isAllLogsToConsoleEnabled(props)) { LoggerContext ctx = getRootContext(); ctx.getLogger(ROOT_LOGGER_NAME).addAppender(newConsoleAppender(ctx, "root_console", logPattern)); } } /** * Finds out whether we are in testing environment (usually ITs) and logs of all processes must be forward to * App's System.out. This is specified by the value of property {@link #ALL_LOGS_TO_CONSOLE_PROPERTY}. */ public boolean isAllLogsToConsoleEnabled(Props props) { return props.valueAsBoolean(ALL_LOGS_TO_CONSOLE_PROPERTY, false); } @CheckForNull private static Level getPropertyValueAsLevel(Props props, String propertyKey) { String value = props.value(propertyKey); if (value == null) { return null; } Level level = Level.toLevel(value, Level.INFO); if (!isAllowed(level)) { throw new IllegalArgumentException(format("log level %s in property %s is not a supported value (allowed levels are %s)", level, propertyKey, Arrays.toString(ALLOWED_ROOT_LOG_LEVELS))); } return level; } private static boolean isAllowed(Level level) { for (Level allowedRootLogLevel : ALLOWED_ROOT_LOG_LEVELS) { if (level.equals(allowedRootLogLevel)) { return true; } } return false; } public Level getLoggerLevel(String loggerName) { return getRootContext().getLogger(loggerName).getLevel(); } /** * Generally used to reset logback in logging tests */ public void resetFromXml(String xmlResourcePath) throws JoranException { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); context.reset(); configurator.doConfigure(LogbackHelper.class.getResource(xmlResourcePath)); } public RollingPolicy createRollingPolicy(Context ctx, Props props, String filenamePrefix) { String rollingPolicy = props.value(ROLLING_POLICY_PROPERTY, "time:yyyy-MM-dd"); int maxFiles = props.valueAsInt(MAX_FILES_PROPERTY, 7); File logsDir = props.nonNullValueAsFile(ProcessProperties.PATH_LOGS); if (rollingPolicy.startsWith("time:")) { return new TimeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "time:")); } else if (rollingPolicy.startsWith("size:")) { return new SizeRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "size:")); } else if ("none".equals(rollingPolicy)) { return new NoRollingPolicy(ctx, filenamePrefix, logsDir, maxFiles); } else { throw new MessageException(format("Unsupported value for property %s: %s", ROLLING_POLICY_PROPERTY, rollingPolicy)); } } public abstract static class RollingPolicy { protected final Context context; final String filenamePrefix; final File logsDir; final int maxFiles; RollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) { this.context = context; this.filenamePrefix = filenamePrefix; this.logsDir = logsDir; this.maxFiles = maxFiles; } public abstract FileAppender<ILoggingEvent> createAppender(String appenderName); } /** * Log files are not rotated, for example for unix command logrotate is in place. */ private static class NoRollingPolicy extends RollingPolicy { private NoRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles) { super(context, filenamePrefix, logsDir, maxFiles); } @Override public FileAppender<ILoggingEvent> createAppender(String appenderName) { FileAppender<ILoggingEvent> appender = new FileAppender<>(); appender.setContext(context); appender.setFile(new File(logsDir, filenamePrefix + ".log").getAbsolutePath()); appender.setName(appenderName); return appender; } } /** * Log files are rotated according to time (one file per day, month or year). * See http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy */ private static class TimeRollingPolicy extends RollingPolicy { private final String datePattern; private TimeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String datePattern) { super(context, filenamePrefix, logsDir, maxFiles); this.datePattern = datePattern; } @Override public FileAppender<ILoggingEvent> createAppender(String appenderName) { RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>(); appender.setContext(context); appender.setName(appenderName); String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath(); appender.setFile(filePath); TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy(); rollingPolicy.setContext(context); rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%d{" + datePattern + "}.log")); rollingPolicy.setMaxHistory(maxFiles); rollingPolicy.setParent(appender); rollingPolicy.start(); appender.setRollingPolicy(rollingPolicy); return appender; } } /** * Log files are rotated according to their size. * See http://logback.qos.ch/manual/appenders.html#FixedWindowRollingPolicy */ private static class SizeRollingPolicy extends RollingPolicy { private final String size; private SizeRollingPolicy(Context context, String filenamePrefix, File logsDir, int maxFiles, String parameter) { super(context, filenamePrefix, logsDir, maxFiles); this.size = parameter; } @Override public FileAppender<ILoggingEvent> createAppender(String appenderName) { RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>(); appender.setContext(context); appender.setName(appenderName); String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath(); appender.setFile(filePath); SizeBasedTriggeringPolicy<ILoggingEvent> trigger = new SizeBasedTriggeringPolicy<>(size); trigger.setContext(context); trigger.start(); appender.setTriggeringPolicy(trigger); FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); rollingPolicy.setContext(context); rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%i.log")); rollingPolicy.setMinIndex(1); rollingPolicy.setMaxIndex(maxFiles); rollingPolicy.setParent(appender); rollingPolicy.start(); appender.setRollingPolicy(rollingPolicy); return appender; } } }