@@ -243,7 +243,7 @@ public class LogbackHelper extends AbstractLogHelper { | |||
public Encoder<ILoggingEvent> createJsonEncoder(LoggerContext context, RootLoggerConfig config) { | |||
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<>(); | |||
encoder.setLayout(new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField())); | |||
encoder.setLayout(new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField(), config.getExcludedFields())); | |||
encoder.setContext(context); | |||
encoder.start(); | |||
return encoder; |
@@ -30,6 +30,7 @@ import java.io.StringWriter; | |||
import java.time.Instant; | |||
import java.time.ZoneId; | |||
import java.time.format.DateTimeFormatter; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.regex.Pattern; | |||
@@ -52,10 +53,16 @@ public class LogbackJsonLayout extends LayoutBase<ILoggingEvent> { | |||
private final String processKey; | |||
private final String nodeName; | |||
private final List<String> exclusions; | |||
public LogbackJsonLayout(String processKey, String nodeName) { | |||
this(processKey, nodeName, List.of()); | |||
} | |||
public LogbackJsonLayout(String processKey, String nodeName, List<String> exclusions) { | |||
this.processKey = requireNonNull(processKey); | |||
this.nodeName = nodeName; | |||
this.exclusions = exclusions; | |||
} | |||
String getProcessKey() { | |||
@@ -72,7 +79,7 @@ public class LogbackJsonLayout extends LayoutBase<ILoggingEvent> { | |||
} | |||
json.name("process").value(processKey); | |||
for (Map.Entry<String, String> entry : event.getMDCPropertyMap().entrySet()) { | |||
if (entry.getValue() != null) { | |||
if (entry.getValue() != null && !exclusions.contains(entry.getKey())) { | |||
json.name(entry.getKey()).value(entry.getValue()); | |||
} | |||
} |
@@ -19,6 +19,8 @@ | |||
*/ | |||
package org.sonar.process.logging; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
@@ -30,11 +32,13 @@ public final class RootLoggerConfig { | |||
private final ProcessId processId; | |||
private final String threadIdFieldPattern; | |||
private final String nodeNameField; | |||
private final List<String> excludedFields; | |||
private RootLoggerConfig(Builder builder) { | |||
this.processId = requireNonNull(builder.processId); | |||
this.threadIdFieldPattern = builder.threadIdFieldPattern; | |||
this.nodeNameField = Optional.ofNullable(builder.nodeNameField).orElse(""); | |||
this.excludedFields = Optional.ofNullable(builder.excludedFields).orElse(List.of()); | |||
} | |||
public static Builder newRootLoggerConfigBuilder() { | |||
@@ -53,11 +57,16 @@ public final class RootLoggerConfig { | |||
return threadIdFieldPattern; | |||
} | |||
public List<String> getExcludedFields() { | |||
return excludedFields; | |||
} | |||
public static final class Builder { | |||
@CheckForNull | |||
private ProcessId processId; | |||
private String threadIdFieldPattern = ""; | |||
private String nodeNameField; | |||
private List<String> excludedFields; | |||
private Builder() { | |||
// prevents instantiation outside RootLoggerConfig, use static factory method | |||
@@ -78,6 +87,11 @@ public final class RootLoggerConfig { | |||
return this; | |||
} | |||
public Builder setExcludedFields(Collection<String> excludedFields) { | |||
this.excludedFields = excludedFields.stream().toList(); | |||
return this; | |||
} | |||
public RootLoggerConfig build() { | |||
return new RootLoggerConfig(this); | |||
} |
@@ -582,8 +582,8 @@ public class LogbackHelperTest { | |||
@Test | |||
public void createJsonEncoder_shouldStartJsonEncoder() { | |||
RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(WEB_SERVER).build(); | |||
LogbackJsonLayout expectedJsonLayout = new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField()); | |||
RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(WEB_SERVER).setExcludedFields(List.of("LOGIN")).build(); | |||
LogbackJsonLayout expectedJsonLayout = new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField(), List.of("LOGIN")); | |||
Encoder<ILoggingEvent> result = underTest.createJsonEncoder(underTest.getRootContext(), config); | |||
@@ -24,6 +24,7 @@ import ch.qos.logback.classic.Logger; | |||
import ch.qos.logback.classic.spi.LoggingEvent; | |||
import com.google.gson.Gson; | |||
import java.time.Instant; | |||
import java.util.List; | |||
import org.junit.Test; | |||
import org.slf4j.LoggerFactory; | |||
import org.slf4j.MDC; | |||
@@ -151,6 +152,22 @@ public class LogbackJsonLayoutTest { | |||
} | |||
} | |||
@Test | |||
public void doLayout_whenMDC_shouldNotContainExcludedFields() { | |||
try { | |||
LogbackJsonLayout logbackJsonLayout = new LogbackJsonLayout("web", "", List.of("fromMdc")); | |||
LoggingEvent event = new LoggingEvent("org.foundation.Caller", (Logger) LoggerFactory.getLogger("the.logger"), Level.WARN, "the message", null, new Object[0]); | |||
MDC.put("fromMdc", "foo"); | |||
String log = logbackJsonLayout.doLayout(event); | |||
JsonLog json = new Gson().fromJson(log, JsonLog.class); | |||
assertThat(json.fromMdc).isNull(); | |||
} finally { | |||
MDC.clear(); | |||
} | |||
} | |||
private static class JsonLog { | |||
private String process; | |||
private String timestamp; |
@@ -26,18 +26,22 @@ import ch.qos.logback.classic.spi.ILoggingEvent; | |||
import ch.qos.logback.core.ConsoleAppender; | |||
import ch.qos.logback.core.FileAppender; | |||
import ch.qos.logback.core.encoder.Encoder; | |||
import org.sonar.process.ProcessId; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogDomain; | |||
import org.sonar.process.logging.LogLevelConfig; | |||
import org.sonar.process.logging.RootLoggerConfig; | |||
import org.sonar.server.log.ServerProcessLogging; | |||
import static org.apache.commons.lang.StringUtils.EMPTY; | |||
import static org.apache.commons.lang.StringUtils.isBlank; | |||
import static org.sonar.process.ProcessId.WEB_SERVER; | |||
import static org.sonar.process.ProcessProperties.Property.LOG_JSON_OUTPUT; | |||
import static org.sonar.process.logging.AbstractLogHelper.PREFIX_LOG_FORMAT; | |||
import static org.sonar.process.logging.AbstractLogHelper.SUFFIX_LOG_FORMAT; | |||
import static org.sonar.process.logging.LogbackHelper.DEPRECATION_LOGGER_NAME; | |||
import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; | |||
import static org.sonar.server.authentication.UserSessionInitializer.USER_LOGIN_MDC_KEY; | |||
import static org.sonar.server.platform.web.logging.EntrypointMDCStorage.ENTRYPOINT_MDC_KEY; | |||
import static org.sonar.server.platform.web.requestid.RequestIdMDCStorage.HTTP_REQUEST_ID_MDC_KEY; | |||
@@ -49,16 +53,18 @@ public class WebServerProcessLogging extends ServerProcessLogging { | |||
private static final String DEPRECATION_LOG_FILE_PREFIX = "deprecation"; | |||
private static final String ENABLE_LOGIN_PROPERTY = "sonar.deprecationLogs.loginEnabled"; | |||
public WebServerProcessLogging() { | |||
super(ProcessId.WEB_SERVER, "%X{" + HTTP_REQUEST_ID_MDC_KEY + "}"); | |||
super(WEB_SERVER, "%X{" + HTTP_REQUEST_ID_MDC_KEY + "}"); | |||
} | |||
@Override | |||
protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) { | |||
logLevelConfigBuilder.levelByDomain("sql", ProcessId.WEB_SERVER, LogDomain.SQL); | |||
logLevelConfigBuilder.levelByDomain("es", ProcessId.WEB_SERVER, LogDomain.ES); | |||
logLevelConfigBuilder.levelByDomain("auth.event", ProcessId.WEB_SERVER, LogDomain.AUTH_EVENT); | |||
JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, ProcessId.WEB_SERVER, LogDomain.JMX)); | |||
logLevelConfigBuilder.levelByDomain("sql", WEB_SERVER, LogDomain.SQL); | |||
logLevelConfigBuilder.levelByDomain("es", WEB_SERVER, LogDomain.ES); | |||
logLevelConfigBuilder.levelByDomain("auth.event", WEB_SERVER, LogDomain.AUTH_EVENT); | |||
JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, WEB_SERVER, LogDomain.JMX)); | |||
logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.ContainerBase"); | |||
logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.StandardContext"); | |||
@@ -72,10 +78,25 @@ public class WebServerProcessLogging extends ServerProcessLogging { | |||
configureDeprecatedApiLogger(props); | |||
} | |||
@Override | |||
protected RootLoggerConfig buildRootLoggerConfig(Props props) { | |||
return getRootLoggerConfigBuilder(props, List.of(USER_LOGIN_MDC_KEY)).build(); | |||
} | |||
private static RootLoggerConfig.Builder getRootLoggerConfigBuilder(Props props, Collection<String> excludedFields) { | |||
return newRootLoggerConfigBuilder() | |||
.setProcessId(WEB_SERVER) | |||
.setNodeNameField(getNodeNameWhenCluster(props)) | |||
.setThreadIdFieldPattern("%X{" + HTTP_REQUEST_ID_MDC_KEY + "}") | |||
.setExcludedFields(excludedFields.stream().toList()); | |||
} | |||
private void configureDeprecatedApiLogger(Props props) { | |||
LoggerContext context = helper.getRootContext(); | |||
RootLoggerConfig config = buildRootLoggerConfig(props); | |||
boolean isLoginEnabled = props.valueAsBoolean(ENABLE_LOGIN_PROPERTY, false); | |||
RootLoggerConfig config = getRootLoggerConfigBuilder(props, isLoginEnabled ? List.of() : List.of(USER_LOGIN_MDC_KEY)).build(); | |||
Encoder<ILoggingEvent> encoder = props.valueAsBoolean(LOG_JSON_OUTPUT.getKey(), Boolean.parseBoolean(LOG_JSON_OUTPUT.getDefaultValue())) | |||
? helper.createJsonEncoder(context, config) | |||
: helper.createPatternLayoutEncoder(context, buildDepractedLogPatrern(config)); | |||
@@ -90,11 +111,12 @@ public class WebServerProcessLogging extends ServerProcessLogging { | |||
} | |||
private static String buildDepractedLogPatrern(RootLoggerConfig config) { | |||
String userLoginPattern = " %X{" + USER_LOGIN_MDC_KEY + "}"; | |||
return PREFIX_LOG_FORMAT | |||
+ (isBlank(config.getNodeNameField()) ? "" : (config.getNodeNameField() + " ")) | |||
+ config.getProcessId().getKey() | |||
+ "[" + config.getThreadIdFieldPattern() + "]" | |||
+ " %X{" + USER_LOGIN_MDC_KEY + "}" | |||
+ (config.getExcludedFields().contains(USER_LOGIN_MDC_KEY) ? EMPTY : userLoginPattern) | |||
+ " %X{" + ENTRYPOINT_MDC_KEY + "}" | |||
+ SUFFIX_LOG_FORMAT; | |||
} |
@@ -32,10 +32,14 @@ import ch.qos.logback.core.encoder.Encoder; | |||
import ch.qos.logback.core.encoder.LayoutWrappingEncoder; | |||
import ch.qos.logback.core.joran.spi.JoranException; | |||
import com.google.common.collect.ImmutableList; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Properties; | |||
import java.util.stream.Stream; | |||
import org.junit.AfterClass; | |||
@@ -43,6 +47,7 @@ import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogbackHelper; | |||
import org.sonar.process.logging.LogbackJsonLayout; | |||
@@ -53,6 +58,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
import static org.sonar.process.ProcessProperties.Property.PATH_LOGS; | |||
@RunWith(DataProviderRunner.class) | |||
public class WebServerProcessLoggingTest { | |||
@Rule | |||
@@ -525,9 +531,20 @@ public class WebServerProcessLoggingTest { | |||
assertThat(((LayoutWrappingEncoder) encoder).getLayout()).isInstanceOf(LogbackJsonLayout.class); | |||
} | |||
@DataProvider | |||
public static Object[][] configuration() { | |||
return new Object[][] { | |||
{Map.of("sonar.deprecationLogs.loginEnabled", "true"), "%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}] %X{LOGIN} %X{ENTRYPOINT} %msg%n"}, | |||
{Map.of("sonar.deprecationLogs.loginEnabled", "false"), "%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}] %X{ENTRYPOINT} %msg%n"}, | |||
{Map.of(), "%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}] %X{ENTRYPOINT} %msg%n"}, | |||
}; | |||
} | |||
@Test | |||
public void configure_whenJsonPropFalse_shouldConfigureDeprecatedLoggerWithPatternLayout() { | |||
@UseDataProvider("configuration") | |||
public void configure_whenJsonPropFalse_shouldConfigureDeprecatedLoggerWithPatternLayout(Map<String, String> additionalProps, String expectedPattern) { | |||
props.set("sonar.log.jsonOutput", "false"); | |||
additionalProps.forEach(props::set); | |||
LoggerContext context = underTest.configure(props); | |||
@@ -540,7 +557,7 @@ public class WebServerProcessLoggingTest { | |||
Encoder<ILoggingEvent> encoder = fileAppender.getEncoder(); | |||
assertThat(encoder).isInstanceOf(PatternLayoutEncoder.class); | |||
PatternLayoutEncoder patternLayoutEncoder = (PatternLayoutEncoder) encoder; | |||
assertThat(patternLayoutEncoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}] %X{LOGIN} %X{ENTRYPOINT} %msg%n"); | |||
assertThat(patternLayoutEncoder.getPattern()).isEqualTo(expectedPattern); | |||
} | |||
@Test |