Quieting JUL Warnings That Maven Tags as [ERROR]

This post describes a small problem in a Maven+Ant based MPS build: several JUL (Java Util Logging[1]) warnings emitted by MPS during headless generation end up prefixed with [ERROR] in the CI log. The build succeeds though. The mislabelling makes real errors harder to spot. Below, we walk through why it happens, two ways to fix it, and the trade-offs each fix brings.

Problem

In a headless MPS build driven by Maven with the maven-antrun-plugin, several loggers inside MPS emit informational messages at WARNING level. They are using java.util.logging — commonly abbreviated JUL — the logging framework built into the JDK. Examples include jetbrains.mps.classloading.ModulesWatcher, jetbrains.mps.persistence.ModelSourceRootWalker, and jetbrains.mps.persistence.DefaultModelRoot. Here is an excerpt from an actual CI run:

[ERROR]  [generate] 2026-05-28 12:12:14,498 WARNING - jetbrains.mps.persistence.ModelSourceRootWalker - Source root 'SourceRoot [/some/path/generator/templates]' does not exist, cannot traverse!

[ERROR]  [generate] 2026-05-28 12:12:14,499 WARNING - jetbrains.mps.persistence.DefaultModelRoot - Models have not been found within the SourceRoot [/some/path/generator/templates]

These messages are not errors. The build succeeds. However, they appear in the Maven output prefixed with [ERROR], which visually mixes them with actual build failures and makes real errors harder to locate. On a large CI log, a dozen mislabelled warnings can hide a single genuine failure.

Cause

The messages are logged as WARNING by MPS via java.util.logging. JUL’s default ConsoleHandler writes WARNING records to System.err. Somewhere in the pipeline between stderr and the final Maven output, stderr content gets tagged as [ERROR]. That is likely why a WARNING written to stderr shows up as an error in CI.

The tag depends on the stream, not on the JUL severity: bytes on stderr get [ERROR], bytes on stdout get [INFO]. Whatever assigns the tag looks at where the bytes came from, not at what they say.

Two intervention points are available: stop the message at the source (filter), or change the stream it uses (redirect).

Solution A: Level-based filter

A JUL configuration file raises the log level of the affected loggers to SEVERE:

jetbrains.mps.classloading.ModulesWatcher.level = SEVERE
jetbrains.mps.persistence.ModelSourceRootWalker.level = SEVERE
jetbrains.mps.persistence.DefaultModelRoot.level = SEVERE

The configuration file is passed to the JVM via -Djava.util.logging.config.file=/path/to/logging.properties. Once loaded, JUL drops all WARNING records from the listed loggers before they reach any handler.

Consequences. The messages are gone from the log. If one of these loggers later emits a new WARNING that would have been informative, it is dropped as well. This solution treats output from the affected loggers as irrelevant noise.

Solution B: Stream redirect via custom handler

A custom JUL handler writes to System.out instead of System.err, while preserving System.err for SEVERE records:

package example.logging; // replace with your own package

import java.util.logging.*;

public class StdoutHandler extends StreamHandler {
    public StdoutHandler() {
        super(System.out, new SimpleFormatter());
    }

    @Override
    public synchronized void publish(LogRecord record) {
        if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
            System.err.println(getFormatter().format(record));
            System.err.flush();
            return;
        }
        super.publish(record);
        flush();
    }

    @Override
    public synchronized void close() {
        flush();
    }
}

StreamHandler buffers its output, so the explicit flush() after each publish() call is necessary; without it, records may appear late or interleave unexpectedly with output from other handlers.

The JUL properties file routes the affected loggers through this handler and detaches them from the root handler chain:

handlers = java.util.logging.ConsoleHandler
.level = WARNING
java.util.logging.ConsoleHandler.level = WARNING
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

handlers registers ConsoleHandler on the root logger. .level sets the root logger’s own level, which every logger in the tree without its own explicit level inherits  — including the three MPS loggers redirected below, which have no explicit level of their own. ConsoleHandler.level sets the handler’s own, independent level. A handler filters separately from the logger it’s attached to, regardless of which of the two is stricter. And ConsoleHandler.formatter sets the formatter explicitly, matching ConsoleHandler own default. These properties and their defaults are documented in the ConsoleHandler class documentation [2] and [3].

The JUL properties file also routes the affected loggers through this handler and detaches them from the root handler chain:

jetbrains.mps.classloading.ModulesWatcher.handlers = example.logging.StdoutHandler
jetbrains.mps.classloading.ModulesWatcher.useParentHandlers = false
jetbrains.mps.persistence.ModelSourceRootWalker.handlers = example.logging.StdoutHandler
jetbrains.mps.persistence.ModelSourceRootWalker.useParentHandlers = false
jetbrains.mps.persistence.DefaultModelRoot.handlers = example.logging.StdoutHandler
jetbrains.mps.persistence.DefaultModelRoot.useParentHandlers = false

example.logging.StdoutHandler.level = WARNING
example.logging.StdoutHandler.formatter = java.util.logging.SimpleFormatter

The useParentHandlers = false line is required. Without it, each record goes both to the custom handler and to the root ConsoleHandler, appearing twice: once on stdout, once on stderr.

The compiled handler must be available to the JVM early in startup, before the first log call. In principle, adding the JAR to the regular classpath (-cp) would work, since JUL loads handler classes through the system classloader. In this setup, however, we pass the logging configuration via JAVA_TOOL_OPTIONS (see below), which propagates to any downstream forked JVMs. Placing the handler JAR on the bootclasspath guarantees availability regardless of the classpath configuration each fork ends up with:

-Xbootclasspath/a:/path/to/logging-handler.jar

Consequences. The WARNING records are kept visible in the log, but appear with an [INFO] prefix (because Maven tags stdout as [INFO]). Real SEVERE records still route through stderr and appear as [ERROR], preserving the ability to see genuine errors from the same loggers.

Trade-off

The two solutions differ in what happens to future, unexpected messages from the affected loggers.

Solution A minimises the moving parts. It is one properties file. It fits situations where the loggers are known to produce only irrelevant noise, and preserving that assumption is acceptable.

Solution B costs a Java class, a compile step, and a bootclasspath entry. It fits situations where the loggers may occasionally produce information worth seeing, and only the [ERROR] labelling is the problem, not the content.

Consequence: JVM argument propagation

The JVM arguments described above (-Xbootclasspath/a:…​ and -Djava.util.logging.config.file=…​) can be passed via either MAVEN_OPTS or JAVA_TOOL_OPTIONS. The two behave differently:

  • MAVEN_OPTS is a shell variable read by the mvn script. It is applied to the initial Maven JVM only. It is not propagated to forked JVMs [4].

  • JAVA_TOOL_OPTIONS is a real environment variable defined by the JVM Tool Interface specification [5]. Every JVM started in the process tree reads and applies it, including forked children [6].

If the build has any forked JVMs downstream of Maven (test runners, generator tasks, RCP builds), those forks need the same logging configuration. In that case, JAVA_TOOL_OPTIONS is required. MAVEN_OPTS alone leaves the forked JVMs on their default JUL configuration, and the warnings reappear from those processes.

Consequence: the Picked up JAVA_TOOL_OPTIONS message

When JAVA_TOOL_OPTIONS is set, the JVM emits a startup notification:

[ERROR] Picked up JAVA_TOOL_OPTIONS: -Xbootclasspath/a:... -Djava.util.logging.config.file=...

This message is emitted by HotSpot itself, written to stderr at every JVM start, and consequently appears as [ERROR] in the CI log. It is not an actual error.

The message cannot be disabled through JVM flags. An OpenJDK bug requesting a suppression option was filed in 2014 and closed with resolution Won’t Fix [7]. The rationale given in the bug comments is brief: "This will not be implemented, as it may introduce a vulnerability."

Practical option: accept the single cosmetic line per JVM start and document it.

Consequence: the two-line log format

After the redirect, SimpleFormatter writes each log entry across two lines by default. In the CI log, each line receives its own [INFO] prefix, and the first line ends with the method name of the log call (warning), which visually resembles a severity tag and can be mistaken for a duplicate:

[INFO]  [generate] Jun 16, 2026 8:13:09 AM jetbrains.mps.logging.JULogger warning
[INFO]  [generate] WARNING: 2 modules are marked as invalid roots for class loading out of 815 modules totally in the CL graph

The fix is a format string in the JUL properties file:

java.util.logging.SimpleFormatter.format = %1$tF %1$tT %4$s %3$s - %5$s%6$s%n

The format tokens are documented in the java.util.logging.SimpleFormatter API [8]. %1$tF %1$tT is ISO date and time, %4$s is the level, %3$s is the logger name, %5$s is the message, %6$s is the stack trace if present, and %n is one newline. The same log entry now appears as a single line:

[INFO]  [generate] 2026-06-16 09:05:53 WARNING jetbrains.mps.classloading.ModulesWatcher - 2 modules are marked as invalid roots for class loading out of 815 modules totally in the CL graph

One entry per line, one prefix per line.

Result

With Solution B in place, the CI log shows the affected warnings prefixed as [INFO] instead of [ERROR]. Real SEVERE events from the same loggers continue to appear as [ERROR]. The build itself is unchanged; only the log presentation is corrected.


1. Java Util Logging (JUL): The Complete Guide to Built-in Java Logging, https://www.dash0.com/faq/java-util-logging-jul-the-complete-guide-to-built-in-java-logging
3. Java Log Levels: How to Use Them in Practice, Dash0, https://www.dash0.com/knowledge/java-log-levels
4. SUREFIRE-1035: When forking a JVM, JVM settings are not inherited from MAVEN_OPTS., https://issues.apache.org/jira/browse/SUREFIRE-1035
7. JDK-8039152, "Need a way to suppress message when picking up JAVA_TOOL_OPTIONS", https://bugs.openjdk.org/browse/JDK-8039152
8. java.util.logging.SimpleFormatter API documentation, https://docs.oracle.com/javase/8/docs/api/java/util/logging/SimpleFormatter.html

About Sergey Zverlov

I am a Senior Engineer at F1RE, since April 2026. I have worked in model-based systems and software engineering since 2013, with experience from both industry and research.

You can contact me at sergey@f1re.io.