GUACAMOLE-2181: Migrate to deriving log context from call stack.
This commit is contained in:
13 files changed
+937
-761
No files matched your search
+9
-74
@@ -23,14 +23,12 @@ 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
|
||||||
@@ -60,12 +58,6 @@ 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.
|
||||||
@@ -99,11 +91,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
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
|
||||||
@@ -130,13 +117,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.getResource();
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
|
||||||
return authProvider.getResource();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,13 +129,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
return credentials;
|
return credentials;
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.updateCredentials(credentials);
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
|
||||||
return authProvider.updateCredentials(credentials);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +197,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +222,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -267,13 +237,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
|
||||||
return authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +255,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,10 +280,6 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -337,13 +296,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.updateUserContext(context, authenticatedUser, credentials);
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
|
||||||
return authProvider.updateUserContext(context, authenticatedUser, credentials);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,13 +310,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
return context;
|
return context;
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.decorate(context, authenticatedUser, credentials);
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
|
||||||
return authProvider.decorate(context, authenticatedUser, credentials);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,27 +324,15 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
|
|||||||
return context;
|
return context;
|
||||||
|
|
||||||
// Delegate to underlying auth provider
|
// Delegate to underlying auth provider
|
||||||
try {
|
return authProvider.redecorate(decorated, context,
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
authenticatedUser, credentials);
|
||||||
return authProvider.redecorate(decorated, context,
|
|
||||||
authenticatedUser, credentials);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
try {
|
if (authProvider != null)
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, logContext);
|
authProvider.shutdown();
|
||||||
if (authProvider != null)
|
|
||||||
authProvider.shutdown();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ import org.apache.guacamole.GuacamoleException;
|
|||||||
* A ClassLoader implementation which finds classes within .jar files within a
|
* A ClassLoader implementation which finds classes within .jar files within a
|
||||||
* given directory.
|
* 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.
|
* 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());
|
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
|
// 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
|
* the given .jar. If classes are defined in both the parent and the extension
|
||||||
* .jar, the versions defined within the extension .jar are used.
|
* .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.
|
* Logger for this class.
|
||||||
@@ -71,6 +71,11 @@ public class ExtensionClassLoader extends URLClassLoader {
|
|||||||
*/
|
*/
|
||||||
private final ClassLoader parent;
|
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
|
* Returns the URL that refers to the given file. If the given file refers
|
||||||
* to a directory, an exception is thrown.
|
* 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
|
* multiple times will not affect previously-returned instances of
|
||||||
* ExtensionClassLoader.
|
* ExtensionClassLoader.
|
||||||
*
|
*
|
||||||
|
* @param manifest
|
||||||
|
* The parsed manifest of the extension.
|
||||||
|
*
|
||||||
* @param extension
|
* @param extension
|
||||||
* The extension .jar file from which classes should be loaded.
|
* 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
|
* If the given file is not actually a file, or the contents of the
|
||||||
* file cannot be read.
|
* file cannot be read.
|
||||||
*/
|
*/
|
||||||
public ExtensionClassLoader(File extension, List<File> temporaryFiles,
|
public ExtensionClassLoader(ExtensionManifest manifest, File extension,
|
||||||
ClassLoader parent) throws GuacamoleException {
|
List<File> temporaryFiles, ClassLoader parent) throws GuacamoleException {
|
||||||
super(getExtensionURLs(extension, temporaryFiles), null);
|
super(getExtensionURLs(extension, temporaryFiles), null);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
this.manifest = manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+12
-35
@@ -19,10 +19,7 @@
|
|||||||
|
|
||||||
package org.apache.guacamole.log;
|
package org.apache.guacamole.log;
|
||||||
|
|
||||||
import ch.qos.logback.classic.LoggerContext;
|
import ch.qos.logback.classic.util.LogbackMDCAdapter;
|
||||||
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.IMarkerFactory;
|
||||||
import org.slf4j.helpers.BasicMarkerFactory;
|
import org.slf4j.helpers.BasicMarkerFactory;
|
||||||
import org.slf4j.spi.MDCAdapter;
|
import org.slf4j.spi.MDCAdapter;
|
||||||
@@ -30,7 +27,7 @@ import org.slf4j.spi.SLF4JServiceProvider;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SLF4JServiceProvider that overrides the default behavior of Logback's
|
* 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 {
|
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";
|
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";
|
private ContextAwareLoggerFactory loggerFactory;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* The singleton instance of the MDCAdapter implementation that should be
|
||||||
* used by Logback and SLF4J. The custom implementation provided here
|
* used by Logback and SLF4J.
|
||||||
* allows the context map to be inherited by child threads.
|
|
||||||
*/
|
*/
|
||||||
private final MDCAdapter mdcAdapter = new InheritableMDCAdapter();
|
private final MDCAdapter mdcAdapter = new LogbackMDCAdapter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The singleton MarkerFactory instance maintained by this service
|
* The singleton MarkerFactory instance maintained by this service
|
||||||
@@ -67,23 +60,7 @@ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
loggerFactory = new ContextAwareLoggerFactory(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();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,8 +69,8 @@ public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ILoggerFactory getLoggerFactory() {
|
public ContextAwareLoggerFactory getLoggerFactory() {
|
||||||
return context;
|
return loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
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.joran.spi.JoranException;
|
||||||
import ch.qos.logback.core.util.StatusPrinter;
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -31,8 +28,8 @@ 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.ILoggerFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -51,95 +48,6 @@ 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.
|
||||||
@@ -212,23 +120,19 @@ public class LogModule extends AbstractModule {
|
|||||||
|
|
||||||
try (InputStream logbackConfiguration = getLogbackConfiguration()) {
|
try (InputStream logbackConfiguration = getLogbackConfiguration()) {
|
||||||
|
|
||||||
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
|
// Bail out if the system is somehow preventing Guacamole from
|
||||||
context.reset();
|
// 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
|
((ReconfigurableLoggerFactory) loggerFactory).reconfigure(logbackConfiguration);
|
||||||
// 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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (JoranException e) {
|
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.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
|
||||||
@@ -118,16 +116,8 @@ 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);
|
||||||
|
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
|
// Return a resource exposing the retrieved UserContext
|
||||||
try {
|
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext);
|
||||||
|
|
||||||
// Return a resource exposing the retrieved UserContext
|
|
||||||
return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext);
|
|
||||||
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
MDC.remove(LogModule.MDC_CONTEXT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,21 +145,13 @@ 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);
|
||||||
|
|
||||||
MDC.put(LogModule.MDC_CONTEXT_KEY, LogModule.getMDCContext(userContext.getAuthenticationProvider()));
|
// Pull resource from user context
|
||||||
try {
|
Object resource = userContext.getResource();
|
||||||
|
if (resource != null)
|
||||||
|
return resource;
|
||||||
|
|
||||||
// Pull resource from user context
|
// UserContext-specific resource could not be found
|
||||||
Object resource = userContext.getResource();
|
throw new GuacamoleResourceNotFoundException("No such resource.");
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user