/*
* SonarQube
* Copyright (C) 2009-2025 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.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.encoder.Encoder;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
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 ch.qos.logback.core.util.FileSize;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.logging.LogManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
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;
import static org.sonar.process.ProcessProperties.Property.LOG_CONSOLE;
import static org.sonar.process.ProcessProperties.Property.LOG_JSON_OUTPUT;
import static org.sonar.process.ProcessProperties.Property.LOG_LEVEL;
import static org.sonar.process.ProcessProperties.Property.LOG_MAX_FILES;
import static org.sonar.process.ProcessProperties.Property.LOG_ROLLING_POLICY;
import static org.sonar.process.ProcessProperties.Property.PATH_LOGS;
/**
* Helps to configure Logback in a programmatic way, without using XML.
*/
public class LogbackHelper extends AbstractLogHelper {
private static final String LOGBACK_LOGGER_NAME_PATTERN = "%logger{20}";
public static final String DEPRECATION_LOGGER_NAME = "SONAR_DEPRECATION";
public LogbackHelper() {
super(LOGBACK_LOGGER_NAME_PATTERN);
}
public static Collection allowedLogLevels() {
return Arrays.asList(ALLOWED_ROOT_LOG_LEVELS);
}
@Override
public String getRootLoggerName() {
return ROOT_LOGGER_NAME;
}
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) {
LogManager.getLogManager().reset();
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
LevelChangePropagator propagator = new LevelChangePropagator();
propagator.setContext(loggerContext);
propagator.setResetJUL(true);
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) {
if (!ROOT_LOGGER_NAME.equals(logLevelConfig.getRootLoggerName())) {
throw new IllegalArgumentException("Value of LogLevelConfig#rootLoggerName must be \"" + ROOT_LOGGER_NAME + "\"");
}
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, LOG_LEVEL.getKey());
boolean traceGloballyEnabled = propertyValueAsLevel == Level.TRACE;
logLevelConfig.getOffUnlessTrace().forEach(logger -> applyHardUnlessTrace(rootContext, logger, traceGloballyEnabled));
return rootContext;
}
private static void applyLevelByProperty(Props props, Logger logger, List properties) {
logger.setLevel(resolveLevel(props, properties.toArray(new String[0])));
}
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)));
}
}
/**
* Creates a new {@link ConsoleAppender} to {@code System.out} with the specified name and log encoder.
*/
public ConsoleAppender newConsoleAppender(Context loggerContext, String name, Encoder encoder) {
ConsoleAppender consoleAppender = new ConsoleAppender<>();
consoleAppender.setContext(loggerContext);
consoleAppender.setEncoder(encoder);
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.
*
*
* - the file's name will use the prefix defined in {@link RootLoggerConfig#getProcessId()#getLogFilenamePrefix()}.
* - the file will follow the rotation policy defined in property {@link ProcessProperties.Property#LOG_ROLLING_POLICY} and
* the max number of files defined in property {@link org.sonar.process.ProcessProperties.Property#LOG_MAX_FILES}
* - the logs will follow the specified log encoder
*
*
*/
public void configureGlobalFileLog(Props props, RootLoggerConfig config, Encoder encoder) {
LoggerContext ctx = getRootContext();
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
FileAppender fileAppender = newFileAppender(ctx, props, config, encoder);
rootLogger.addAppender(fileAppender);
}
public FileAppender newFileAppender(LoggerContext ctx, Props props, RootLoggerConfig config, Encoder encoder) {
return newFileAppender(ctx, props, config.getProcessId().getLogFilenamePrefix(), encoder);
}
public FileAppender newFileAppender(LoggerContext ctx, Props props, String fileNamePrefix, Encoder encoder) {
RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, fileNamePrefix);
FileAppender fileAppender = rollingPolicy.createAppender("file_" + fileNamePrefix);
fileAppender.setContext(ctx);
fileAppender.setEncoder(encoder);
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.
*/
public void configureForSubprocessGobbler(Props props, Encoder encoder) {
if (isAllLogsToConsoleEnabled(props)) {
LoggerContext ctx = getRootContext();
ctx.getLogger(ROOT_LOGGER_NAME).addAppender(newConsoleAppender(ctx, "root_console", encoder));
}
}
/**
* 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 ProcessProperties.Property#LOG_CONSOLE}.
*/
public boolean isAllLogsToConsoleEnabled(Props props) {
return props.valueAsBoolean(LOG_CONSOLE.getKey(), 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 Encoder createEncoder(Props props, RootLoggerConfig config, LoggerContext context) {
return props.valueAsBoolean(LOG_JSON_OUTPUT.getKey(), Boolean.parseBoolean(LOG_JSON_OUTPUT.getDefaultValue()))
? createJsonEncoder(context, config)
: createPatternLayoutEncoder(context, buildLogPattern(config));
}
public Encoder createJsonEncoder(LoggerContext context, RootLoggerConfig config) {
LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>();
encoder.setLayout(new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField(), config.getExcludedFields()));
encoder.setContext(context);
encoder.start();
return encoder;
}
public PatternLayoutEncoder createPatternLayoutEncoder(LoggerContext context, String pattern) {
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern(pattern);
encoder.start();
return encoder;
}
public RollingPolicy createRollingPolicy(Context ctx, Props props, String filenamePrefix) {
String rollingPolicy = props.value(LOG_ROLLING_POLICY.getKey(), "time:yyyy-MM-dd");
int maxFiles = props.valueAsInt(LOG_MAX_FILES.getKey(), 7);
File logsDir = props.nonNullValueAsFile(PATH_LOGS.getKey());
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", LOG_ROLLING_POLICY.getKey(), 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 createAppender(String appenderName);
}
/**
* Log files are not rotated, for example when 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 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 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 createAppender(String appenderName) {
RollingFileAppender 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 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 createAppender(String appenderName) {
RollingFileAppender appender = new RollingFileAppender<>();
appender.setContext(context);
appender.setName(appenderName);
String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath();
appender.setFile(filePath);
SizeBasedTriggeringPolicy trigger = new SizeBasedTriggeringPolicy<>();
trigger.setMaxFileSize(FileSize.valueOf(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;
}
}
}