GUACAMOLE-2181: Include extension information and line continuations in log messages.

This commit is contained in:
Michael Jumper committed 2025-12-08 18:04:12 -08:00
1 parent 6221929989
commit be53376f28
9 files changed
+768 -28

No files matched your search

+1
View File
@@ -1 +1,2 @@
src/main/frontend/dist/**/* src/main/frontend/dist/**/*
src/main/resources/META-INF/services/*
@@ -23,12 +23,14 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.log.LogModule;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/** /**
* Provides a safe wrapper around an AuthenticationProvider subclass, such that * Provides a safe wrapper around an AuthenticationProvider subclass, such that
@@ -40,7 +42,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
/** /**
* Logger for this class. * 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 * The underlying authentication provider, or null if the authentication
@@ -58,6 +60,12 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
*/ */
private final Set<String> tolerateFailures; 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 identifier to provide for the underlying authentication provider if
* the authentication provider could not be loaded. * the authentication provider could not be loaded.
@@ -86,9 +94,16 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
public AuthenticationProviderFacade( public AuthenticationProviderFacade(
Class<? extends AuthenticationProvider> authProviderClass, Class<? extends AuthenticationProvider> authProviderClass,
Set<String> tolerateFailures) { Set<String> tolerateFailures) {
this.tolerateFailures = tolerateFailures; this.tolerateFailures = tolerateFailures;
this.authProvider = ProviderFactory.newInstance("authentication provider", this.authProvider = ProviderFactory.newInstance("authentication provider",
authProviderClass); authProviderClass);
if (this.authProvider != null)
this.logContext = LogModule.getMDCContext(authProvider);
else
this.logContext = LogModule.MDC_CONTEXT_ROOT;
} }
@Override @Override
@@ -115,7 +130,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
// Delegate to underlying auth provider // 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; return credentials;
// Delegate to underlying auth provider // 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 // Delegate to underlying auth provider
try { try {
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
return authProvider.authenticateUser(credentials); 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 @Override
@@ -239,7 +271,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
// Delegate to underlying auth provider // 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 // Delegate to underlying auth provider
try { try {
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
return authProvider.getUserContext(authenticatedUser); 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 @Override
@@ -298,8 +341,14 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
} }
// Delegate to underlying auth provider // 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 @Override
@@ -312,7 +361,13 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
return context; return context;
// Delegate to underlying auth provider // 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; return context;
// Delegate to underlying auth provider // Delegate to underlying auth provider
return authProvider.redecorate(decorated, context, try {
authenticatedUser, credentials); 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 @Override
public void shutdown() { public void shutdown() {
if (authProvider != null) try {
authProvider.shutdown(); MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
if (authProvider != null)
authProvider.shutdown();
}
finally {
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT);
}
} }
} }
@@ -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.
* <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();
}
}
@@ -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;
}
}
@@ -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<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.
*/
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) {
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<String, String> 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<String, String> 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<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) {
return getDeque(key).pop();
}
@Override
public Deque<String> getCopyOfDequeByKey(String key) {
return new LinkedList<>(getDeque(key));
}
@Override
public void clearDequeByKey(String key) {
getDeque(key).clear();
}
}
@@ -56,14 +56,14 @@ public enum LogLevel {
* messages, while verbose, will not affect performance. * messages, while verbose, will not affect performance.
*/ */
@PropertyValue("debug") @PropertyValue("debug")
DEBUG("debug"), DEBUG("debug", "${guac_verbose_pattern}"),
/** /**
* Informational messages that may be useful for debugging, but which are * Informational messages that may be useful for debugging, but which are
* so low-level that they may affect performance. * so low-level that they may affect performance.
*/ */
@PropertyValue("trace") @PropertyValue("trace")
TRACE("trace"); TRACE("trace", "${guac_verbose_pattern}");
/** /**
* Format string whose sole format argument is a String containing the * Format string whose sole format argument is a String containing the
@@ -77,7 +77,7 @@ public enum LogLevel {
+ " <!-- Default appender -->\n" + " <!-- Default appender -->\n"
+ " <appender name=\"GUAC-DEFAULT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n" + " <appender name=\"GUAC-DEFAULT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n"
+ " <encoder>\n" + " <encoder>\n"
+ " <pattern>%%d{HH:mm:ss.SSS} [%%thread] %%-5level %%logger{36} - %%msg%%n</pattern>\n" + " <pattern>%s</pattern>\n"
+ " </encoder>\n" + " </encoder>\n"
+ " </appender>\n" + " </appender>\n"
+ "\n" + "\n"
@@ -102,9 +102,10 @@ public enum LogLevel {
private final byte[] logbackConfig; private final byte[] logbackConfig;
/** /**
* Creates a new LogLevel with the given names. The pair of names provided * Creates a new LogLevel with the given names and pattern. The pair of
* correspond to the name used within Guacamole's configuration and the * names provided correspond to the name used within Guacamole's
* name used within Logback's configuration. * configuration and the name used within Logback's configuration, while
* the pattern determines how Logback will convert log messages to text.
* *
* @param canonicalName * @param canonicalName
* The name that should be used for this log level when configuring * The name that should be used for this log level when configuring
@@ -113,10 +114,14 @@ public enum LogLevel {
* @param logbackLogLevel * @param logbackLogLevel
* The name that would be provided to Logback to log at this level if * The name that would be provided to Logback to log at this level if
* manually configuring Logback using "logback.xml". * 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.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. * "logback.xml" configuration file.
*/ */
private LogLevel(String logLevel) { 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);
} }
/** /**
@@ -31,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.properties.EnumGuacamoleProperty; import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -50,6 +51,95 @@ public class LogModule extends AbstractModule {
*/ */
private final Environment environment; 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 * Property that specifies the highest level of verbosity that Guacamole
* should use for the messages in its logs. * should use for the messages in its logs.
@@ -125,6 +215,13 @@ public class LogModule extends AbstractModule {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset(); 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 // Initialize logback
JoranConfigurator configurator = new JoranConfigurator(); JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context); configurator.setContext(context);
@@ -32,10 +32,12 @@ import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSession; import org.apache.guacamole.GuacamoleSession;
import org.apache.guacamole.log.LogModule;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.rest.auth.AuthenticationService; import org.apache.guacamole.rest.auth.AuthenticationService;
import org.apache.guacamole.rest.tunnel.TunnelCollectionResource; import org.apache.guacamole.rest.tunnel.TunnelCollectionResource;
import org.apache.guacamole.rest.tunnel.TunnelCollectionResourceFactory; import org.apache.guacamole.rest.tunnel.TunnelCollectionResourceFactory;
import org.slf4j.MDC;
/** /**
* A REST resource which exposes all data associated with a Guacamole user's * A REST resource which exposes all data associated with a Guacamole user's
@@ -116,8 +118,16 @@ public class SessionResource {
// Pull UserContext defined by the given auth provider identifier // Pull UserContext defined by the given auth provider identifier
UserContext userContext = session.getUserContext(authProviderIdentifier); UserContext userContext = session.getUserContext(authProviderIdentifier);
// Return a resource exposing the retrieved UserContext MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext); try {
// Return a resource exposing the retrieved UserContext
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext);
}
finally {
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT);
}
} }
@@ -145,13 +155,21 @@ public class SessionResource {
// Pull UserContext defined by the given auth provider identifier // Pull UserContext defined by the given auth provider identifier
UserContext userContext = session.getUserContext(authProviderIdentifier); UserContext userContext = session.getUserContext(authProviderIdentifier);
// Pull resource from user context MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
Object resource = userContext.getResource(); try {
if (resource != null)
return resource;
// UserContext-specific resource could not be found // Pull resource from user context
throw new GuacamoleResourceNotFoundException("No such resource."); Object resource = userContext.getResource();
if (resource != null)
return resource;
// UserContext-specific resource could not be found
throw new GuacamoleResourceNotFoundException("No such resource.");
}
finally {
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.MDC_CONTEXT_ROOT);
}
} }
@@ -0,0 +1 @@
org.apache.guacamole.log.GuacamoleLogbackServiceProvider