Skip to content

Replace servlet context logging with logging facade#10138

Open
Lactantius wants to merge 19 commits intogwtproject:mainfrom
Lactantius:context-logging-replacement
Open

Replace servlet context logging with logging facade#10138
Lactantius wants to merge 19 commits intogwtproject:mainfrom
Lactantius:context-logging-replacement

Conversation

@Lactantius
Copy link
Copy Markdown

Fixes #10136

@@ -0,0 +1,21 @@
package com.google.gwt.user.server.rpc.logging;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +23 to +26
if (provider == null) {
synchronized (initLock) {
provider = loggerProvider.get();
if (provider == null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest version of your branch still has double checked locking here:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Fixed.

Comment on lines +80 to +84
ClassLoader[] loaders = new ClassLoader[] {
Thread.currentThread().getContextClassLoader(),
LogManager.class.getClassLoader(),
ClassLoader.getSystemClassLoader()
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to clear out the known loggers, but any class that already holds a Logger instance will keep it (with its stale delegate).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


@Override
public void error(String message, Throwable throwable) {
servletContext.log(message, throwable);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
servletContext.log(message, throwable);
servletContext.log("ERROR: " + message, throwable);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was explicitly info before, but error now?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

&& exceedsUncompressedContentLengthLimit(responseContent);
}

@Deprecated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc as to why it is deprecated, what the user should do instead

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer deprecated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@niloc132 niloc132 marked this pull request as draft October 29, 2025 15:15
@Lactantius Lactantius marked this pull request as ready for review December 11, 2025 20:29
@Lactantius Lactantius changed the title Replace servlet context logging with JUL logging Replace servlet context logging with logging facade Dec 12, 2025
}

SerializationPolicy loadPolicy(String url, Logger logger) {
SerializationPolicy loadPolicy(String url, ServletContext servletContext) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only two ways I can think around this are to:

  1. Pass in the logger as before, which would make it harder to see where a logging statement came from
  2. Since servlet context logging does not have any system for multiple named loggers, ServletContextLogger could just be a singleton, the init method of RemoteServiceServlet could set the ServletContext on it, and it could fall back to no-op or console logging or something if the ServletContext has not yet been set. (Since there are currently no log statements that could happen before the init method, this would not be a behavior change.) Then the ServletContext would 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your fault, but this is still junk. Not required to make less-bad to merge, but here's my quick stab at it.

Suggested change
* 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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +50 to +57
protected Object getLogLevel(Class<?> levelClass, String levelName) {
for (Object level : levelClass.getEnumConstants()) {
if (level.toString().equals(levelName)) {
return level;
}
}
return null;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +151 to +153
logger.warning(provider.getClass().getName());
logger.warning(String.valueOf(provider.isAvailable()));
logger.warning(String.valueOf(provider.isDefault()));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be warnings? Probably some debug/trace level ("config", "fine" etc), but with some other content to explain what they mean?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were accidentally committed, and are now removed. Thanks for catching them.

Comment on lines +54 to +62
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;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +113 to +119
if (loggerProvider.get() == null) {
synchronized (initLock) {
if (loggerProvider.get() == null) {
loggerProvider.set(provider);
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The atomic is removed.

@Lactantius Lactantius marked this pull request as draft February 13, 2026 15:06
@Lactantius Lactantius marked this pull request as ready for review February 25, 2026 21:09
Copy link
Copy Markdown
Member

@niloc132 niloc132 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PROVIDER_PROPERTY_KEY or the like, if we want it public (as documentation for downstream users)? Add Javadoc?

Comment on lines +71 to +82
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;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace ServletContext logging with logging facade

4 participants