/* * 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 java.io.File; import java.util.List; import java.util.Objects; import java.util.Properties; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.process.MessageException; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.lang.String.format; import static java.lang.String.valueOf; import static java.util.Optional.ofNullable; 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; public class Log4JPropertiesBuilder extends AbstractLogHelper { private static final String PATTERN_LAYOUT = "PatternLayout"; private static final String ROOT_LOGGER_NAME = "rootLogger"; private static final int UNLIMITED_MAX_FILES = 100_000; private final Properties log4j2Properties = new Properties(); private final Props props; private RootLoggerConfig config; private String logPattern; private boolean allLogsToConsole; private File logDir; private LogLevelConfig logLevelConfig; private boolean jsonOutput; public Log4JPropertiesBuilder(Props props) { super("%logger{1.}"); this.props = Objects.requireNonNull(props, "Props can't be null"); internalLogLevel(Level.ERROR); } @Override public String getRootLoggerName() { return ROOT_LOGGER_NAME; } public Log4JPropertiesBuilder rootLoggerConfig(RootLoggerConfig config) { this.config = config; return this; } public Log4JPropertiesBuilder logPattern(String logPattern) { this.logPattern = logPattern; return this; } public Log4JPropertiesBuilder enableAllLogsToConsole(boolean allLogsToConsoleEnabled) { allLogsToConsole = allLogsToConsoleEnabled; return this; } public Log4JPropertiesBuilder jsonOutput(boolean jsonOutput) { this.jsonOutput = jsonOutput; return this; } public Log4JPropertiesBuilder logDir(File logDir) { this.logDir = logDir; return this; } public Log4JPropertiesBuilder logLevelConfig(LogLevelConfig logLevelConfig) { this.logLevelConfig = logLevelConfig; return this; } public Properties build() { checkNotNull(logDir, config); checkState(jsonOutput || (logPattern != null), "log pattern must be specified if not using json output"); configureGlobalFileLog(); if (allLogsToConsole) { configureGlobalStdoutLog(); } ofNullable(logLevelConfig).ifPresent(this::applyLogLevelConfiguration); Properties res = new Properties(); res.putAll(log4j2Properties); return res; } public Log4JPropertiesBuilder internalLogLevel(Level level) { putProperty("status", level.toString()); return this; } private void putProperty(String key, String value) { log4j2Properties.put(key, value); } private void putProperty(String prefix, String key, @Nullable String value) { log4j2Properties.put(prefix + key, value); } /** * Make log4j2 configuration for a process to push all its logs to a log file. *

*

*

* * @see #buildLogPattern(RootLoggerConfig) */ private void configureGlobalFileLog() { String appenderName = "file_" + config.getProcessId().getLogFilenamePrefix(); RollingPolicy rollingPolicy = createRollingPolicy(logDir, config.getProcessId().getLogFilenamePrefix()); writeFileAppender(appenderName, rollingPolicy, logPattern, jsonOutput); putProperty(ROOT_LOGGER_NAME + ".appenderRef." + appenderName + ".ref", appenderName); } private void configureGlobalStdoutLog() { String appenderName = "stdout"; writeConsoleAppender(appenderName, logPattern, jsonOutput); putProperty(ROOT_LOGGER_NAME + ".appenderRef." + appenderName + ".ref", appenderName); } private RollingPolicy createRollingPolicy(File logDir, String filenamePrefix) { String rollingPolicy = props.value(LOG_ROLLING_POLICY.getKey(), "time:yyyy-MM-dd"); int maxFiles = props.valueAsInt(LOG_MAX_FILES.getKey(), 7); if (maxFiles <= 0) { maxFiles = UNLIMITED_MAX_FILES; } if (rollingPolicy.startsWith("time:")) { return new TimeRollingPolicy(filenamePrefix, logDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "time:")); } else if (rollingPolicy.startsWith("size:")) { return new SizeRollingPolicy(filenamePrefix, logDir, maxFiles, StringUtils.substringAfter(rollingPolicy, "size:")); } else if ("none".equals(rollingPolicy)) { return new NoRollingPolicy(filenamePrefix, logDir); } else { throw new MessageException(format("Unsupported value for property %s: %s", LOG_ROLLING_POLICY.getKey(), rollingPolicy)); } } private void applyLogLevelConfiguration(LogLevelConfig logLevelConfig) { if (!ROOT_LOGGER_NAME.equals(logLevelConfig.getRootLoggerName())) { throw new IllegalArgumentException("Value of LogLevelConfig#rootLoggerName must be \"" + ROOT_LOGGER_NAME + "\""); } Level propertyValueAsLevel = getPropertyValueAsLevel(props, LOG_LEVEL.getKey()); boolean traceGloballyEnabled = propertyValueAsLevel == Level.TRACE; List loggerNames = Stream.of( logLevelConfig.getConfiguredByProperties().keySet().stream(), logLevelConfig.getConfiguredByHardcodedLevel().keySet().stream(), logLevelConfig.getOffUnlessTrace().stream().filter(k -> !traceGloballyEnabled)) .flatMap(s -> s) .filter(loggerName -> !ROOT_LOGGER_NAME.equals(loggerName)) .distinct() .sorted() .toList(); if (!loggerNames.isEmpty()) { putProperty("loggers", loggerNames.stream().collect(Collectors.joining(","))); } logLevelConfig.getConfiguredByProperties().forEach((loggerName, value) -> applyLevelByProperty(props, loggerName, value)); logLevelConfig.getConfiguredByHardcodedLevel().forEach(this::applyHardcodedLevel); logLevelConfig.getOffUnlessTrace().stream().filter(k -> !traceGloballyEnabled).forEach(logger -> applyHardcodedLevel(logger, Level.OFF)); } private void applyLevelByProperty(Props props, String loggerKey, List properties) { putLevel(loggerKey, resolveLevel(props, properties.toArray(new String[0]))); } private void applyHardcodedLevel(String loggerName, Level newLevel) { putLevel(loggerName, newLevel); } private void putLevel(String loggerName, Level level) { if (loggerName.equals(ROOT_LOGGER_NAME)) { putProperty(loggerName + ".level", level.toString()); } else { putProperty("logger." + loggerName + ".name", loggerName); putProperty("logger." + loggerName + ".level", level.toString()); } } private void writeFileAppender(String appenderName, RollingPolicy rollingPolicy, @Nullable String logPattern, boolean jsonOutput) { String prefix = "appender." + appenderName + "."; putProperty(prefix, "name", appenderName); writeAppenderLayout(logPattern, jsonOutput, prefix); rollingPolicy.writePolicy(prefix); } private void writeConsoleAppender(String appenderName, @Nullable String logPattern, boolean jsonOutput) { String prefix = "appender." + appenderName + "."; putProperty(prefix, "type", "Console"); putProperty(prefix, "name", appenderName); writeAppenderLayout(logPattern, jsonOutput, prefix); } private void writeAppenderLayout(@Nullable String logPattern, boolean jsonOutput, String prefix) { putProperty(prefix, "layout.type", PATTERN_LAYOUT); if (!jsonOutput) { putProperty(prefix, "layout.pattern", logPattern); } else { putProperty(prefix, "layout.pattern", getJsonPattern()); } } /** * json pattern based on https://github.com/elastic/elasticsearch/blob/7.13/server/src/main/java/org/elasticsearch/common/logging/ESJsonLayout.java */ private String getJsonPattern() { String json = "{"; if (!"".equals(config.getNodeNameField())) { json = json + jsonKey("nodename") + inQuotes(config.getNodeNameField()) + ","; } return json + jsonKey("process") + inQuotes(config.getProcessId().getKey()) + "," + jsonKey("timestamp") + inQuotes("%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}") + "," + jsonKey("severity") + inQuotes("%p") + "," + jsonKey("logger") + inQuotes("%c{1.}") + "," + jsonKey("message") + inQuotes("%notEmpty{%enc{%marker}{JSON} }%enc{%.-10000m}{JSON}") + "%exceptionAsJson " + "}" + System.lineSeparator(); } private static CharSequence jsonKey(String s) { return inQuotes(s) + ": "; } private static String inQuotes(String s) { return "\"" + s + "\""; } private abstract class RollingPolicy { final String filenamePrefix; final File logsDir; RollingPolicy(String filenamePrefix, File logsDir) { this.filenamePrefix = filenamePrefix; this.logsDir = logsDir; } abstract void writePolicy(String propertyPrefix); void writeTypeProperty(String propertyPrefix, String type) { putProperty(propertyPrefix + "type", type); } void writeFileNameProperty(String propertyPrefix) { putProperty(propertyPrefix + "fileName", new File(logsDir, filenamePrefix + ".log").getAbsolutePath()); } void writeFilePatternProperty(String propertyPrefix, String pattern) { putProperty(propertyPrefix + "filePattern", new File(logsDir, filenamePrefix + "." + pattern + ".log").getAbsolutePath()); } } /** * Log files are not rotated, for example when unix command logrotate is in place. */ private class NoRollingPolicy extends RollingPolicy { private NoRollingPolicy(String filenamePrefix, File logsDir) { super(filenamePrefix, logsDir); } @Override public void writePolicy(String propertyPrefix) { writeTypeProperty(propertyPrefix, "File"); writeFileNameProperty(propertyPrefix); } } private class TimeRollingPolicy extends RollingPolicy { private final String datePattern; private final int maxFiles; private TimeRollingPolicy(String filenamePrefix, File logsDir, int maxFiles, String datePattern) { super(filenamePrefix, logsDir); this.datePattern = datePattern; this.maxFiles = maxFiles; } @Override public void writePolicy(String propertyPrefix) { writeTypeProperty(propertyPrefix, "RollingFile"); writeFileNameProperty(propertyPrefix); writeFilePatternProperty(propertyPrefix, "%d{" + datePattern + "}"); putProperty(propertyPrefix + "policies.type", "Policies"); putProperty(propertyPrefix + "policies.time.type", "TimeBasedTriggeringPolicy"); putProperty(propertyPrefix + "policies.time.interval", "1"); putProperty(propertyPrefix + "policies.time.modulate", "true"); putProperty(propertyPrefix + "strategy.type", "DefaultRolloverStrategy"); putProperty(propertyPrefix + "strategy.fileIndex", "nomax"); putProperty(propertyPrefix + "strategy.action.type", "Delete"); putProperty(propertyPrefix + "strategy.action.basepath", logsDir.getAbsolutePath()); putProperty(propertyPrefix + "strategy.action.maxDepth", valueOf(1)); putProperty(propertyPrefix + "strategy.action.condition.type", "IfFileName"); putProperty(propertyPrefix + "strategy.action.condition.glob", filenamePrefix + "*"); putProperty(propertyPrefix + "strategy.action.condition.nested_condition.type", "IfAccumulatedFileCount"); putProperty(propertyPrefix + "strategy.action.condition.nested_condition.exceeds", valueOf(maxFiles)); } } private class SizeRollingPolicy extends RollingPolicy { private final String size; private final int maxFiles; private SizeRollingPolicy(String filenamePrefix, File logsDir, int maxFiles, String size) { super(filenamePrefix, logsDir); this.size = size; this.maxFiles = maxFiles; } @Override public void writePolicy(String propertyPrefix) { writeTypeProperty(propertyPrefix, "RollingFile"); writeFileNameProperty(propertyPrefix); writeFilePatternProperty(propertyPrefix, "%i"); putProperty(propertyPrefix + "policies.type", "Policies"); putProperty(propertyPrefix + "policies.size.type", "SizeBasedTriggeringPolicy"); putProperty(propertyPrefix + "policies.size.size", size); putProperty(propertyPrefix + "strategy.type", "DefaultRolloverStrategy"); putProperty(propertyPrefix + "strategy.max", valueOf(maxFiles)); } } }