Replace servlet context logging with logging facade#10138
Replace servlet context logging with logging facade#10138Lactantius wants to merge 19 commits intogwtproject:mainfrom
Conversation
| @@ -0,0 +1,21 @@ | |||
| package com.google.gwt.user.server.rpc.logging; | |||
There was a problem hiding this comment.
Files are going to need copyright headers, and public classes will need at least some semblance of javadoc. Indentation for this codebase is two spaces, and imports have a super-picky sort order (I find the messages to be somewhat unclear, so looking for a similar class and using its imports as a template can help).
You probably want to run ant checkstyle locally before we run it on the PR - there would be a lot of logging for this.
| if (provider == null) { | ||
| synchronized (initLock) { | ||
| provider = loggerProvider.get(); | ||
| if (provider == null) { |
There was a problem hiding this comment.
This synchronization doesn't make any sense to me - it looks like you're doing a double-checked locking, but for a local variable? Why double check on an atomic at all? And after the CAS on 21, there is no other chance for the atomic to have held a null to begin with?
There was a problem hiding this comment.
I changed it to allow setting up a provider only once, so the locking is much more straightforward now, and might even work as intended.
There was a problem hiding this comment.
The latest version of your branch still has double checked locking here:
| ClassLoader[] loaders = new ClassLoader[] { | ||
| Thread.currentThread().getContextClassLoader(), | ||
| LogManager.class.getClassLoader(), | ||
| ClassLoader.getSystemClassLoader() | ||
| }; |
There was a problem hiding this comment.
The use of different classloaders seems to suggest that you expect that an application might have a class available to a different classloader than the one that the application is actively using (and its parents...), but then below you dedup in a way that doesn't respect which classloader the item came from?
What's the goal here? If you just want "classes the application can see", I think the default that ServiceLoader.load(Class<?>) uses is sufficient?
There was a problem hiding this comment.
Removed. I was trying to get it to work better with Tomcat, but reading the documentation again suggested this wouldn't really do anything, and it didn't.
| providers = loadAllProviders(); | ||
| } | ||
| for (LoggerProvider provider : providers) { | ||
| if (provider.isDefault() || provider.getClass().getName().equals(name)) { |
There was a problem hiding this comment.
To confirm, isDefault is meant to sometimes override requesting the named provider, based on classloader order?
As written, if A is "default" but B is requested, if the ServiceLoader returns [A,B] you'll get A, but if it returns [B,A] you'll get B.
There was a problem hiding this comment.
Fixed. It will now pick a default only after checking through all the providers for a name match.
| String currentProviderName = currentProvider != null ? currentProvider.getClass().getName() : null; | ||
| if (!provider.getClass().getName().equals(currentProviderName)) { | ||
| loggerProvider.set(provider); | ||
| loggers.clear(); |
There was a problem hiding this comment.
This appears to clear out the known loggers, but any class that already holds a Logger instance will keep it (with its stale delegate).
There was a problem hiding this comment.
Removed. No longer trying to allow resetting the provider.
| private LoggerDelegate createDelegate() { | ||
| LoggerProvider provider = LogManager.getLoggerProvider(); | ||
| LoggerDelegate newDelegate = provider.createLogger(name); | ||
| return delegate.compareAndSet(null, newDelegate) ? newDelegate : delegate.get(); |
There was a problem hiding this comment.
This is safe as written, but will cease to be safe if you ever traverse the LogManager.loggers.values() collection and try to replace/update the delegates. Not saying you want to or even should do that, but this could be made safe by using updateAndGet(old -> old == null ? newDelegate : old).
|
|
||
| @Override | ||
| public void error(String message, Throwable throwable) { | ||
| servletContext.log(message, throwable); |
There was a problem hiding this comment.
I think this is missing the error prefix? Could also delegate between the two impls (maybe even make LoggerDelegate.error(String) do that for you).
| servletContext.log(message, throwable); | |
| servletContext.log("ERROR: " + message, throwable); |
There was a problem hiding this comment.
At least some implementations of ServletContext#log seem to add some sort of prefix or level automatically when you use the overload with the Throwable argument, but I will check.
There was a problem hiding this comment.
I seem to have hallucinated that. Fixed.
| try { | ||
| List<ClassNotFoundException> errs = new ArrayList<ClassNotFoundException>(); | ||
| SerializationPolicy policy = SerializationPolicyLoader.loadFromStream(in, errs); | ||
| logger.logInfo("Downloaded serialization policy from " + sourceName); |
There was a problem hiding this comment.
This was explicitly info before, but error now?
| && exceedsUncompressedContentLengthLimit(responseContent); | ||
| } | ||
|
|
||
| @Deprecated |
There was a problem hiding this comment.
javadoc as to why it is deprecated, what the user should do instead
There was a problem hiding this comment.
I don't understand your reply - perhaps you changed your mind, and it is still deprecated, but now has a comment? Or did you intend to drop this?
…tialization of RpcLoggerProvider Various other improvements as suggested by niloc32
| } | ||
|
|
||
| SerializationPolicy loadPolicy(String url, Logger logger) { | ||
| SerializationPolicy loadPolicy(String url, ServletContext servletContext) { |
There was a problem hiding this comment.
My interpretation of this was that the original authors took care to not permanently tie these internals to servlets, but let any server impl support adapting calls to be handled by the rpc impl.
However, the only way one could achieve that would be to actually build non-servlet code in the com.google.gwt.user.server.rpc package to be able to use this package protected class, so I think it doesn't actually matter in any realistic case.
There was a problem hiding this comment.
The only two ways I can think around this are to:
- Pass in the logger as before, which would make it harder to see where a logging statement came from
- Since servlet context logging does not have any system for multiple named loggers,
ServletContextLoggercould just be a singleton, theinitmethod ofRemoteServiceServletcould set theServletContexton it, and it could fall back to no-op or console logging or something if theServletContexthas not yet been set. (Since there are currently no log statements that could happen before theinitmethod, this would not be a behavior change.) Then theServletContextwould no longer have to be a parameter on the logging methods. Something like this:
diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
index 5d80b35b2..d4c054209 100644
--- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
@@ -191,6 +191,7 @@ public class RemoteServiceServlet extends AbstractRemoteServiceServlet
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
+ RpcLogManager.setServletContext(config.getServletContext());
codeServerPort = getCodeServerPort();
}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java
index 6a11dae2f..9686243e3 100644
--- a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java
+++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java
@@ -15,6 +15,7 @@
*/
package com.google.gwt.user.server.rpc.logging;
+import javax.servlet.ServletContext;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
@@ -49,6 +50,12 @@ public class RpcLogManager {
name -> new RpcLogger(name, getLoggerProvider()));
}
+ public static void setServletContext(ServletContext servletContext) {
+ if (loggerProvider instanceof ServletContextLoggerProvider) {
+ ((ServletContextLoggerProvider) loggerProvider).setServletContext(servletContext);
+ }
+ }
+
/**
* Retrieves the current instance of the {@link RpcLoggerProvider}.
* <br />
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java
index bd0f67e4d..20a615691 100644
--- a/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java
+++ b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java
@@ -26,33 +26,58 @@ public class ServletContextLoggerProvider implements RpcLoggerProvider {
public ServletContextLoggerProvider() {
}
+ public void setServletContext(ServletContext servletContext) {
+ ServletContextLogger.INSTANCE.servletContext = servletContext;
+ }
+
@Override
public RpcLoggerDelegate createLogger(String name) {
- return new ServletContextLogger();
+ return ServletContextLogger.INSTANCE;
}
private static final class ServletContextLogger implements RpcLoggerDelegate {
- ServletContextLogger() { }
+ private ServletContext servletContext;
+
+ private static final ServletContextLogger INSTANCE = new ServletContextLogger();
+
+ private ServletContextLogger() { }
...
}
}|
|
||
| /** | ||
| * Creates an client with the given configuration, | ||
| * Creates a client with the given configuration. |
There was a problem hiding this comment.
Not your fault, but this is still junk. Not required to make less-bad to merge, but here's my quick stab at it.
| * Creates a client with the given configuration. | |
| * Creates a client to load serialization policies from a dev mode server, with the given timeouts. |
| try { | ||
| logMessage.invoke(logger, warnLevel, message); | ||
| } catch (Exception e) { | ||
| System.err.println("Failed to log message: " + message); |
There was a problem hiding this comment.
| System.err.println("Failed to log message: " + message); | |
| System.err.println("Failed to log warning: " + message); |
| try { | ||
| logMessage.invoke(logger, errorLevel, message); | ||
| } catch (Exception e) { | ||
| System.err.println("Failed to log message: " + message); |
There was a problem hiding this comment.
| System.err.println("Failed to log message: " + message); | |
| System.err.println("Failed to log error: " + message); |
| try { | ||
| logThrowable.invoke(logger, errorLevel, message, throwable); | ||
| } catch (Exception e) { | ||
| System.err.println("Failed to log message: " + message); |
There was a problem hiding this comment.
| System.err.println("Failed to log message: " + message); | |
| System.err.println("Failed to log error: " + message); |
|
|
||
| /** | ||
| * Uses reflection to create loggers that may be unavailable in some environments. | ||
| * Compatible with platform logging and Log4j; incompatible with SLF4j. |
There was a problem hiding this comment.
File a bug for SLF4J? I'm not sure if there'll be much interest (and there are adapters for JULI/etc), but it at least will answer the question and have a place to discuss if someone wants to pick up the work.
There was a problem hiding this comment.
I removed all the reflective stuff. I did not see a good way to make a base class that would actually fit the APIs of all the main players, there will at least be some config file alterations no matter what, and if you actually have access to the relevant classes, it takes about ninety seconds to copy-paste the JulLoggerProvider, convert it to a different logger, and drop it in your own application or shared library.
| protected Object getLogLevel(Class<?> levelClass, String levelName) { | ||
| for (Object level : levelClass.getEnumConstants()) { | ||
| if (level.toString().equals(levelName)) { | ||
| return level; | ||
| } | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Since we already insist this is an enum (rather than a class that has static/final fields), maybe just use Enum.valueOf(Class, String)? It won't matter at all for perf, but you could effectively inline it away that way. You'll need a slightly better cast, but arguably you need one anyway since we're asserting that the Class is an enum type.
| logger.warning(provider.getClass().getName()); | ||
| logger.warning(String.valueOf(provider.isAvailable())); | ||
| logger.warning(String.valueOf(provider.isDefault())); |
There was a problem hiding this comment.
Should these be warnings? Probably some debug/trace level ("config", "fine" etc), but with some other content to explain what they mean?
There was a problem hiding this comment.
These were accidentally committed, and are now removed. Thanks for catching them.
| private RpcLoggerDelegate getDelegate() { | ||
| RpcLoggerDelegate result = this.delegate.get(); | ||
| if (result == null) { | ||
| RpcLoggerProvider provider = RpcLogManager.getLoggerProvider(); | ||
| RpcLoggerDelegate newDelegate = provider.createLogger(name); | ||
| result = delegate.updateAndGet(old -> old == null ? newDelegate : old); | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Why is this lazy rather than initializing it when constructed, and avoiding the atomics? It is still racy, though the new/old guard at least ensures we'll always write to the same logger, but the extra logger can still be created.
Used this way, the updateAndGet call is just
delegate.compareAndSet(null, newDelegate);
return delegate.get()except this impl won't potentially loop.
With that said, at least from reading the review we don't have initialization cycles, so we can just initialize once in the constructor, right? Make RpcLogger final and don't let its constructor be public, now we guarantee that only RpcLogManager can create it, and RpcLogManager will always do so after it is itself initialied - then it is always safe to either call RpcLogManager.getLoggerProvider(), or make it part of the contract that RpcLogManager itself passes in the delegate with the name?
I'm sorry if I'm being thick and missing some contention, but I don't think we gain very much here, and we lose a little due to complexity and atomic performance.
There was a problem hiding this comment.
Sorry I’m just now getting back to this. The idea was that, since getLogger is called statically at the top of RemoteServiceServlet, it would be hard to guarantee that a manual call to initialize would happen first. On the other hand, if someone just adds a logging statement to a static block that runs early enough in the lifecycle, the lazy initialization will suddenly break for reasons that might take a long time for the user to uncover. I am just going to reduce the choice mechanisms to either setting a system property or writing a custom provider that returns true for isDefault(). Without a way of guaranteeing, in all environments, that a manual initialization in application code will actually run first, it will probably cause frustration. That is why I was originally trying to allow the provider to be reset, but I think you correctly pointed out that such a mechanism would probably not be worth it.
| if (loggerProvider.get() == null) { | ||
| synchronized (initLock) { | ||
| if (loggerProvider.get() == null) { | ||
| loggerProvider.set(provider); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Why use atomics if we're going to block on the entire manager's lock anyway? Both writes are guarded by a lock, maybe this should just be a standard double-checked lock - or just initialize eagerly with the class?
Most of these are apparently unused - but appear to provide an escape hatch for the three documented ways to specify a logger. Should these public initialize() methods be a 4th+ option?
…y initialize the delegate
…roperly and would not work in many environments
niloc132
left a comment
There was a problem hiding this comment.
I largely like this as it stands, though I think it could stand to have a little more cleanup before we say go - empty default constructors where access modifier matches the type, some added javadoc for public APIs.
I'm going to play with it a bit today so that I am sure I don't have more to say about how it is set up.
| && exceedsUncompressedContentLengthLimit(responseContent); | ||
| } | ||
|
|
||
| @Deprecated |
There was a problem hiding this comment.
I don't understand your reply - perhaps you changed your mind, and it is still deprecated, but now has a comment? Or did you intend to drop this?
| /** | ||
| * A very simplified interface for logging RPC events. | ||
| */ | ||
| public interface RpcLoggerDelegate { |
There was a problem hiding this comment.
What if we dropped the existing RpcLogger, and renamed this interface to RpcLogger? The single place where we call new RpcLogger(...) could just as easily be rewritten as getLoggerProvider().createLogger(name), and otherwise RpcLogger and RpcLoggerDelegate have the exact same public API (though one is a final class and one an interface).
| */ | ||
| public class RpcLogManager { | ||
|
|
||
| public static final String PROVIDER_PARAMETER_KEY = "gwt.rpc.logging"; |
There was a problem hiding this comment.
PROVIDER_PROPERTY_KEY or the like, if we want it public (as documentation for downstream users)? Add Javadoc?
| private static RpcLoggerProvider getLoggerProvider() { | ||
| if (loggerProvider == null) { | ||
| synchronized (RpcLogManager.class) { | ||
| if (loggerProvider == null) { | ||
| RpcLoggerProvider fallback = new ServletContextLoggerProvider(); | ||
| String providerName = System.getProperty(PROVIDER_PARAMETER_KEY); | ||
| loggerProvider = loadProvider(providerName, fallback); | ||
| } | ||
| } | ||
| } | ||
| return loggerProvider; | ||
| } |
There was a problem hiding this comment.
I think this and the volatile field are still overkill - there's nothing the user can do to influence creation of the logger provider between class initialization and getLoggerProvider() being called (that window is very very narrow as written - setServletContext and getLogger both directly call getLoggerProvider). We're relying only on the system property being set (or not) and the service providers being on the classpath - those are typically set as they will remain at startup.
Why not make loggerProvider a static final field, and initialize it just once? The loadProvider method can still be used as is, inlined as an initializer, or the system property can be made a local instead of a param (its a private method anyway, no one can pass a different value).
Fixes #10136