From be53376f280cd646d096c67a6feb930b2bf56f01 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 8 Dec 2025 18:04:12 -0800 Subject: [PATCH] GUACAMOLE-2181: Include extension information and line continuations in log messages. --- guacamole/.ratignore | 1 + .../AuthenticationProviderFacade.java | 89 +++++- .../apache/guacamole/log/ForkableHashMap.java | 267 ++++++++++++++++++ .../log/GuacamoleLogbackServiceProvider.java | 109 +++++++ .../guacamole/log/InheritableMDCAdapter.java | 155 ++++++++++ .../org/apache/guacamole/log/LogLevel.java | 43 ++- .../org/apache/guacamole/log/LogModule.java | 97 +++++++ .../rest/session/SessionResource.java | 34 ++- .../org.slf4j.spi.SLF4JServiceProvider | 1 + 9 files changed, 768 insertions(+), 28 deletions(-) create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java create mode 100644 guacamole/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider diff --git a/guacamole/.ratignore b/guacamole/.ratignore index 548fcb76c..6ced18f45 100644 --- a/guacamole/.ratignore +++ b/guacamole/.ratignore @@ -1 +1,2 @@ src/main/frontend/dist/**/* +src/main/resources/META-INF/services/* 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 b89b2ada6..e2122fdef 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java @@ -23,12 +23,14 @@ 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 @@ -40,7 +42,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { /** * Logger for this class. */ - private Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class); + private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class); /** * The underlying authentication provider, or null if the authentication @@ -58,6 +60,12 @@ 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. @@ -86,9 +94,16 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { public AuthenticationProviderFacade( Class authProviderClass, Set tolerateFailures) { + this.tolerateFailures = tolerateFailures; this.authProvider = ProviderFactory.newInstance("authentication provider", authProviderClass); + + if (this.authProvider != null) + this.logContext = LogModule.getMDCContext(authProvider); + else + this.logContext = LogModule.MDC_CONTEXT_ROOT; + } @Override @@ -115,7 +130,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - return authProvider.getResource(); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.getResource(); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } @@ -127,7 +148,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return credentials; // Delegate to underlying auth provider - return authProvider.updateCredentials(credentials); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.updateCredentials(credentials); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } @@ -199,6 +226,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { // Delegate to underlying auth provider try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); return authProvider.authenticateUser(credentials); } @@ -224,6 +252,10 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } + } @Override @@ -239,7 +271,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - return authProvider.updateAuthenticatedUser(authenticatedUser, credentials); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.updateAuthenticatedUser(authenticatedUser, credentials); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } @@ -257,6 +295,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { // Delegate to underlying auth provider try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); return authProvider.getUserContext(authenticatedUser); } @@ -282,6 +321,10 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } + } @Override @@ -298,8 +341,14 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { } // Delegate to underlying auth provider - return authProvider.updateUserContext(context, authenticatedUser, credentials); - + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.updateUserContext(context, authenticatedUser, credentials); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } + } @Override @@ -312,7 +361,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return context; // Delegate to underlying auth provider - return authProvider.decorate(context, authenticatedUser, credentials); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.decorate(context, authenticatedUser, credentials); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } @@ -326,15 +381,27 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { return context; // Delegate to underlying auth provider - return authProvider.redecorate(decorated, context, - authenticatedUser, credentials); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + return authProvider.redecorate(decorated, context, + authenticatedUser, credentials); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } @Override public void shutdown() { - if (authProvider != null) - authProvider.shutdown(); + try { + MDC.put(LogModule.MDC_CONTEXT_KEY, logContext); + if (authProvider != null) + authProvider.shutdown(); + } + finally { + MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT); + } } } diff --git a/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java b/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java new file mode 100644 index 000000000..2ef5e1c47 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/log/ForkableHashMap.java @@ -0,0 +1,267 @@ +/* + * 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 new file mode 100644 index 000000000..c1414c554 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/log/GuacamoleLogbackServiceProvider.java @@ -0,0 +1,109 @@ +/* + * 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.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * SLF4JServiceProvider that overrides the default behavior of Logback's + * service provider, forcing the use of a custom MDCAdapter. + */ +public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider { + + /** + * The highest version of SLF4J that this service provider can support. + * This is intentionally not actually a released version, but a reasonable + * upper bound. This value is identical to the value used by Logback. + */ + private static final String SLF4J_API_MAX_VERSION = "2.0.99"; + + /** + * The name of the LoggerContext maintained by this service provider. + */ + 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; + + /** + * 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. + */ + private final MDCAdapter mdcAdapter = new InheritableMDCAdapter(); + + /** + * The singleton MarkerFactory instance maintained by this service + * provider. + */ + private final IMarkerFactory markerFactory = new BasicMarkerFactory(); + + @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(); + + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public ILoggerFactory getLoggerFactory() { + return context; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public String getRequestedApiVersion() { + return SLF4J_API_MAX_VERSION; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java b/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java new file mode 100644 index 000000000..493925476 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/log/InheritableMDCAdapter.java @@ -0,0 +1,155 @@ +/* + * 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 { + + /** + * MDC context map that is automatically inherited by child threads. + */ + 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. + */ + 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) { + return context.get().get(key); + } + + @Override + public void remove(String key) { + context.get().remove(key); + } + + @Override + public void clear() { + context.get().clear(); + } + + @Override + public Map getCopyOfContextMap() { + // This function is invoked by Logback for every log message produced, + // so simply copying the context map is expensive. We instead create a + // copy-on-write reference to the context map. + return context.get().fork(); + } + + @Override + public void setContextMap(Map contextMap) { + 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) { + return getDeque(key).pop(); + } + + @Override + public Deque getCopyOfDequeByKey(String key) { + return new LinkedList<>(getDeque(key)); + } + + @Override + public void clearDequeByKey(String key) { + getDeque(key).clear(); + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java b/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java index 27bf0fe88..7d3c118d2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java +++ b/guacamole/src/main/java/org/apache/guacamole/log/LogLevel.java @@ -56,14 +56,14 @@ public enum LogLevel { * messages, while verbose, will not affect performance. */ @PropertyValue("debug") - DEBUG("debug"), + DEBUG("debug", "${guac_verbose_pattern}"), /** * Informational messages that may be useful for debugging, but which are * so low-level that they may affect performance. */ @PropertyValue("trace") - TRACE("trace"); + TRACE("trace", "${guac_verbose_pattern}"); /** * Format string whose sole format argument is a String containing the @@ -77,7 +77,7 @@ public enum LogLevel { + " \n" + " \n" + " \n" - + " %%d{HH:mm:ss.SSS} [%%thread] %%-5level %%logger{36} - %%msg%%n\n" + + " %s\n" + " \n" + " \n" + "\n" @@ -102,9 +102,10 @@ public enum LogLevel { private final byte[] logbackConfig; /** - * Creates a new LogLevel with the given names. The pair of names provided - * correspond to the name used within Guacamole's configuration and the - * name used within Logback's configuration. + * Creates a new LogLevel with the given names and pattern. The pair of + * names provided correspond to the name used within Guacamole's + * configuration and the name used within Logback's configuration, while + * the pattern determines how Logback will convert log messages to text. * * @param canonicalName * The name that should be used for this log level when configuring @@ -113,10 +114,14 @@ public enum LogLevel { * @param logbackLogLevel * The name that would be provided to Logback to log at this level if * manually configuring Logback using "logback.xml". + * + * @param pattern + * The Logback encoder pattern that should be use for messages logged + * at this level. */ - private LogLevel(String canonicalName, String logbackLogLevel) { + private LogLevel(String canonicalName, String logbackLogLevel, String pattern) { this.canonicalName = canonicalName; - this.logbackConfig = String.format(LOGBACK_XML_TEMPLATE, logbackLogLevel).getBytes(StandardCharsets.UTF_8); + this.logbackConfig = String.format(LOGBACK_XML_TEMPLATE, pattern, logbackLogLevel).getBytes(StandardCharsets.UTF_8); } /** @@ -131,7 +136,27 @@ public enum LogLevel { * "logback.xml" configuration file. */ private LogLevel(String logLevel) { - this(logLevel, logLevel); + this(logLevel, logLevel, "${guac_pattern}"); + } + + /** + * Creates a new LogLevel with the given name and pattern. The provided name + * corresponds to both the name used within Guacamole's configuration and + * the name used within Logback's configuration, while the pattern + * determines how Logback will convert log messages to text. + * + * @param logLevel + * The name that should be used for this log level when configuring + * Guacamole to log at this level using the "log-level" property AND + * when manually configuring Logback to log at this level using a + * "logback.xml" configuration file. + * + * @param pattern + * The Logback encoder pattern that should be use for messages logged + * at this level. + */ + private LogLevel(String logLevel, String pattern) { + this(logLevel, logLevel, pattern); } /** 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 a9e4ac3bc..9497ec0f4 100644 --- a/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/log/LogModule.java @@ -31,6 +31,7 @@ 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.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +51,95 @@ 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 + + "(?