From 751ca979ce0783663b97ff48f44c1378cd291275 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 11 Dec 2025 22:37:55 -0800 Subject: [PATCH] GUACAMOLE-2181: Migrate to deriving log context from call stack. --- .../AuthenticationProviderFacade.java | 83 +--- .../extension/DirectoryClassLoader.java | 7 +- .../apache/guacamole/extension/Extension.java | 2 +- .../extension/ExtensionClassLoader.java | 20 +- .../extension/LoggerContextProvider.java | 41 ++ .../guacamole/log/ContextAwareLogger.java | 429 ++++++++++++++++++ .../log/ContextAwareLoggerFactory.java | 355 +++++++++++++++ .../apache/guacamole/log/ForkableHashMap.java | 267 ----------- .../log/GuacamoleLogbackServiceProvider.java | 47 +- .../guacamole/log/InheritableMDCAdapter.java | 245 ---------- .../org/apache/guacamole/log/LogModule.java | 122 +---- .../log/ReconfigurableLoggerFactory.java | 46 ++ .../rest/session/SessionResource.java | 34 +- 13 files changed, 937 insertions(+), 761 deletions(-) create mode 100644 guacamole/src/main/java/org/apache/guacamole/extension/LoggerContextProvider.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLogger.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLoggerFactory.java delete mode 100644 guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java delete mode 100644 guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/ReconfigurableLoggerFactory.java diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java index 79d757bdd..d922f851e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java @@ -23,14 +23,12 @@ import java.util.Set; import java.util.UUID; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.log.LogModule; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.UserContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.MDC; /** * Provides a safe wrapper around an AuthenticationProvider subclass, such that @@ -60,12 +58,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { */ private final Set tolerateFailures; - /** - * The logging context that should be provided to SLF4J's MDC for all log - * messages originating from the underlying authentication provider. - */ - private final String logContext; - /** * The identifier to provide for the underlying authentication provider if * the authentication provider could not be loaded. @@ -99,11 +91,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { this.authProvider = ProviderFactory.newInstance("authentication provider", authProviderClass); - if (this.authProvider != null) - this.logContext = LogModule.getMDCContext(authProvider); - else - this.logContext = LogModule.MDC_CONTEXT_ROOT; - } @Override @@ -130,13 +117,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.getResource(); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.getResource(); } @@ -148,13 +129,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return credentials; // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.updateCredentials(credentials); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.updateCredentials(credentials); } @@ -222,7 +197,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { // Delegate to underlying auth provider try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); return authProvider.authenticateUser(credentials); } @@ -248,10 +222,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } - } @Override @@ -267,13 +237,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.updateAuthenticatedUser(authenticatedUser, credentials); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.updateAuthenticatedUser(authenticatedUser, credentials); } @@ -291,7 +255,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { // Delegate to underlying auth provider try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); return authProvider.getUserContext(authenticatedUser); } @@ -317,10 +280,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } - } @Override @@ -337,13 +296,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.updateUserContext(context, authenticatedUser, credentials); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.updateUserContext(context, authenticatedUser, credentials); } @@ -357,13 +310,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return context; // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.decorate(context, authenticatedUser, credentials); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.decorate(context, authenticatedUser, credentials); } @@ -377,27 +324,15 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return context; // Delegate to underlying auth provider - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - return authProvider.redecorate(decorated, context, - authenticatedUser, credentials); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + return authProvider.redecorate(decorated, context, + authenticatedUser, credentials); } @Override public void shutdown() { - try { - MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); - if (authProvider != null) - authProvider.shutdown(); - } - finally { - MDC.remove(LogModule.MDC_CONTEXT_KEY); - } + if (authProvider != null) + authProvider.shutdown(); } } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java b/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java index 83ad9be73..71508c47d 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java @@ -32,7 +32,7 @@ import org.apache.guacamole.GuacamoleException; * A ClassLoader implementation which finds classes within .jar files within a * given directory. */ -public class DirectoryClassLoader extends URLClassLoader { +public class DirectoryClassLoader extends URLClassLoader implements LoggerContextProvider { /** * Returns all .jar files within the given directory as an array of URLs. @@ -105,4 +105,9 @@ public class DirectoryClassLoader extends URLClassLoader { super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader()); } + @Override + public String getLoggerContext() { + return "lib"; + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java index be67a384b..355dc50e4 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java @@ -397,7 +397,7 @@ public class Extension { } // Create isolated classloader for this extension - classLoader = new ExtensionClassLoader(file, temporaryFiles, parent); + classLoader = new ExtensionClassLoader(manifest, file, temporaryFiles, parent); } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java index 6473aa230..4f79678e2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java @@ -46,7 +46,7 @@ import org.slf4j.LoggerFactory; * the given .jar. If classes are defined in both the parent and the extension * .jar, the versions defined within the extension .jar are used. */ -public class ExtensionClassLoader extends URLClassLoader { +public class ExtensionClassLoader extends URLClassLoader implements LoggerContextProvider { /** * Logger for this class. @@ -71,6 +71,11 @@ public class ExtensionClassLoader extends URLClassLoader { */ private final ClassLoader parent; + /** + * The parsed manifest file of the extension that this class loader is for. + */ + private final ExtensionManifest manifest; + /** * Returns the URL that refers to the given file. If the given file refers * to a directory, an exception is thrown. @@ -251,6 +256,9 @@ public class ExtensionClassLoader extends URLClassLoader { * multiple times will not affect previously-returned instances of * ExtensionClassLoader. * + * @param manifest + * The parsed manifest of the extension. + * * @param extension * The extension .jar file from which classes should be loaded. * @@ -267,10 +275,11 @@ public class ExtensionClassLoader extends URLClassLoader { * If the given file is not actually a file, or the contents of the * file cannot be read. */ - public ExtensionClassLoader(File extension, List temporaryFiles, - ClassLoader parent) throws GuacamoleException { + public ExtensionClassLoader(ExtensionManifest manifest, File extension, + List temporaryFiles, ClassLoader parent) throws GuacamoleException { super(getExtensionURLs(extension, temporaryFiles), null); this.parent = parent; + this.manifest = manifest; } @Override @@ -288,4 +297,9 @@ public class ExtensionClassLoader extends URLClassLoader { } + @Override + public String getLoggerContext() { + return "ext:" + manifest.getNamespace(); + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/LoggerContextProvider.java b/guacamole/src/main/java/org/apache/guacamole/extension/LoggerContextProvider.java new file mode 100644 index 000000000..3cf37f4bf --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/extension/LoggerContextProvider.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.extension; + +/** + * Provider of general, human-readable text that describes the context of a + * logger. This is currently used to allow our various custom class loaders to + * provide contextual information, which our custom logger factory can pull from + * the call stack when loggers are created. + */ +public interface LoggerContextProvider { + + /** + * Returns human-readable text describing the context of events logged by + * a logger. The text should describe the general context of the source of + * the entry. + * + * @return + * Human-readable text describing the context of events logged by any + * logger created/maintained by the class implementing this interface. + */ + String getLoggerContext(); + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLogger.java b/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLogger.java new file mode 100644 index 000000000..68ab7c973 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLogger.java @@ -0,0 +1,429 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.log; + +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.slf4j.event.Level; +import org.slf4j.spi.LoggingEventBuilder; + +/** + * {@link Logger} that automatically includes arbitrary context. The additional + * context is included as a {@link Marker}, and all logging functions are + * overridden to include at least this Marker. If a logging function is given a + * Marker by the caller, it is wrapped as a reference of Marker created by the + * ContextAwareLogger's Marker. + */ +public class ContextAwareLogger implements Logger { + + /** + * The wrapped Logger. + */ + private final Logger logger; + + /** + * The arbitrary context that should be included as a Marker in all log + * messages. + */ + private final String context; + + /** + * Creates a new ContextAwareLogger that wraps given Logger, automatically + * including the provided context as a Marker for each logged message. + * + * @param context + * The arbitrary context to include as a Marker with each + * logged message. + * + * @param logger + * The Logger to wrap. + */ + public ContextAwareLogger(String context, Logger logger) { + this.logger = logger; + this.context = context; + } + + /** + * Returns a Marker containing the arbitrary context provided when this + * ContextAwareLogger was created. If Markers are provided to this function, + * they are included as references within the returned Marker. + * + * @param refs + * The Markers to include in the returned Marker as references, if any. + * + * @return + * A Marker containing the context provided when this + * ContextAwareLogger was created, as well as any provided Marker + * references. + */ + private Marker getContextMarker(Marker... refs) { + Marker marker = MarkerFactory.getMarker(context); + Arrays.stream(refs).forEach(marker::add); + return marker; + } + + @Override + public String getName() { + return logger.getName(); + } + + @Override + public LoggingEventBuilder makeLoggingEventBuilder(Level level) { + return logger.makeLoggingEventBuilder(level); + } + + @Override + public LoggingEventBuilder atLevel(Level level) { + return logger.atLevel(level); + } + + @Override + public boolean isEnabledForLevel(Level level) { + return logger.isEnabledForLevel(level); + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logger.trace(getContextMarker(), msg); + } + + @Override + public void trace(String format, Object arg) { + logger.trace(getContextMarker(), format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + logger.trace(getContextMarker(), format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + logger.trace(getContextMarker(), format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logger.trace(getContextMarker(), msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return logger.isTraceEnabled(marker); + } + + @Override + public LoggingEventBuilder atTrace() { + return logger.atTrace(); + } + + @Override + public void trace(Marker marker, String msg) { + logger.trace(getContextMarker(marker), msg); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + logger.trace(getContextMarker(marker), format, arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + logger.trace(getContextMarker(marker), format, arg1, arg2); + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + logger.trace(getContextMarker(marker), format, argArray); + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + logger.trace(getContextMarker(marker), msg, t); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logger.debug(getContextMarker(), msg); + } + + @Override + public void debug(String format, Object arg) { + logger.debug(getContextMarker(), format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + logger.debug(getContextMarker(), format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + logger.debug(getContextMarker(), format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logger.debug(getContextMarker(), msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return logger.isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String msg) { + logger.debug(getContextMarker(marker), msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + logger.debug(getContextMarker(marker), format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + logger.debug(getContextMarker(marker), format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + logger.debug(getContextMarker(marker), format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + logger.debug(getContextMarker(marker), msg, t); + } + + @Override + public LoggingEventBuilder atDebug() { + return logger.atDebug(); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logger.info(getContextMarker(), msg); + } + + @Override + public void info(String format, Object arg) { + logger.info(getContextMarker(), format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + logger.info(getContextMarker(), format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + logger.info(getContextMarker(), format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logger.info(getContextMarker(), msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return logger.isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String msg) { + logger.info(getContextMarker(marker), msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + logger.info(getContextMarker(marker), format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + logger.info(getContextMarker(marker), format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + logger.info(getContextMarker(marker), format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + logger.info(getContextMarker(marker), msg, t); + } + + @Override + public LoggingEventBuilder atInfo() { + return logger.atInfo(); + } + + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logger.warn(getContextMarker(), msg); + } + + @Override + public void warn(String format, Object arg) { + logger.warn(getContextMarker(), format, arg); + } + + @Override + public void warn(String format, Object... arguments) { + logger.warn(getContextMarker(), format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + logger.warn(getContextMarker(), format, arg1, arg2); + } + + @Override + public void warn(String msg, Throwable t) { + logger.warn(getContextMarker(), msg, t); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return logger.isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String msg) { + logger.warn(getContextMarker(marker), msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + logger.warn(getContextMarker(marker), format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + logger.warn(getContextMarker(marker), format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + logger.warn(getContextMarker(marker), format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + logger.warn(getContextMarker(marker), msg, t); + } + + @Override + public LoggingEventBuilder atWarn() { + return logger.atWarn(); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logger.error(getContextMarker(), msg); + } + + @Override + public void error(String format, Object arg) { + logger.error(getContextMarker(), format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + logger.error(getContextMarker(), format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + logger.error(getContextMarker(), format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(getContextMarker(), msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return logger.isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String msg) { + logger.error(getContextMarker(marker), msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + logger.error(getContextMarker(marker), format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + logger.error(getContextMarker(marker), format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + logger.error(getContextMarker(marker), format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + logger.error(getContextMarker(marker), msg, t); + } + + @Override + public LoggingEventBuilder atError() { + return logger.atError(); + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLoggerFactory.java b/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLoggerFactory.java new file mode 100644 index 000000000..e751e71eb --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/log/ContextAwareLoggerFactory.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.log; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import ch.qos.logback.core.util.StatusPrinter; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.guacamole.extension.LoggerContextProvider; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.MDC; +import org.slf4j.Marker; +import org.slf4j.spi.MDCAdapter; + +/** + * {@link ILoggerFactory} implementation that automatically includes additional + * {@link Marker} context based on the origin of each log message. The origin + * of each message is determined by investigating the caller classes, with + * the actual context provided by the associated classloader if it implements + * {@link LoggerContextProvider}. + */ +public class ContextAwareLoggerFactory implements ReconfigurableLoggerFactory { + + /** + * The name of the LoggerContext maintained by this logger factory. + */ + private static final String LOGGER_CONTEXT_NAME = "guacamole"; + + /** + * The default text that should be displayed if no other context can be + * derived from any caller. + */ + private static final String DEFAULT_CONTEXT = "app"; + + /** + * Regular expression that matches one or more newlines, regardless of style + * (Windows, Linux, etc.), preserving only the first such newline. This + * expression is intended to reliably match newlines that may be interpreted + * by the shell/viewer, regardless of the platform producing the log + * message. + *

+ * This specific regular expression is intended for use with content that is + * being included inline (concatenated with an existing line of output). + */ + private static final String INLINE_NEWLINE_REGEX = + + // Locate the right edge of the first newline sequence, whether + // that is CR, LF, CRLF, or LFCR + "(?<=" + + // Do not match newline sequences that are not the first + // newline in the sequence + + "(? + * NOTE: This method is only supported by Java 9 and late and will throw a + * ClassNotFoundException otherwise.. + * + * @return + * A List of all classes currently on the call stack, in order of + * decreasing depth. + * + * @throws UnsupportedOperationException + * If support for StackWalker is not present (Java 8). + */ + @SuppressWarnings("unchecked") + private List> getCallersFromStackWalker() + throws UnsupportedOperationException { + + // + // NOTE: The following is the reflection equivalent of: + // + // return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk((stack) -> + // stack.map((frame) -> frame.getDeclaringClass()).collect(Collectors.toList()) + // ); + // + + try { + + // Retrieve StackWalker definition and related classes + Class stackWalkerClass = Class.forName("java.lang.StackWalker"); + Class stackFrameClass = Class.forName("java.lang.StackWalker$StackFrame"); + Class optionEnum = (Class) Class.forName("java.lang.StackWalker$Option"); + + // Retrieve references to functions we will be needing + Method stackWalkerGetInstanceMethod = stackWalkerClass.getMethod("getInstance", optionEnum); + Method walkMethod = stackWalkerClass.getMethod("walk", Function.class); + Method getDeclaringClassMethod = stackFrameClass.getMethod("getDeclaringClass"); + + // Equivalent to: + // StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + Object stackWalker = stackWalkerGetInstanceMethod.invoke(null, + Enum.valueOf(optionEnum, "RETAIN_CLASS_REFERENCE")); + + // Equivalent to: + // (stack) -> stack.map((frame) -> frame.getDeclaringClass()).collect(Collectors.toList()) + Function, List>> walker = + (stack) -> stack.map((frame) -> { + try { + return (Class) getDeclaringClassMethod.invoke(frame); + } + catch (ReflectiveOperationException e) { + throw new UnsupportedOperationException("getDeclaringClass() " + + "could not be invoked for stack frame ", e); + } + }).collect(Collectors.toList()); + + // Equivalent to: stackWalker.walk(...) + return (List>) walkMethod.invoke(stackWalker, walker); + + } + catch (ReflectiveOperationException e) { + throw new UnsupportedOperationException("StackWalker is not " + + "supported by this version of Java", e); + } + + } + + /** + * Uses the deprecated {@link SecurityManager} to retrieve all classes + * currently on the call stack for the current function, in order of + * decreasing depth. The current function (the deepest function in terms of + * call depth) is the first item in the returned list. + *

+ * NOTE: This is the only method that is supported by Java 8 that includes + * direct Class references. + * + * @return + * A List of all classes currently on the call stack, in order of + * decreasing depth. + * + * @throws UnsupportedOperationException + * If support for SecurityManager is not present (expected for future + * Java versions). + */ + @SuppressWarnings("removal") + private List> getCallersFromSecurityManager() { + try { + + // Atempt to locate SecurityManager class and grab its + // getClassContext() method (which returns a stack trace) + Class securityManagerClass = Class.forName("java.lang.SecurityManager"); + Object securityManager = securityManagerClass.getDeclaredConstructor().newInstance(); + Method getClassContextMethod = securityManagerClass.getDeclaredMethod("getClassContext"); + + // getClassContext() is protected + getClassContextMethod.setAccessible(true); + + return Arrays.asList((Class[]) getClassContextMethod.invoke(securityManager)); + + } + catch (ReflectiveOperationException e) { + throw new UnsupportedOperationException("SecurityManager is not " + + "supported by this version of Java", e); + } + } + + /** + * Returns all classes currently on the call stack for the current function, + * in order of decreasing depth. The current function (the deepest function + * in terms of call depth) is the first item in the returned list. + * + * @return + * A List of all classes currently on the call stack, in order of + * decreasing depth. + * + * @throws UnsupportedOperationException + * If no supported mechanism exists for retrieving the call stack. + */ + private List> getCallers() throws UnsupportedOperationException { + try { + return getCallersFromStackWalker(); + } + catch (UnsupportedOperationException e) { + return getCallersFromSecurityManager(); + } + } + + /** + * Derives the context of the message currently being logged based on the + * content of the call stack. + * + * @return + * The context of the message currently being logged. + */ + private String getCallerLogContext() { + + // Derive context from the most recent caller that explicitly provides + // their own logging context, if any + for (Class caller : getCallers()) { + + ClassLoader classloader = caller.getClassLoader(); + if (classloader instanceof LoggerContextProvider) + return ((LoggerContextProvider) classloader).getLoggerContext(); + + } + + // Use default context if no callers provide their own + return DEFAULT_CONTEXT; + + } + + @Override + public Logger getLogger(String name) { + return new ContextAwareLogger(getCallerLogContext(), context.getLogger(name)); + } + + @Override + public void reconfigure(InputStream logbackConfiguration) throws JoranException { + + context.reset(); + + // Include convenience properties for building off Guacamole's + // built-in configuration with a custom logback.xml + context.putProperty("guac_timestamp_pattern", LOG_PATTERN_FRAGMENT_TIMESTAMP); + context.putProperty("guac_message_pattern", LOG_PATTERN_FRAGMENT_MESSAGE_BODY); + context.putProperty("guac_pattern", LOG_PATTERN_DEFAULT); + context.putProperty("guac_verbose_pattern", LOG_PATTERN_VERBOSE); + + // Initialize logback + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + configurator.doConfigure(logbackConfiguration); + + // Dump any errors that occur during logback init + StatusPrinter.printInCaseOfErrorsOrWarnings(context); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java b/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java deleted file mode 100644 index 2ef5e1c47..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.guacamole.log; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -/** - * HashMap implementation that uses copy-on-write to allow efficient cloning. - * When an instance of ForkableHashMap has been copied using {@link #fork()}, - * the parent and child both continue to refer to the same copy of their - * underlying data. If the parent (or child) is modified, only then does the - * parent (or child) create an independent copy of that underlying data. - *

- * For simplicity, this implementation DOES NOT permit modification via the - * collections/sets returned by {@link #values()}, {@link #keySet()}, or - * {@link #entrySet()}. - *

- * As with the standard HashMap, this implementation IS NOT threadsafe. - * - * @param - * The type of key used to store/retrieve values within this map. - * - * @param - * The type of values stored by this map. - */ -public class ForkableHashMap implements Map { - - /** - * The underlying content of this ForkableHashMap. This content is shared - * with at least one other ForkableHashMap if {@link #shared} is true, and - * is an independent copy that may safely be modified if {@link #shared} is - * false. - */ - private Map content; - - /** - * Whether the Map referenced by {@link #content} is shared with at least - * one other ForkableHashMap. - */ - private boolean shared; - - /** - * Creates a new, empty ForkableHashMap. - */ - public ForkableHashMap() { - this.content = new HashMap<>(); - this.shared = false; - } - - /** - * Creates a new ForkableHashMap that contains an independent copy of the - * entries in the provided map. - *

- * This is not a deep copy; only the references to keys and values are - * copied. - * - * @param map - * The map to copy. - */ - public ForkableHashMap(Map map) { - this.content = new HashMap<>(map); - this.shared = false; - } - - /** - * Creates a new ForkableHashMap that shares the content of the given - * ForkableHashMap. It is the responsibility of the caller to ensure that - * the given ForkableHashMap has its content marked as shared. - * - * @param parent - * The ForkableHashMap to lazily copy. - */ - private ForkableHashMap(ForkableHashMap parent) { - this.content = parent.content; - this.shared = true; - } - - /** - * Creates a new ForkableHashMap that shares the content of this map. No - * actual copy operation will be performed until this map or the new map - * are modified. - * - * @return - * A new ForkableHashMap that lazily shares the content of this map. - */ - public ForkableHashMap fork() { - shared = true; - return new ForkableHashMap<>(this); - } - - /** - * Returns the map providing content for this ForkableHashMap while - * ensuring that map is safe to modify. If the map is shared with another - * ForkableHashMap, the map is first replaced with an independent copy. - * - * @return - * The map providing content for this ForkableHashMap, which shall be - * safe to modify after this function returns. - */ - private Map writableContent() { - - // Transform into independent copy if other instances of ForkableHashMap - // share the same underlying data - if (shared) { - content = new HashMap<>(content); - shared = false; - } - - return content; - - } - - @Override - public int size() { - return content.size(); - } - - @Override - public boolean isEmpty() { - return content.isEmpty(); - } - - @Override - public boolean containsKey(Object o) { - return content.containsKey(o); - } - - @Override - public boolean containsValue(Object o) { - return content.containsValue(o); - } - - @Override - public V get(Object o) { - return content.get(o); - } - - @Override - public V put(K k, V v) { - return writableContent().put(k, v); - } - - @Override - public V remove(Object o) { - return writableContent().remove(o); - } - - @Override - public void putAll(Map map) { - writableContent().putAll(map); - } - - @Override - public void clear() { - writableContent().clear(); - } - - @Override - public Set keySet() { - return Collections.unmodifiableSet(content.keySet()); - } - - @Override - public Collection values() { - return Collections.unmodifiableCollection(content.values()); - } - - @Override - public Set> entrySet() { - return Collections.unmodifiableSet(content.entrySet()); - } - - @Override - public boolean equals(Object o) { - return content.equals(o); - } - - @Override - public int hashCode() { - return content.hashCode(); - } - - @Override - public V getOrDefault(Object key, V defaultValue) { - return content.getOrDefault(key, defaultValue); - } - - @Override - public void forEach(BiConsumer action) { - content.forEach(action); - } - - @Override - public void replaceAll(BiFunction function) { - writableContent().replaceAll(function); - } - - @Override - public V putIfAbsent(K key, V value) { - return writableContent().putIfAbsent(key, value); - } - - @Override - public boolean remove(Object key, Object value) { - return writableContent().remove(key, value); - } - - @Override - public boolean replace(K key, V oldValue, V newValue) { - return writableContent().replace(key, oldValue, newValue); - } - - @Override - public V replace(K key, V value) { - return writableContent().replace(key, value); - } - - @Override - public V computeIfAbsent(K key, Function mappingFunction) { - return content.computeIfAbsent(key, mappingFunction); - } - - @Override - public V computeIfPresent(K key, BiFunction remappingFunction) { - return content.computeIfPresent(key, remappingFunction); - } - - @Override - public V compute(K key, BiFunction remappingFunction) { - return content.compute(key, remappingFunction); - } - - @Override - public V merge(K key, V value, BiFunction remappingFunction) { - return writableContent().merge(key, value, remappingFunction); - } - - @Override - public String toString() { - return content.toString(); - } - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java b/guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java index c1414c554..4ae3e1a97 100644 --- a/guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java +++ b/guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java @@ -19,10 +19,7 @@ package org.apache.guacamole.log; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.util.ContextInitializer; -import ch.qos.logback.core.joran.spi.JoranException; -import org.slf4j.ILoggerFactory; +import ch.qos.logback.classic.util.LogbackMDCAdapter; import org.slf4j.IMarkerFactory; import org.slf4j.helpers.BasicMarkerFactory; import org.slf4j.spi.MDCAdapter; @@ -30,7 +27,7 @@ import org.slf4j.spi.SLF4JServiceProvider; /** * SLF4JServiceProvider that overrides the default behavior of Logback's - * service provider, forcing the use of a custom MDCAdapter. + * service provider, forcing the use of a custom, context-aware logger factory. */ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider { @@ -42,22 +39,18 @@ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider { private static final String SLF4J_API_MAX_VERSION = "2.0.99"; /** - * The name of the LoggerContext maintained by this service provider. + * The singleton instance of the LoggerFactory maintained by this service + * provider. The custom implementation provided here uses the call stack + * and our various custom class loaders to determine the context of the + * loggers that will be created. */ - private static final String LOGGER_CONTEXT_NAME = "guacamole"; - - /** - * The singleton instance of the LoggerContext maintained by this service - * provider. The LoggerContext is actually the LoggerFactory. - */ - private LoggerContext context; + private ContextAwareLoggerFactory loggerFactory; /** * The singleton instance of the MDCAdapter implementation that should be - * used by Logback and SLF4J. The custom implementation provided here - * allows the context map to be inherited by child threads. + * used by Logback and SLF4J. */ - private final MDCAdapter mdcAdapter = new InheritableMDCAdapter(); + private final MDCAdapter mdcAdapter = new LogbackMDCAdapter(); /** * The singleton MarkerFactory instance maintained by this service @@ -67,23 +60,7 @@ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider { @Override public void initialize() { - - context = new LoggerContext(); - context.setName(LOGGER_CONTEXT_NAME); - context.setMDCAdapter(mdcAdapter); - - // Perform basic init (prior to loading any Guacamole-specific - // configuration, which is loaded by the LogModule) - try { - new ContextInitializer(context).autoConfig(); - } - catch (JoranException | RuntimeException | Error e) { - System.err.println("Logging system could not be initialized: " + e.getMessage()); - e.printStackTrace(System.err); - } - - context.start(); - + loggerFactory = new ContextAwareLoggerFactory(mdcAdapter); } @Override @@ -92,8 +69,8 @@ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider { } @Override - public ILoggerFactory getLoggerFactory() { - return context; + public ContextAwareLoggerFactory getLoggerFactory() { + return loggerFactory; } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java b/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java deleted file mode 100644 index e75586b47..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.guacamole.log; - -import java.util.Deque; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import org.slf4j.spi.MDCAdapter; - -/** - * MDCAdapter implementation that allows individual string context values to be - * inherited by child threads. Deque context values (as manipulated with - * {@link #pushByKey(java.lang.String, java.lang.String)}, etc.) are not - * inherited. - */ -public class InheritableMDCAdapter implements MDCAdapter { - - // - // IMPORTANT: Thread-local values MUST be removed at web application - // shutdown or a memory leak may result. To avoid this, care must be - // taken in this implementation: - // - // * Ensure that a balanced remove() for every put() is sufficient to - // clear and remove the thread-local itself. - // - // * Calls to ThreadLocal.get() implicitly assign a value to the - // thread-local if no value was present before. This value needs to be - // removed to avoid leaking references at web application shutdown. - // - - /** - * MDC context map that is automatically inherited by child threads. - *

- * IMPORTANT: As a thread-local value, it is critical that this value be - * removed at web application shutdown or a memory leak may result. Here, - * we maintain a reference within the thread-local only as long as at least - * one value is stored. Once the map is empty, the thread-local is removed. - */ - private final InheritableThreadLocal> context = - new InheritableThreadLocal>() { - - @Override - protected ForkableHashMap initialValue() { - return new ForkableHashMap<>(); - } - - @Override - protected ForkableHashMap childValue(ForkableHashMap parentValue) { - // This function is invoked by the JVM for every thread created, - // so simply creating a new copy is expensive. We instead create a - // copy-on-write reference. - return parentValue.fork(); - } - - }; - - /** - * MDC deque map that is NOT automatically inherited by child threads. We - * choose not to inherit the deque map as that would result in pushed values - * possibly never getting popped. - *

- * NOTE: The Deques have no relation to the single values maintained using - * {@link #put(java.lang.String, java.lang.String)} and retrieved using - * {@link #get(java.lang.String)} which are intentionally stored - * separately in {@link #context}. This matches the behavior of the Logback - * implementation of MDC. - *

- * IMPORTANT: As a thread-local value, it is critical that this value be - * removed at web application shutdown or a memory leak may result. Here, - * we maintain a reference within the thread-local only as long as at least - * one non-empty deque is stored. Once no non-empty deques are present - * empty, the thread-local is removed. - */ - private final ThreadLocal>> contextDeques = - new ThreadLocal>>() { - - @Override - protected Map> initialValue() { - return new HashMap<>(); - } - - }; - - @Override - public void put(String key, String value) { - context.get().put(key, value); - } - - @Override - public String get(String key) { - - ForkableHashMap contextMap = context.get(); - - // Do not maintain a thread-local reference to an empty map - if (contextMap.isEmpty()) { - context.remove(); - return null; - } - - return contextMap.get(key); - - } - - @Override - public void remove(String key) { - - ForkableHashMap contextMap = context.get(); - contextMap.remove(key); - - // Do not maintain a thread-local reference to an empty map - if (contextMap.isEmpty()) - context.remove(); - - } - - @Override - public void clear() { - // Do not maintain a thread-local reference to an empty map - context.remove(); - } - - @Override - public Map getCopyOfContextMap() { - - ForkableHashMap contextMap = context.get(); - - // Do not maintain a thread-local reference to an empty map - instead - // repurpose the new empty map as the "copy" - if (contextMap.isEmpty()) { - context.remove(); - return contextMap; - } - - // Universally copying the context map would be expensive, as the - // getCopyOfContextMap() function is invoked by Logback for every log - // message produced. Instead, we create a copy-on-write reference to - // the context map. - return contextMap.fork(); - - } - - @Override - public void setContextMap(Map contextMap) { - - // Do not maintain a thread-local reference to an empty map - if (contextMap.isEmpty()) - context.remove(); - - else - context.set(new ForkableHashMap<>(contextMap)); - - } - - /** - * Returns the Deque of pushed values associated with the given key, as - * required for {@link #pushByKey(java.lang.String, java.lang.String)} and - * {@link #popByKey(java.lang.String)}. If no Deque yet exists for the - * given key, it is first created. - * - * @param key - * The key to retrieve the Deque for. - * - * @return - * The Deque of pushed values for the given key. - */ - private Deque getDeque(String key) { - - Deque values = contextDeques.get().get(key); - if (values == null) { - values = new LinkedList<>(); - contextDeques.get().put(key, values); - } - - return values; - - } - - @Override - public void pushByKey(String key, String value) { - getDeque(key).push(value); - } - - @Override - public String popByKey(String key) { - - Deque deque = getDeque(key); - String value = deque.pop(); - - // Do not maintain any references to empty deques (so that it's easy to - // detect when there are no items in any deques) - if (deque.isEmpty()) - clearDequeByKey(key); - - return value; - - } - - @Override - public Deque getCopyOfDequeByKey(String key) { - - Deque deque = getDeque(key); - - // Do not maintain a thread-local reference to an empty deque - instead - // repurpose the new empty deque as the "copy" - if (deque.isEmpty()) { - clearDequeByKey(key); // ... and ensure the deque map is removed if empty, too - return deque; - } - - return new LinkedList<>(deque); - - } - - @Override - public void clearDequeByKey(String key) { - - // Clear by entirely removing - Map> dequeMap = contextDeques.get(); - dequeMap.remove(key); - - // Do not maintain a thread-local reference to an empty map - if (dequeMap.isEmpty()) - contextDeques.remove(); - - } - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java b/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java index e3b7d8b23..ef07aeb84 100644 --- a/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java @@ -19,10 +19,7 @@ package org.apache.guacamole.log; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; -import ch.qos.logback.core.util.StatusPrinter; import com.google.inject.AbstractModule; import java.io.File; import java.io.FileInputStream; @@ -31,8 +28,8 @@ import java.io.IOException; import java.io.InputStream; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; -import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.properties.EnumGuacamoleProperty; +import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,95 +48,6 @@ public class LogModule extends AbstractModule { */ private final Environment environment; - /** - * The name of the MDC key that should be used to store Guacamole-specific - * context information. If an extension logs a message, this information - * will include the identifier of the relevant authentication provider. - */ - public static final String MDC_CONTEXT_KEY = "context"; - - /** - * The name that should be considered the top-level context when populating - * the MDC entry pointed to by {@link #MDC_CONTEXT_KEY}. - */ - public static final String MDC_CONTEXT_ROOT = "guacamole"; - - /** - * Returns the context information that should be stored in MDC under - * {@link #MDC_CONTEXT_KEY} for any message logged by the given - * authentication provider. - * - * @param authProvider - * The authentication provider to provide context for. - * - * @return - * The context information that should be stored in MDC for any message - * logged by the given authentication provider. - */ - public static String getMDCContext(AuthenticationProvider authProvider) { - return LogModule.MDC_CONTEXT_ROOT + "/" + authProvider.getIdentifier(); - } - - /** - * Regular expression that matches one or more newlines, regardless of style - * (Windows, Linux, etc.), preserving only the first such newline. This - * expression is intended to reliably match newlines that may be interpreted - * by the shell/viewer, regardless of the platform producing the log - * message. - */ - private static final String NEWLINE_REGEX = - - // Locate the right edge of the first newline sequence, whether - // that is CR, LF, CRLF, or LFCR - "(?<=" - - // Do not match newline sequences that are not the first - // newline in the sequence - + "(?