GUACAMOLE-2181: Migrate to deriving log context from call stack.

This commit is contained in:
Michael Jumper committed 2025-12-12 10:42:28 -08:00
1 parent cf3902ea8c
commit 751ca979ce
13 files changed
+937 -761

No files matched your search

@@ -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<String> 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();
}
}
@@ -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";
}
}
@@ -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);
}
@@ -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<File> temporaryFiles,
ClassLoader parent) throws GuacamoleException {
public ExtensionClassLoader(ExtensionManifest manifest, File extension,
List<File> 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();
}
}
@@ -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();
}
@@ -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();
}
}
@@ -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.
* <p>
* 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
+ "(?<![\\n\\r])"
// Match either CR, LF, CRLF, or LFCR, ensuring that CRLF
// does not get matched as just CR (same for LFCR vs. LF)
+ "("
+ "\\n(?!\\r)" // LF (with no following CR)
+ "|\\r(?!\\n)" // CR (with no following LF)
+ "|\\n\\r" // LFCR
+ "|\\r\\n" // CRLF
+ ")"
+ ")"
// Ignore (and strip) and remaining newline characters
+ "[\\n\\r]*";
/**
* Identical in purpose and behavior to {@link #INLINE_NEWLINE_REGEX}, but
* intended for output that stands on its own as a block of text. This
* variation preserves formatting of a block by matching the beginning of
* all lines (regardless of whether preceded by a newline) and not matching
* the final newline.
*/
private static final String BLOCK_NEWLINE_REGEX = "(^|" + INLINE_NEWLINE_REGEX + ")(?=[^\\r\\n])";
/**
* Logback encoder pattern fragment that produces a local timestamp (with
* timezone information) in roughly ISO 8601 format.
*/
private static final String LOG_PATTERN_FRAGMENT_TIMESTAMP = "%d{yyyy-MM-dd HH:mm:ss.SSS xxx}";
/**
* The text that should be prepended to log messages when they span multiple
* lines.
*/
private static final String CONTINUATION_PREFIX = "+ ";
/**
* Logback encoder pattern fragment that produces the logged message. This
* automatically prepends any newlines within the message with a "+ " prefix
* to clearly represent when a message has been split across multiple lines.
*/
private static final String LOG_PATTERN_FRAGMENT_MESSAGE_BODY = "%replace(%msg){'" + INLINE_NEWLINE_REGEX + "','" + CONTINUATION_PREFIX + "'}";
/**
* Logback encoder pattern that should be used for messages when the highest
* level of verbosity is not needed.
*/
private static final String LOG_PATTERN_DEFAULT = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " guacamole [%marker] %level: "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n"
+ "%nopex";
/**
* Logback encoder pattern that should be used for messages with the highest
* level of verbosity.
*/
private static final String LOG_PATTERN_VERBOSE = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " guacamole [%marker, thread:%thread] %level: %logger{36} - "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n"
+ "%replace(%xThrowable){'" + BLOCK_NEWLINE_REGEX + "','" + CONTINUATION_PREFIX + "'}";
/**
* The top-level Logback object, the LoggerContext, which is also Logback's
* LoggerFactory implementation.
*/
private final LoggerContext context;
/**
* Creates a new ContextAwareLoggerFactory that uses the given MDCAdapter
* for the {@link MDC} implementation.
*
* @param mdcAdapter
* The MDCAdapter that should be used to provide the MDC
* implementation.
*/
public ContextAwareLoggerFactory(MDCAdapter mdcAdapter) {
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();
}
/**
* Uses the {@link StackWalker} class provided by Java9+ 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.
* <p>
* 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<Class<?>> 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<Enum> optionEnum = (Class<Enum>) 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<Stream<?>, List<Class<?>>> 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<Class<?>>) 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.
* <p>
* 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<Class<?>> 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<Class<?>> 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);
}
}
@@ -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.
* <p>
* For simplicity, this implementation DOES NOT permit modification via the
* collections/sets returned by {@link #values()}, {@link #keySet()}, or
* {@link #entrySet()}.
* <p>
* As with the standard HashMap, this implementation IS NOT threadsafe.
*
* @param <K>
* The type of key used to store/retrieve values within this map.
*
* @param <V>
* The type of values stored by this map.
*/
public class ForkableHashMap<K, V> implements Map<K, V> {
/**
* 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<K, V> 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.
* <p>
* This is not a deep copy; only the references to keys and values are
* copied.
*
* @param map
* The map to copy.
*/
public ForkableHashMap(Map<K, V> 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<K, V> 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<K, V> 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<K, V> 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<? extends K, ? extends V> map) {
writableContent().putAll(map);
}
@Override
public void clear() {
writableContent().clear();
}
@Override
public Set<K> keySet() {
return Collections.unmodifiableSet(content.keySet());
}
@Override
public Collection<V> values() {
return Collections.unmodifiableCollection(content.values());
}
@Override
public Set<Entry<K, V>> 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<? super K, ? super V> action) {
content.forEach(action);
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> 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<? super K, ? extends V> mappingFunction) {
return content.computeIfAbsent(key, mappingFunction);
}
@Override
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return content.computeIfPresent(key, remappingFunction);
}
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return content.compute(key, remappingFunction);
}
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
return writableContent().merge(key, value, remappingFunction);
}
@Override
public String toString() {
return content.toString();
}
}
@@ -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
@@ -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.
* <p>
* 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<ForkableHashMap<String, String>> context =
new InheritableThreadLocal<ForkableHashMap<String, String>>() {
@Override
protected ForkableHashMap<String, String> initialValue() {
return new ForkableHashMap<>();
}
@Override
protected ForkableHashMap<String, String> childValue(ForkableHashMap<String, String> 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.
* <p>
* 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.
* <p>
* 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<Map<String, Deque<String>>> contextDeques =
new ThreadLocal<Map<String, Deque<String>>>() {
@Override
protected Map<String, Deque<String>> initialValue() {
return new HashMap<>();
}
};
@Override
public void put(String key, String value) {
context.get().put(key, value);
}
@Override
public String get(String key) {
ForkableHashMap<String, String> 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<String, String> 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<String, String> getCopyOfContextMap() {
ForkableHashMap<String, String> 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<String, String> 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<String> getDeque(String key) {
Deque<String> 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<String> 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<String> getCopyOfDequeByKey(String key) {
Deque<String> 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<String, Deque<String>> dequeMap = contextDeques.get();
dequeMap.remove(key);
// Do not maintain a thread-local reference to an empty map
if (dequeMap.isEmpty())
contextDeques.remove();
}
}
@@ -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
+ "(?<![\\n\\r])"
// Match either CR, LF, CRLF, or LFCR, ensuring that CRLF
// does not get matched as just CR (same for LFCR vs. LF)
+ "("
+ "\\n(?!\\r)" // LF (with no following CR)
+ "|\\r(?!\\n)" // CR (with no following LF)
+ "|\\n\\r" // LFCR
+ "|\\r\\n" // CRLF
+ ")"
+ ")"
// Ignore (and strip) and remaining newline characters
+ "[\\n\\r]*";
/**
* Logback encoder pattern fragment that produces a local timestamp (with
* timezone information) in roughly ISO 8601 format.
*/
private static final String LOG_PATTERN_FRAGMENT_TIMESTAMP = "%d{yyyy-MM-dd HH:mm:ss.SSS xxx}";
/**
* Logback encoder pattern fragment that produces the logged message. This
* automatically prepends any newlines within the message with a "+ " prefix
* to clearly represent when a message has been split across multiple lines.
*/
private static final String LOG_PATTERN_FRAGMENT_MESSAGE_BODY = "%replace(%msg){'" + NEWLINE_REGEX + "','+ '}";
/**
* Logback encoder pattern that should be used for messages when the highest
* level of verbosity is not needed.
*/
private static final String LOG_PATTERN_DEFAULT = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " [%X{" + MDC_CONTEXT_KEY + ":-" + MDC_CONTEXT_ROOT + "}] %level: "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n%nopex";
/**
* Logback encoder pattern that should be used for messages with the highest
* level of verbosity.
*/
private static final String LOG_PATTERN_VERBOSE = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " [%X{" + MDC_CONTEXT_KEY + ":-" + MDC_CONTEXT_ROOT + "}] %logger{48} %level: "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n%ex";
/**
* Property that specifies the highest level of verbosity that Guacamole
* should use for the messages in its logs.
@@ -212,23 +120,19 @@ public class LogModule extends AbstractModule {
try (InputStream logbackConfiguration = getLogbackConfiguration()) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
// Bail out if the system is somehow preventing Guacamole from
// using its own logging
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (!(loggerFactory instanceof ReconfigurableLoggerFactory)) {
logger.warn("Guacamole depends on its own SLF4J service "
+ "provider to configure logging, but your system has "
+ "forced a different service provider to take "
+ "priority. Guacamole's own logging configuration "
+ "may not have any effect, and logging context may "
+ "be missing.");
}
// 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);
((ReconfigurableLoggerFactory) loggerFactory).reconfigure(logbackConfiguration);
}
catch (JoranException e) {
@@ -0,0 +1,46 @@
/*
* 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.core.joran.spi.JoranException;
import java.io.InputStream;
import org.slf4j.ILoggerFactory;
/**
* An {@link ILoggerFactory} that can be reconfigured as needed.
*/
public interface ReconfigurableLoggerFactory extends ILoggerFactory {
/**
* Reconfigures this {@link ILoggerFactory} using the given InputStream for
* a Logback XML configuration file. It is the responsibility of the caller
* to close the provided InputStream.
*
* @param logbackConfiguration
* An InputStream that provides the desired Logback configuration in
* XML format.
*
* @throws JoranException
* If the configuration is invalid, or an error occurs that prevents
* the configuration from being read/applied.
*/
void reconfigure(InputStream logbackConfiguration) throws JoranException;
}
@@ -32,12 +32,10 @@ import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSession;
import org.apache.guacamole.log.LogModule;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.rest.auth.AuthenticationService;
import org.apache.guacamole.rest.tunnel.TunnelCollectionResource;
import org.apache.guacamole.rest.tunnel.TunnelCollectionResourceFactory;
import org.slf4j.MDC;
/**
* A REST resource which exposes all data associated with a Guacamole user's
@@ -118,16 +116,8 @@ public class SessionResource {
// Pull UserContext defined by the given auth provider identifier
UserContext userContext = session.getUserContext(authProviderIdentifier);
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
try {
// Return a resource exposing the retrieved UserContext
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext);
}
finally {
MDC.remove(LogModule.MDC_CONTEXT_KEY);
}
// Return a resource exposing the retrieved UserContext
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext);
}
@@ -155,21 +145,13 @@ public class SessionResource {
// Pull UserContext defined by the given auth provider identifier
UserContext userContext = session.getUserContext(authProviderIdentifier);
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
try {
// Pull resource from user context
Object resource = userContext.getResource();
if (resource != null)
return resource;
// Pull resource from user context
Object resource = userContext.getResource();
if (resource != null)
return resource;
// UserContext-specific resource could not be found
throw new GuacamoleResourceNotFoundException("No such resource.");
}
finally {
MDC.remove(LogModule.MDC_CONTEXT_KEY);
}
// UserContext-specific resource could not be found
throw new GuacamoleResourceNotFoundException("No such resource.");
}