@@ -21,6 +21,7 @@ package org.sonar.ce.logging; | |||
import ch.qos.logback.classic.Level; | |||
import org.sonar.process.ProcessId; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogDomain; | |||
import org.sonar.process.logging.LogLevelConfig; | |||
import org.sonar.server.log.ServerProcessLogging; | |||
@@ -45,7 +46,7 @@ public class CeProcessLogging extends ServerProcessLogging { | |||
} | |||
@Override | |||
protected void extendConfigure() { | |||
protected void extendConfigure(Props props) { | |||
// nothing to do | |||
} | |||
} |
@@ -29,8 +29,8 @@ import static java.lang.String.format; | |||
public abstract class AbstractLogHelper { | |||
static final Level[] ALLOWED_ROOT_LOG_LEVELS = new Level[] {Level.TRACE, Level.DEBUG, Level.INFO}; | |||
private static final String PREFIX_LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level "; | |||
private static final String SUFFIX_LOG_FORMAT = " %msg%n"; | |||
public static final String PREFIX_LOG_FORMAT = "%d{yyyy.MM.dd HH:mm:ss} %-5level "; | |||
public static final String SUFFIX_LOG_FORMAT = " %msg%n"; | |||
private final String loggerNamePattern; | |||
protected AbstractLogHelper(String loggerNamePattern) { |
@@ -187,8 +187,12 @@ public class LogbackHelper extends AbstractLogHelper { | |||
} | |||
public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, RootLoggerConfig config, Encoder<ILoggingEvent> encoder) { | |||
RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, config.getProcessId().getLogFilenamePrefix()); | |||
FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + config.getProcessId().getLogFilenamePrefix()); | |||
return newFileAppender(ctx, props, config.getProcessId().getLogFilenamePrefix(), encoder); | |||
} | |||
public FileAppender<ILoggingEvent> newFileAppender(LoggerContext ctx, Props props, String fileNamePrefix, Encoder<ILoggingEvent> encoder) { | |||
RollingPolicy rollingPolicy = createRollingPolicy(ctx, props, fileNamePrefix); | |||
FileAppender<ILoggingEvent> fileAppender = rollingPolicy.createAppender("file_" + fileNamePrefix); | |||
fileAppender.setContext(ctx); | |||
fileAppender.setEncoder(encoder); | |||
fileAppender.start(); | |||
@@ -230,16 +234,23 @@ public class LogbackHelper extends AbstractLogHelper { | |||
} | |||
public Encoder<ILoggingEvent> createEncoder(Props props, RootLoggerConfig config, LoggerContext context) { | |||
if (props.valueAsBoolean(LOG_JSON_OUTPUT.getKey(), Boolean.parseBoolean(LOG_JSON_OUTPUT.getDefaultValue()))) { | |||
LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>(); | |||
encoder.setLayout(new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField())); | |||
encoder.setContext(context); | |||
encoder.start(); | |||
return encoder; | |||
} | |||
return props.valueAsBoolean(LOG_JSON_OUTPUT.getKey(), Boolean.parseBoolean(LOG_JSON_OUTPUT.getDefaultValue())) | |||
? createJsonEncoder(context, config) | |||
: createPatternLayoutEncoder(context, buildLogPattern(config)); | |||
} | |||
public Encoder<ILoggingEvent> createJsonEncoder(LoggerContext context, RootLoggerConfig config) { | |||
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<>(); | |||
encoder.setLayout(new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField())); | |||
encoder.setContext(context); | |||
encoder.start(); | |||
return encoder; | |||
} | |||
public PatternLayoutEncoder createPatternLayoutEncoder(LoggerContext context, String pattern) { | |||
PatternLayoutEncoder encoder = new PatternLayoutEncoder(); | |||
encoder.setContext(context); | |||
encoder.setPattern(buildLogPattern(config)); | |||
encoder.setPattern(pattern); | |||
encoder.start(); | |||
return encoder; | |||
} | |||
@@ -299,7 +310,7 @@ public class LogbackHelper extends AbstractLogHelper { | |||
/** | |||
* Log files are rotated according to time (one file per day, month or year). | |||
* See http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy | |||
* See <a href="http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy">TimeBasedRollingPolicy</a> | |||
*/ | |||
private static class TimeRollingPolicy extends RollingPolicy { | |||
private final String datePattern; | |||
@@ -317,7 +328,7 @@ public class LogbackHelper extends AbstractLogHelper { | |||
String filePath = new File(logsDir, filenamePrefix + ".log").getAbsolutePath(); | |||
appender.setFile(filePath); | |||
TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy(); | |||
TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<>(); | |||
rollingPolicy.setContext(context); | |||
rollingPolicy.setFileNamePattern(StringUtils.replace(filePath, filenamePrefix + ".log", filenamePrefix + ".%d{" + datePattern + "}.log")); | |||
rollingPolicy.setMaxHistory(maxFiles); | |||
@@ -331,7 +342,7 @@ public class LogbackHelper extends AbstractLogHelper { | |||
/** | |||
* Log files are rotated according to their size. | |||
* See http://logback.qos.ch/manual/appenders.html#FixedWindowRollingPolicy | |||
* See <a href="http://logback.qos.ch/manual/appenders.html#FixedWindowRollingPolicy">FixedWindowRollingPolicy</a> | |||
*/ | |||
private static class SizeRollingPolicy extends RollingPolicy { | |||
private final String size; |
@@ -61,6 +61,7 @@ import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.junit.Assert.fail; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
import static org.sonar.process.ProcessId.WEB_SERVER; | |||
import static org.sonar.process.ProcessProperties.Property.PATH_LOGS; | |||
import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; | |||
@@ -134,7 +135,7 @@ public class LogbackHelperTest { | |||
public void buildLogPattern_does_not_put_threadIdFieldPattern_from_RootLoggerConfig_is_empty() { | |||
String pattern = underTest.buildLogPattern( | |||
newRootLoggerConfigBuilder() | |||
.setProcessId(ProcessId.WEB_SERVER) | |||
.setProcessId(WEB_SERVER) | |||
.setThreadIdFieldPattern("") | |||
.build()); | |||
@@ -157,7 +158,7 @@ public class LogbackHelperTest { | |||
LoggerContext ctx = underTest.getRootContext(); | |||
String logbackRootLoggerName = underTest.getRootLoggerName(); | |||
LogLevelConfig config = LogLevelConfig.newBuilder(logbackRootLoggerName) | |||
.levelByDomain(logbackRootLoggerName, ProcessId.WEB_SERVER, LogDomain.JMX).build(); | |||
.levelByDomain(logbackRootLoggerName, WEB_SERVER, LogDomain.JMX).build(); | |||
props.set("sonar.log.level.web", "TRACE"); | |||
underTest.apply(config, props); | |||
@@ -313,7 +314,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_fails_with_IAE_if_global_property_has_unsupported_level() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
props.set("sonar.log.level", "ERROR"); | |||
@@ -324,7 +325,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_fails_with_IAE_if_process_property_has_unsupported_level() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
props.set("sonar.log.level.web", "ERROR"); | |||
@@ -335,7 +336,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_logger_to_INFO_if_no_property_is_set() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
LoggerContext context = underTest.apply(config, props); | |||
@@ -344,7 +345,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_logger_to_globlal_property_if_set() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
props.set("sonar.log.level", "TRACE"); | |||
@@ -355,7 +356,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_logger_to_process_property_if_set() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
props.set("sonar.log.level.web", "DEBUG"); | |||
@@ -366,7 +367,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_logger_to_process_property_over_global_property_if_both_set() { | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(ProcessId.WEB_SERVER).build(); | |||
LogLevelConfig config = newLogLevelConfig().rootLevelFor(WEB_SERVER).build(); | |||
props.set("sonar.log.level", "DEBUG"); | |||
props.set("sonar.log.level.web", "TRACE"); | |||
@@ -377,7 +378,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_domain_property_over_process_and_global_property_if_all_set() { | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES).build(); | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", WEB_SERVER, LogDomain.ES).build(); | |||
props.set("sonar.log.level", "DEBUG"); | |||
props.set("sonar.log.level.web", "DEBUG"); | |||
props.set("sonar.log.level.web.es", "TRACE"); | |||
@@ -389,7 +390,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_domain_property_over_process_property_if_both_set() { | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES).build(); | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", WEB_SERVER, LogDomain.ES).build(); | |||
props.set("sonar.log.level.web", "DEBUG"); | |||
props.set("sonar.log.level.web.es", "TRACE"); | |||
@@ -400,7 +401,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_sets_domain_property_over_global_property_if_both_set() { | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.ES).build(); | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", WEB_SERVER, LogDomain.ES).build(); | |||
props.set("sonar.log.level", "DEBUG"); | |||
props.set("sonar.log.level.web.es", "TRACE"); | |||
@@ -411,7 +412,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void apply_fails_with_IAE_if_domain_property_has_unsupported_level() { | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.JMX).build(); | |||
LogLevelConfig config = newLogLevelConfig().levelByDomain("foo", WEB_SERVER, LogDomain.JMX).build(); | |||
props.set("sonar.log.level.web.jmx", "ERROR"); | |||
@@ -433,8 +434,8 @@ public class LogbackHelperTest { | |||
@Test | |||
public void changeRoot_sets_level_of_ROOT_and_all_loggers_with_a_config_but_the_hardcoded_one() { | |||
LogLevelConfig config = newLogLevelConfig() | |||
.rootLevelFor(ProcessId.WEB_SERVER) | |||
.levelByDomain("foo", ProcessId.WEB_SERVER, LogDomain.JMX) | |||
.rootLevelFor(WEB_SERVER) | |||
.levelByDomain("foo", WEB_SERVER, LogDomain.JMX) | |||
.levelByDomain("bar", ProcessId.COMPUTE_ENGINE, LogDomain.ES) | |||
.immutableLevel("doh", Level.ERROR) | |||
.immutableLevel("pif", Level.TRACE) | |||
@@ -486,7 +487,7 @@ public class LogbackHelperTest { | |||
@Test | |||
public void createEncoder_uses_pattern_by_default() { | |||
RootLoggerConfig config = newRootLoggerConfigBuilder() | |||
.setProcessId(ProcessId.WEB_SERVER) | |||
.setProcessId(WEB_SERVER) | |||
.build(); | |||
Encoder<ILoggingEvent> encoder = underTest.createEncoder(props, config, underTest.getRootContext()); | |||
@@ -498,7 +499,7 @@ public class LogbackHelperTest { | |||
public void createEncoder_uses_json_output() { | |||
props.set("sonar.log.jsonOutput", "true"); | |||
RootLoggerConfig config = newRootLoggerConfigBuilder() | |||
.setProcessId(ProcessId.WEB_SERVER) | |||
.setProcessId(WEB_SERVER) | |||
.build(); | |||
Encoder<ILoggingEvent> encoder = underTest.createEncoder(props, config, underTest.getRootContext()); | |||
@@ -554,6 +555,54 @@ public class LogbackHelperTest { | |||
assertThat(underTest.isAllLogsToConsoleEnabled(new Props(properties))).isFalse(); | |||
} | |||
@Test | |||
public void newFileAppender_shouldStartAppender() { | |||
PatternLayoutEncoder patternLayoutEncoder = new PatternLayoutEncoder(); | |||
FileAppender<ILoggingEvent> result = underTest.newFileAppender(underTest.getRootContext(), props, "foo", patternLayoutEncoder); | |||
assertThat(result.isStarted()).isTrue(); | |||
assertThat(result.getName()).isEqualTo("file_foo"); | |||
assertThat(result.getEncoder()).isSameAs(patternLayoutEncoder); | |||
assertThat(result.getContext()).isSameAs(underTest.getRootContext()); | |||
} | |||
@Test | |||
public void newFileAppender_whenConfig_shouldStartAppenderWithConfigFileName() { | |||
PatternLayoutEncoder patternLayoutEncoder = new PatternLayoutEncoder(); | |||
RootLoggerConfig config = newRootLoggerConfigBuilder() | |||
.setProcessId(WEB_SERVER) | |||
.build(); | |||
FileAppender<ILoggingEvent> result = underTest.newFileAppender(underTest.getRootContext(), props, config, patternLayoutEncoder); | |||
assertThat(result.isStarted()).isTrue(); | |||
assertThat(result.getName()).isEqualTo("file_" + WEB_SERVER.getLogFilenamePrefix()); | |||
} | |||
@Test | |||
public void createJsonEncoder_shouldStartJsonEncoder() { | |||
RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(WEB_SERVER).build(); | |||
LogbackJsonLayout expectedJsonLayout = new LogbackJsonLayout(config.getProcessId().getKey(), config.getNodeNameField()); | |||
Encoder<ILoggingEvent> result = underTest.createJsonEncoder(underTest.getRootContext(), config); | |||
assertThat(result.isStarted()).isTrue(); | |||
assertThat(result).isInstanceOf(LayoutWrappingEncoder.class); | |||
LayoutWrappingEncoder layoutWrappingEncoder = (LayoutWrappingEncoder) result; | |||
assertThat(layoutWrappingEncoder.getLayout()).usingRecursiveComparison().isEqualTo(expectedJsonLayout); | |||
assertThat(layoutWrappingEncoder.getContext()).isSameAs(underTest.getRootContext()); | |||
} | |||
@Test | |||
public void createPatternLayoutEncoder_shouldStartPatternEncoder() { | |||
PatternLayoutEncoder result = underTest.createPatternLayoutEncoder(underTest.getRootContext(), "foo"); | |||
assertThat(result.isStarted()).isTrue(); | |||
assertThat(result.getPattern()).isEqualTo("foo"); | |||
assertThat(result.getContext()).isSameAs(underTest.getRootContext()); | |||
} | |||
public static class MemoryAppender extends AppenderBase<ILoggingEvent> { | |||
private static final List<ILoggingEvent> LOGS = new ArrayList<>(); | |||
@@ -59,7 +59,7 @@ public abstract class ServerProcessLogging { | |||
private final ProcessId processId; | |||
private final String threadIdFieldPattern; | |||
private final LogbackHelper helper = new LogbackHelper(); | |||
protected final LogbackHelper helper = new LogbackHelper(); | |||
private final LogLevelConfig logLevelConfig; | |||
protected ServerProcessLogging(ProcessId processId, String threadIdFieldPattern) { | |||
@@ -122,7 +122,7 @@ public abstract class ServerProcessLogging { | |||
configureRootLogger(props); | |||
helper.apply(logLevelConfig, props); | |||
configureDirectToConsoleLoggers(props, ctx, STARTUP_LOGGER_NAME); | |||
extendConfigure(); | |||
extendConfigure(props); | |||
helper.enableJulChangePropagation(ctx); | |||
@@ -135,21 +135,25 @@ public abstract class ServerProcessLogging { | |||
protected abstract void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder); | |||
protected abstract void extendConfigure(); | |||
protected abstract void extendConfigure(Props props); | |||
private void configureRootLogger(Props props) { | |||
RootLoggerConfig config = newRootLoggerConfigBuilder() | |||
RootLoggerConfig config = buildRootLoggerConfig(props); | |||
Encoder<ILoggingEvent> encoder = helper.createEncoder(props, config, helper.getRootContext()); | |||
helper.configureGlobalFileLog(props, config, encoder); | |||
helper.configureForSubprocessGobbler(props, encoder); | |||
} | |||
protected RootLoggerConfig buildRootLoggerConfig(Props props) { | |||
return newRootLoggerConfigBuilder() | |||
.setProcessId(processId) | |||
.setNodeNameField(getNodeNameWhenCluster(props)) | |||
.setThreadIdFieldPattern(threadIdFieldPattern) | |||
.build(); | |||
Encoder<ILoggingEvent> encoder = helper.createEncoder(props, config, helper.getRootContext()); | |||
helper.configureGlobalFileLog(props, config, encoder); | |||
helper.configureForSubprocessGobbler(props, encoder); | |||
} | |||
@CheckForNull | |||
private static String getNodeNameWhenCluster(Props props) { | |||
protected static String getNodeNameWhenCluster(Props props) { | |||
boolean clusterEnabled = props.valueAsBoolean(CLUSTER_ENABLED.getKey(), | |||
Boolean.parseBoolean(CLUSTER_ENABLED.getDefaultValue())); | |||
return clusterEnabled ? props.value(CLUSTER_NODE_NAME.getKey(), CLUSTER_NODE_NAME.getDefaultValue()) : null; |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.server.log; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.sonar.process.ProcessId; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogLevelConfig; | |||
import org.sonar.process.logging.RootLoggerConfig; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.process.ProcessId.WEB_SERVER; | |||
import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; | |||
public class ServerProcessLoggingTest { | |||
@Test | |||
public void buildRootLoggerConfig_shouldBuildConfig() { | |||
ServerProcessLogging serverProcessLogging = getServerProcessLoggingFakeImpl(WEB_SERVER, "threadIdFieldPattern"); | |||
Props props = Mockito.mock(Props.class); | |||
RootLoggerConfig expected = newRootLoggerConfigBuilder() | |||
.setProcessId(WEB_SERVER) | |||
.setNodeNameField(null) | |||
.setThreadIdFieldPattern("threadIdFieldPattern") | |||
.build(); | |||
RootLoggerConfig result = serverProcessLogging.buildRootLoggerConfig(props); | |||
assertThat(result).usingRecursiveComparison().isEqualTo(expected); | |||
} | |||
private ServerProcessLogging getServerProcessLoggingFakeImpl(ProcessId processId, String threadIdFieldPattern) { | |||
return new ServerProcessLogging(processId, threadIdFieldPattern) { | |||
@Override | |||
protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) {} | |||
@Override | |||
protected void extendConfigure(Props props) {} | |||
}; | |||
} | |||
} |
@@ -62,6 +62,11 @@ | |||
<filter-class>org.sonar.server.platform.web.CspFilter</filter-class> | |||
<async-supported>true</async-supported> | |||
</filter> | |||
<filter> | |||
<filter-name>EndpointPathFilter</filter-name> | |||
<filter-class>org.sonar.server.platform.web.EndpointPathFilter</filter-class> | |||
<async-supported>true</async-supported> | |||
</filter> | |||
<!-- order of execution is important --> | |||
<filter-mapping> | |||
@@ -76,6 +81,10 @@ | |||
<filter-name>RequestUidFilter</filter-name> | |||
<url-pattern>/*</url-pattern> | |||
</filter-mapping> | |||
<filter-mapping> | |||
<filter-name>EndpointPathFilter</filter-name> | |||
<url-pattern>/*</url-pattern> | |||
</filter-mapping> | |||
<filter-mapping> | |||
<filter-name>RedirectFilter</filter-name> | |||
<url-pattern>/*</url-pattern> |
@@ -25,6 +25,7 @@ import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.slf4j.MDC; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.server.authentication.BaseIdentityProvider; | |||
import org.sonar.api.server.http.Cookie; | |||
@@ -215,6 +216,37 @@ public class UserSessionInitializerIT { | |||
verify(response).addHeader("SonarQube-Authentication-Token-Expiration", formatDateTime(expirationTimestamp)); | |||
} | |||
@Test | |||
public void initUserSession_shouldPutLoginInMDC() { | |||
when(threadLocalSession.isLoggedIn()).thenReturn(false); | |||
when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user")); | |||
underTest.initUserSession(request, response); | |||
assertThat(MDC.get("LOGIN")).isEqualTo("user"); | |||
} | |||
@Test | |||
public void initUserSession_whenSessionLoginIsNull_shouldPutDefaultLoginValueInMDC() { | |||
when(threadLocalSession.isLoggedIn()).thenReturn(false); | |||
when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession()); | |||
underTest.initUserSession(request, response); | |||
assertThat(MDC.get("LOGIN")).isEqualTo("-"); | |||
} | |||
@Test | |||
public void removeUserSession_shoudlRemoveMDCLogin() { | |||
when(threadLocalSession.isLoggedIn()).thenReturn(false); | |||
when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user")); | |||
underTest.initUserSession(request, response); | |||
underTest.removeUserSession(); | |||
assertThat(MDC.get("LOGIN")).isNull(); | |||
} | |||
private void assertPathIsIgnored(String path) { | |||
when(request.getRequestURI()).thenReturn(path); | |||
@@ -21,6 +21,7 @@ package org.sonar.server.authentication; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import org.slf4j.MDC; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.impl.ws.StaticResources; | |||
import org.sonar.api.server.ServerSide; | |||
@@ -52,6 +53,8 @@ public class UserSessionInitializer { | |||
*/ | |||
private static final String ACCESS_LOG_LOGIN = "LOGIN"; | |||
public static final String USER_LOGIN_MDC_KEY = "LOGIN"; | |||
private static final String SQ_AUTHENTICATION_TOKEN_EXPIRATION = "SonarQube-Authentication-Token-Expiration"; | |||
// SONAR-6546 these urls should be get from WebService | |||
@@ -97,6 +100,7 @@ public class UserSessionInitializer { | |||
} | |||
public boolean initUserSession(HttpRequest request, HttpResponse response) { | |||
MDC.put(USER_LOGIN_MDC_KEY, "-"); | |||
String path = request.getRequestURI().replaceFirst(request.getContextPath(), ""); | |||
try { | |||
// Do not set user session when url is excluded | |||
@@ -137,6 +141,7 @@ public class UserSessionInitializer { | |||
threadLocalSession.set(session); | |||
checkTokenUserSession(response, session); | |||
request.setAttribute(ACCESS_LOG_LOGIN, defaultString(session.getLogin(), "-")); | |||
MDC.put(USER_LOGIN_MDC_KEY, defaultString(session.getLogin(), "-")); | |||
} | |||
private static void checkTokenUserSession(HttpResponse response, UserSession session) { | |||
@@ -147,6 +152,7 @@ public class UserSessionInitializer { | |||
} | |||
public void removeUserSession() { | |||
MDC.remove(USER_LOGIN_MDC_KEY); | |||
threadLocalSession.unload(); | |||
} | |||
@@ -20,18 +20,34 @@ | |||
package org.sonar.server.app; | |||
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.FileAppender; | |||
import ch.qos.logback.core.encoder.Encoder; | |||
import org.sonar.process.ProcessId; | |||
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.isBlank; | |||
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.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; | |||
import static org.sonar.server.ws.WebServiceEngine.DEPRECATION_LOGGER_NAME; | |||
/** | |||
* Configure logback for the Web Server process. Logs are written to file "web.log" in SQ's log directory. | |||
*/ | |||
public class WebServerProcessLogging extends ServerProcessLogging { | |||
private static final String DEPRECATION_LOG_FILE_PREFIX = "deprecation"; | |||
public WebServerProcessLogging() { | |||
super(ProcessId.WEB_SERVER, "%X{" + HTTP_REQUEST_ID_MDC_KEY + "}"); | |||
} | |||
@@ -51,7 +67,33 @@ public class WebServerProcessLogging extends ServerProcessLogging { | |||
} | |||
@Override | |||
protected void extendConfigure() { | |||
// No extension needed | |||
protected void extendConfigure(Props props) { | |||
configureDeprecatedApiLogger(props); | |||
} | |||
private void configureDeprecatedApiLogger(Props props) { | |||
LoggerContext context = helper.getRootContext(); | |||
RootLoggerConfig config = buildRootLoggerConfig(props); | |||
Encoder<ILoggingEvent> encoder = props.valueAsBoolean(LOG_JSON_OUTPUT.getKey(), Boolean.parseBoolean(LOG_JSON_OUTPUT.getDefaultValue())) | |||
? helper.createJsonEncoder(context, config) | |||
: helper.createPatternLayoutEncoder(context, buildDepractedLogPatrern(config)); | |||
FileAppender<ILoggingEvent> appender = helper.newFileAppender(context, props, DEPRECATION_LOG_FILE_PREFIX, encoder); | |||
Logger deprecated = context.getLogger(DEPRECATION_LOGGER_NAME); | |||
deprecated.setAdditive(false); | |||
deprecated.addAppender(appender); | |||
} | |||
private static String buildDepractedLogPatrern(RootLoggerConfig config) { | |||
return PREFIX_LOG_FORMAT | |||
+ (isBlank(config.getNodeNameField()) ? "" : (config.getNodeNameField() + " ")) | |||
+ config.getProcessId().getKey() | |||
+ "[" + config.getThreadIdFieldPattern() + "]" | |||
+ " %X{" + USER_LOGIN_MDC_KEY + "}" | |||
+ " %X{" + ENTRYPOINT_MDC_KEY + "}" | |||
+ SUFFIX_LOG_FORMAT; | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.server.platform.web.logging; | |||
import javax.annotation.Nullable; | |||
import org.slf4j.MDC; | |||
import static org.apache.commons.lang.StringUtils.isBlank; | |||
public class EntrypointMDCStorage implements AutoCloseable { | |||
public static final String ENTRYPOINT_MDC_KEY = "ENTRYPOINT"; | |||
public EntrypointMDCStorage(@Nullable String entrypoint) { | |||
MDC.put(ENTRYPOINT_MDC_KEY, isBlank(entrypoint) ? "-" : entrypoint); | |||
} | |||
@Override | |||
public void close() { | |||
MDC.remove(ENTRYPOINT_MDC_KEY); | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.platform.web.logging; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -59,8 +59,8 @@ public class WebServerProcessLoggingTest { | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
private File logDir; | |||
private Props props = new Props(new Properties()); | |||
private WebServerProcessLogging underTest = new WebServerProcessLogging(); | |||
private final Props props = new Props(new Properties()); | |||
private final WebServerProcessLogging underTest = new WebServerProcessLogging(); | |||
@Before | |||
public void setUp() throws IOException { | |||
@@ -525,6 +525,39 @@ public class WebServerProcessLoggingTest { | |||
assertThat(((LayoutWrappingEncoder) encoder).getLayout()).isInstanceOf(LogbackJsonLayout.class); | |||
} | |||
@Test | |||
public void configure_whenJsonPropFalse_shouldConfigureDeprecatedLoggerWithPatternLayout() { | |||
props.set("sonar.log.jsonOutput", "false"); | |||
LoggerContext context = underTest.configure(props); | |||
Logger logger = context.getLogger("SONAR_DEPRECATION"); | |||
assertThat(logger.isAdditive()).isFalse(); | |||
Appender<ILoggingEvent> appender = logger.getAppender("file_deprecation"); | |||
assertThat(appender).isNotNull() | |||
.isInstanceOf(FileAppender.class); | |||
FileAppender<ILoggingEvent> fileAppender = (FileAppender<ILoggingEvent>) appender; | |||
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"); | |||
} | |||
@Test | |||
public void configure_whenJsonPropTrue_shouldConfigureDeprecatedLoggerWithJsonLayout() { | |||
props.set("sonar.log.jsonOutput", "true"); | |||
LoggerContext context = underTest.configure(props); | |||
Logger logger = context.getLogger("SONAR_DEPRECATION"); | |||
assertThat(logger.isAdditive()).isFalse(); | |||
Appender<ILoggingEvent> appender = logger.getAppender("file_deprecation"); | |||
assertThat(appender).isNotNull() | |||
.isInstanceOf(FileAppender.class); | |||
FileAppender<ILoggingEvent> fileAppender = (FileAppender<ILoggingEvent>) appender; | |||
assertThat(fileAppender.getEncoder()).isInstanceOf(LayoutWrappingEncoder.class); | |||
} | |||
private void verifyRootLogLevel(LoggerContext ctx, Level expected) { | |||
Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); | |||
assertThat(rootLogger.getLevel()).isEqualTo(expected); |
@@ -61,6 +61,8 @@ public class WebServiceEngine implements LocalConnector, Startable { | |||
private static final Logger LOGGER = LoggerFactory.getLogger(WebServiceEngine.class); | |||
public static final String DEPRECATION_LOGGER_NAME = "SONAR_DEPRECATION"; | |||
private final WebService[] webServices; | |||
private final ActionInterceptor[] actionInterceptors; | |||
@@ -0,0 +1,89 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.server.platform.web; | |||
import java.io.IOException; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.ServletRequest; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.slf4j.MDC; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.Mockito.doAnswer; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class EndpointPathFilterTest { | |||
private static final String ENDPOINT_PATH = "/api/system/status"; | |||
private static final String ENTRYPOINT_MDC_KEY = "ENTRYPOINT"; | |||
private final HttpServletRequest servletRequest = mock(HttpServletRequest.class); | |||
private final HttpServletResponse servletResponse = mock(HttpServletResponse.class); | |||
private final FilterChain filterChain = mock(FilterChain.class); | |||
private final EndpointPathFilter endpointPathFilter = new EndpointPathFilter(); | |||
@Before | |||
public void setUp() { | |||
when(servletRequest.getRequestURI()).thenReturn(ENDPOINT_PATH); | |||
} | |||
@Test | |||
public void doFilter_shouldPutEndpointToMDCAndRemoveItAfterChainExecution() throws ServletException, IOException { | |||
doAnswer(invocation -> assertThat(MDC.get("ENTRYPOINT")).isEqualTo(ENDPOINT_PATH)) | |||
.when(filterChain) | |||
.doFilter(servletRequest, servletResponse); | |||
endpointPathFilter.doFilter(servletRequest, servletResponse, filterChain); | |||
assertThat(MDC.get(ENTRYPOINT_MDC_KEY)).isNull(); | |||
} | |||
@Test | |||
public void doFilter_whenChainFails_shouldPutInMDCAndRemoveItAfter() throws IOException, ServletException { | |||
RuntimeException exception = new RuntimeException("Simulating chain failing"); | |||
doAnswer(invocation -> { | |||
assertThat(MDC.get(ENTRYPOINT_MDC_KEY)).isEqualTo(ENDPOINT_PATH); | |||
throw exception; | |||
}) | |||
.when(filterChain) | |||
.doFilter(servletRequest, servletResponse); | |||
assertThatThrownBy(() -> endpointPathFilter.doFilter(servletRequest, servletResponse, filterChain)).isEqualTo(exception); | |||
assertThat(MDC.get(ENTRYPOINT_MDC_KEY)).isNull(); | |||
} | |||
@Test | |||
public void doFilter_whenNotHttpServletRequest_shouldAddEmptyPath() throws ServletException, IOException { | |||
doAnswer(invocation -> assertThat(MDC.get("ENTRYPOINT")).isEqualTo("-")) | |||
.when(filterChain) | |||
.doFilter(servletRequest, servletResponse); | |||
endpointPathFilter.doFilter(mock(ServletRequest.class), servletResponse, filterChain); | |||
assertThat(MDC.get(ENTRYPOINT_MDC_KEY)).isNull(); | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.server.platform.web; | |||
import java.io.IOException; | |||
import javax.servlet.Filter; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.FilterConfig; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.ServletRequest; | |||
import javax.servlet.ServletResponse; | |||
import javax.servlet.http.HttpServletRequest; | |||
import org.sonar.server.platform.web.logging.EntrypointMDCStorage; | |||
public class EndpointPathFilter implements Filter { | |||
@Override | |||
public void init(FilterConfig filterConfig) throws ServletException { | |||
// nothing to do | |||
} | |||
@Override | |||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | |||
String endpointPath = null; | |||
if (request instanceof HttpServletRequest httpRequest) { | |||
endpointPath = httpRequest.getRequestURI(); | |||
} | |||
try (EntrypointMDCStorage entrypointMDCStorage = new EntrypointMDCStorage(endpointPath)) { | |||
chain.doFilter(request, response); | |||
} | |||
} | |||
@Override | |||
public void destroy() { | |||
// nothing to do | |||
} | |||
} |