Conversation
bae1db9 to
5a5af1e
Compare
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e64db31 to
de8044a
Compare
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8baa65a to
02ac855
Compare
gnodet
left a comment
There was a problem hiding this comment.
Well-designed foundational logging infrastructure. The structured LogEvent API, JUL handler, and Log API enhancements provide a solid base for the build report and console modes PRs. A few issues noted below.
Also noted:
- Good catch fixing
warn(Supplier<String>, Throwable)callinglogger.info()instead oflogger.warn(). - The logger name change from
getFullGoalName()togetImplementation()(FQCN) enables proper hierarchical SLF4J level configuration but is a behavioral change — worth mentioning in release notes for users who configured logging by short-form names. - No unit tests were added for the new functionality (MavenJulHandler, DefaultLogEvent, StackWalker metadata capture, LogSink contract). Given this is foundational for the entire logging pipeline, targeted tests would increase confidence.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
| default long sequenceNumber() { | ||
| return -1; | ||
| } | ||
| } |
There was a problem hiding this comment.
The @return Javadoc says "always non-negative" but the default implementation returns -1, and DefaultLogEvent convenience constructors also pass -1. The threadId() method in this same interface correctly documents its sentinel ("or -1 if unavailable").
| } | |
| * @return the sequence number, or {@code -1} if unavailable |
There was a problem hiding this comment.
Fixed — javadoc now reads @return the sequence number, or {@code -1} if unavailable.
There was a problem hiding this comment.
Fixed — javadoc now reads @return the sequence number, or {@code -1} if unavailable.
| * Formats the log message, applying i18n resource bundle lookup and | ||
| * {@link MessageFormat} parameter substitution, matching the behavior | ||
| * of {@code SLF4JBridgeHandler}. | ||
| */ |
There was a problem hiding this comment.
formatForConsole() produces [LEVEL] message (no timestamp, no logger name), while MavenSimpleLogger produces a full formatted line with timestamp and logger name per configuration. This means LogEvent.formattedMessage() has an inconsistent format depending on whether the event originated from JUL or SLF4J. The formattedMessage() Javadoc promises "the level prefix, timestamp, and any ANSI styling" — JUL-sourced events would be missing the timestamp and logger name.
There was a problem hiding this comment.
Fixed — removed the formatForConsole() method entirely. All JUL events now always route through SLF4J so that MavenSimpleLogger produces a consistent formattedMessage (with timestamp, logger name, and ANSI styling) regardless of origin. The JUL metadata is stashed in a ThreadLocal before the SLF4J call so that ProjectBuildLogAppender can read it when constructing the LogEvent.
ascheman
left a comment
There was a problem hiding this comment.
Really solid foundation — the three-path convergence (Log API / SLF4J / JUL) onto one structured LogEvent is clean, and preserving the LogRecord metadata the stock SLF4JBridgeHandler drops is a genuine improvement. Nice catch on the warn(Supplier, Throwable) → logger.info() bug.
A few things worth a look before this becomes the base of the 7-PR chain — one API-compat question, one fork-context correctness question, one perf note, and some small nits. Nothing structural.
On tests (echoing the earlier note): the two I'd most want are a regression test asserting warn(Supplier, Throwable) actually logs at WARN, and a table test for the JUL→SLF4J level mapping (esp. FINEST→TRACE and CONFIG→INFO). Given the ThreadLocal/StackWalker plumbing, those would lock down the easy-to-regress bits.
| * {@return true if the <b>trace</b> error level is enabled} | ||
| * @since 4.1.0 | ||
| */ | ||
| boolean isTraceEnabled(); |
There was a problem hiding this comment.
The six new trace methods (isTraceEnabled + 5 overloads) are abstract, while child(String) below ships with a default. Any existing third-party implementor of Log breaks — source and binary (AbstractMethodError) — on trace, but not on child. Log is @Experimental, so it's arguably in-bounds for 4.1.0, but the asymmetry looks unintentional. Could the trace methods get backward-compatible defaults — isTraceEnabled() returning false and the trace(…) overloads as no-ops — so existing implementations keep compiling and opt in by overriding? (DefaultLog overrides all of it, so the runtime path is unchanged.)
There was a problem hiding this comment.
Fixed — all 6 trace methods (isTraceEnabled + 5 overloads) now have default implementations: isTraceEnabled() returns false by default, and all trace(...) overloads are no-ops. This prevents AbstractMethodError for existing third-party Log implementors.
| * source method name is resolved by walking the stack past this class | ||
| * to find the first external caller frame. | ||
| */ | ||
| private void withMetadata(Runnable logAction) { |
There was a problem hiding this comment.
withMetadata runs a StackWalker.walk(…) on every enabled Log-API call to recover the caller method name. The isXxxEnabled() guards mean disabled levels are free, but INFO-level logging in a loop now pays a stack-walk per call. Worth a quick benchmark; alternatively make the source-method capture lazy/opt-in, since most appenders won't render it.
Minor, same method: Thread.currentThread().getId() is deprecated since Java 19 (Thread.threadId()), and the JUL path already uses the modern getLongThreadID() — aligning them would be consistent.
There was a problem hiding this comment.
Fixed — StackWalker is now conditional: if (ProjectBuildLogAppender.hasReportCapture()) before walking. The ~1-5μs per-call cost is only paid when build report capture is actually active. During normal builds, withMetadata() just sets the ThreadLocal with the logger name and thread ID (no stack walking).
| * | ||
| * @param mojoId the mojo identifier, or {@code null} to clear | ||
| */ | ||
| public static void setMojoId(String mojoId) { |
There was a problem hiding this comment.
Project id has a fork-aware restore via FORKING_PROJECT_ID (consulted in getProjectId()), but mojo id doesn't: mojoSucceeded/mojoFailed in LoggingExecutionListener call setMojoId(null), so when a forking mojo (e.g. a report goal that forks a lifecycle) resumes after its fork completes, maven.mojo.id has been cleared and is never restored — subsequent log lines from the forking mojo lose their MDC attribution. Should there be a symmetric FORKING_MOJO_ID (or a save/restore around the fork) mirroring the project-id handling?
Related: MOJO_ID is an InheritableThreadLocal; with reused pool threads a value not cleared on an exceptional path could inherit into a later, unrelated task.
There was a problem hiding this comment.
Fixed — added FORKING_MOJO_ID ThreadLocal mirroring the existing FORKING_PROJECT_ID pattern. LoggingExecutionListener.forkStarted() saves the current mojoId via setForkingMojoId(), and forkSucceeded()/forkFailed() clear it. Also fixed the cleanup ordering: setMojoId(null) now runs after delegate.mojoSucceeded/mojoFailed so the mojo context is available during the delegate callback, and setMojoId(null) restores the forking mojo's ID if one was saved.
| } | ||
|
|
||
| String loggerName = record.getLoggerName(); | ||
| org.slf4j.Logger slf4jLogger = LoggerFactory.getLogger(loggerName); |
There was a problem hiding this comment.
record.getLoggerName() can be null per the JUL spec; it's passed straight to LoggerFactory.getLogger(loggerName). Worth confirming that resolves to the root logger across SLF4J bindings rather than producing a literally "null"-named logger.
There was a problem hiding this comment.
Fixed — added a null guard: if (loggerName == null) { loggerName = ""; }. SLF4J's LoggerFactory.getLogger("") returns the root logger, which is safe.
| writeThrowable(t, sink); | ||
| StringBuilder full = new StringBuilder(formatted); | ||
| full.append(System.lineSeparator()); | ||
| appendFormattedThrowable(full, t, ""); |
There was a problem hiding this comment.
The new LogSink path hand-rolls throwable rendering (appendFormattedThrowable/appendStackTrace) while the non-sink path still goes through super.write(…) → writeThrowable. Two throwable renderings that can drift over time — could the sink path reuse the existing writeThrowable/printStackTrace (which still take Consumer<String>) to keep them single-sourced? Relatedly, MavenJulHandler.formatForConsole emits a bare "[LEVEL] message" that won't match this class's console layout, so JUL-origin lines look different from native lines on the sink path.
There was a problem hiding this comment.
Fixed — unified throwable rendering: the write() method now calls the existing writeThrowable() method instead of hand-rolling its own appendFormattedThrowable()/appendStackTrace(). Removed the duplicate methods entirely — single rendering path now.
| * @return the sequence number, always non-negative | ||
| * @since 4.1.0 | ||
| */ | ||
| default long sequenceNumber() { |
There was a problem hiding this comment.
sequenceNumber() javadoc says "always non-negative," but the default returns -1 and DefaultLogEvent passes -1 when unknown. Fix the doc (or the sentinel) so they agree.
There was a problem hiding this comment.
Fixed (same as @gnodet's comment above) — javadoc now reads @return the sequence number, or {@code -1} if unavailable.
gnodet
left a comment
There was a problem hiding this comment.
Well-designed logging infrastructure foundation with clean three-path convergence (Log API, JUL, SLF4J). The bug fix for warn(Supplier, Throwable) calling logger.info() is confirmed correct.
Findings:
-
[medium]
sequenceNumber()javadoc/contract mismatch —LogEvent.sequenceNumber()javadoc says@return the sequence number, always non-negativebut the default implementation returns-1. The sibling methodthreadId()correctly documentsor -1 if unavailablein its@returntag. ThesequenceNumber()javadoc should follow the same pattern for consistency. -
[medium] Inconsistent
formattedMessageformat between JUL and SLF4J — When aLogSinkis installed, JUL events'formattedMessageis built byformatForConsole()which produces a minimal[LEVEL] messagestring, while SLF4J events produce a full formatted string with timestamps, thread names, and logger names viaMavenBaseLogger.innerHandleNormalizedLoggingCall(). The practical impact is limited since the cleanmessage()field is available for consumers who need consistent content, but inSimpleBuildEventListener.projectLogMessage()which usesformattedMessage()for console output, JUL events will look noticeably different from SLF4J events. -
[low] Log4j2/Logback backend removal — The removal of
Log4j2Configuration,LogbackConfiguration, and thelogback-classicdependency means Maven no longer supports these as alternative SLF4J backends. This is intentional for the Maven 4.x logging redesign, but warrants mention in release notes for users who embedded Maven with a custom logging backend.
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
gnodet
left a comment
There was a problem hiding this comment.
Well-architected logging foundation PR. Clean design with proper ThreadLocal management, volatile concurrency handling, and good layering (API → impl → collector). A few items to address:
High severity:
-
API contract contradiction (
LogEvent.javaline 188):sequenceNumber()Javadoc says "@return the sequence number, always non-negative" but the default implementation returns-1. Compare withthreadId()which correctly documents "or -1 if unavailable". This is a public API interface marked@Experimental/@since 4.1.0— the Javadoc should match the actual contract. -
No test coverage: 1000+ lines of foundational code across 22 files with zero test files.
LogEvent/DefaultLogEvent,MavenJulHandler(249 lines),DefaultLog.withMetadata/trace/child,LogSinkinterface,ProjectBuildLogAppenderstructured event creation, and the mojo MDC lifecycle are all untested. The PR description mentions "580 tests pass" but these are all pre-existing tests.
Medium severity:
-
StackWalker overhead (
DefaultLog.javaline 649):withMetadata()callsStackWalker.walk()on every log call for enabled levels. While trace/debug are typically disabled and info/warn/error are low-volume, plugins logging many INFO/WARN messages will pay the 1-5μs per-call cost. -
Logger name change (
DefaultBuildPluginManager.javaline 128): Logger name changed fromgetFullGoalName()(e.g., "compiler:compile") togetImplementation()(e.g., "org.apache.maven.plugins.compiler.CompilerMojo"). Intentional for proper hierarchical SLF4J configuration, but a user-visible behavior change that could break existing SLF4J level configurations. -
Dead code for future PR (
ProjectBuildLogAppender.javaline 130):reportCapturevolatile field and setter are infrastructure for PR #12695 (build report). Currently unused in this PR — consider adding a brief comment noting the intent.
Low severity:
-
setMojoId(null)is called beforedelegate.mojoSucceeded/mojoFailedcallbacks, inconsistent with theforkSucceeded/forkFailedpattern where cleanup happens after the delegate. -
The bug fix changing
logger.info()tologger.warn()inwarn(Supplier<String>, Throwable)is correct and important. 👍
The removal of Logback/Log4j2 support is a significant architectural decision — worth explicit mention in release notes since users plugging in alternative SLF4J backends will lose that ability.
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
02ac855 to
812a842
Compare
Review feedback addressedAll 8 review comments from @gnodet and @ascheman have been addressed in the latest force-push. Summary of changes: Bug fixes
Design improvements
Tests added
All 6 downstream PRs (#12695, #12697, #12698, #12699, #12702, #12714) have been rebased onto the updated commit. |
812a842 to
84568d2
Compare
Apply review fixes from #12694 to align the backport: - Log.java: make all 6 trace methods default (no-ops) to prevent AbstractMethodError for existing third-party Log implementors. isTraceEnabled() returns false by default. - ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is called, the forking mojo's ID is restored instead of clearing. - LoggingExecutionListener: save current mojoId in forkStarted(), clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup ordering in mojoSucceeded/mojoFailed — delegate runs first, then MDC is cleared. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Backport Log API enhancements and mojo MDC to 4.0.x
Backport four Log-related improvements from master to the 4.0.x branch
for inclusion in rc-7:
1. Log.trace() — new trace level (maps to SLF4J TRACE / JUL FINEST)
to separate Maven core internals from user-facing debug messages.
Currently -X floods debug output with resolver/interpolation details
that drown user-relevant diagnostics.
2. Log.child(name) — creates a sub-logger with an independently
filterable name (e.g. "CompilerMojo.diagnostics"), letting plugin
sub-components log under their own namespace.
3. Logger name alignment — Maven 4 Log now uses the mojo implementation
class name (e.g. "org.apache.maven.plugins.compiler.CompilerMojo")
instead of the goal name ("compiler:compile"). This matches what
Maven 3 mojos already use and enables standard SLF4J hierarchical
level configuration.
4. Mojo MDC propagation — sets "maven.mojo.id" (prefix:goal@executionId)
in the SLF4J MDC during mojo execution. All log messages — including
those arriving through the JUL-to-SLF4J bridge — now carry mojo
context, available to any SLF4J appender via %X{maven.mojo.id}.
Also fixes a pre-existing bug in DefaultLog where warn(Supplier, Throwable)
incorrectly delegated to logger.info() instead of logger.warn().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add isXxxEnabled() guards to Throwable-only log overloads
Align with master by wrapping the five xxx(Throwable) overloads
in level-enabled checks, avoiding unnecessary method calls and
empty string construction when the level is disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address review: default trace methods and fork-aware mojoId
Apply review fixes from #12694 to align the backport:
- Log.java: make all 6 trace methods default (no-ops) to prevent
AbstractMethodError for existing third-party Log implementors.
isTraceEnabled() returns false by default.
- ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring
the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is
called, the forking mojo's ID is restored instead of clearing.
- LoggingExecutionListener: save current mojoId in forkStarted(),
clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup
ordering in mojoSucceeded/mojoFailed — delegate runs first, then
MDC is cleared.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address review: add DefaultLogTest and clear MDC on mojoSkipped
- Add DefaultLogTest with 5 tests: warn/supplier regression,
trace delegation, trace no-op guard, child() sub-logger,
and default trace methods (AbstractMethodError prevention).
- Clear mojo MDC in mojoSkipped() to prevent stale mojo context
from leaking into subsequent log messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
84568d2 to
42cde86
Compare
* Log API enhancements and mojo MDC (#12690) * Backport Log API enhancements and mojo MDC to 4.0.x Backport four Log-related improvements from master to the 4.0.x branch for inclusion in rc-7: 1. Log.trace() — new trace level (maps to SLF4J TRACE / JUL FINEST) to separate Maven core internals from user-facing debug messages. Currently -X floods debug output with resolver/interpolation details that drown user-relevant diagnostics. 2. Log.child(name) — creates a sub-logger with an independently filterable name (e.g. "CompilerMojo.diagnostics"), letting plugin sub-components log under their own namespace. 3. Logger name alignment — Maven 4 Log now uses the mojo implementation class name (e.g. "org.apache.maven.plugins.compiler.CompilerMojo") instead of the goal name ("compiler:compile"). This matches what Maven 3 mojos already use and enables standard SLF4J hierarchical level configuration. 4. Mojo MDC propagation — sets "maven.mojo.id" (prefix:goal@executionId) in the SLF4J MDC during mojo execution. All log messages — including those arriving through the JUL-to-SLF4J bridge — now carry mojo context, available to any SLF4J appender via %X{maven.mojo.id}. Also fixes a pre-existing bug in DefaultLog where warn(Supplier, Throwable) incorrectly delegated to logger.info() instead of logger.warn(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add isXxxEnabled() guards to Throwable-only log overloads Align with master by wrapping the five xxx(Throwable) overloads in level-enabled checks, avoiding unnecessary method calls and empty string construction when the level is disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review: default trace methods and fork-aware mojoId Apply review fixes from #12694 to align the backport: - Log.java: make all 6 trace methods default (no-ops) to prevent AbstractMethodError for existing third-party Log implementors. isTraceEnabled() returns false by default. - ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is called, the forking mojo's ID is restored instead of clearing. - LoggingExecutionListener: save current mojoId in forkStarted(), clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup ordering in mojoSucceeded/mojoFailed — delegate runs first, then MDC is cleared. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review: add DefaultLogTest and clear MDC on mojoSkipped - Add DefaultLogTest with 5 tests: warn/supplier regression, trace delegation, trace no-op guard, child() sub-logger, and default trace methods (AbstractMethodError prevention). - Clear mojo MDC in mojoSkipped() to prevent stale mojo context from leaking into subsequent log messages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Address review: javadoc wording in trace methods - "in the trace error level" → "at the trace error level" - "e.g." → "for instance," Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
42cde86 to
93accfb
Compare
b1f7e4c to
d670691
Compare
d670691 to
79328b4
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
All 8 findings from previous reviews have been verified as addressed in the latest commits:
sequenceNumber()javadoc — fixed: now reads@return the sequence number, or {@code -1} if unavailableformatForConsole()inconsistency — fixed: removed entirely; all JUL events now route through SLF4J for consistentformattedMessageoutputLog.trace()backward compatibility — fixed: all 6 trace methods aredefaultno-ops, preventingAbstractMethodErrorfor existing implementors- StackWalker overhead — fixed: gated behind
ProjectBuildLogAppender.hasReportCapture(), zero cost during normal builds - Fork-aware mojoId — fixed:
FORKING_MOJO_IDThreadLocal added, mirroringFORKING_PROJECT_IDsave/restore pattern inforkStarted/forkSucceeded/forkFailed - Null
loggerNameguard — fixed:MavenJulHandler.publish()now falls back to""for null logger names per JUL spec - Throwable rendering unified — fixed:
MavenSimpleLogger.write()reuseswriteThrowable(), removed duplicateappendFormattedThrowable/appendStackTrace mojoSucceeded/mojoFailedcleanup ordering — fixed:setMojoId(null)now runs afterdelegate.mojoSucceeded/mojoFailedcallbacks
The ThreadLocal lifecycle management is correct throughout (always in try/finally blocks). The ConcurrentHashMap.computeIfAbsent reentrancy mitigation in MavenJulHandler (using putIfAbsent + IllegalStateException catch) is well-designed. Test coverage hits the key regression points (warn/Supplier bug, JUL level mapping, metadata lifecycle).
This review was generated by an AI agent, Hermès on behalf of @gnodet.
The root cause was an ordering bug in doInvoke():
configureLogging(context); // sets desired log level, not yet applied
createTerminal(context); // starts FastTerminal background thread →
// JLine TerminalBuilder runs and emits
// DEBUG via SLF4J while level = INFO still
activateLogging(context); // activate() finally applies ERROR in quiet
FastTerminal spawns a daemon thread that calls TerminalBuilder.build()
immediately. TerminalBuilder logs DEBUG messages (provider discovery,
terminal type selection) via SLF4J. Because slf4jConfiguration.activate()
had not been called yet, these loggers were initialised with the default
INFO level rather than the quiet-mode ERROR level, leaking output that
MavenITmng4387QuietLoggingTest detects as a flaky failure on Windows CI.
Fix: move activateLogging() before createTerminal() so that SLF4J logger
levels are fully applied before the JLine background thread starts.
As a secondary hardening, also scope the JUL root logger level set in
activateLogging() to match the effective Maven log level instead of
unconditionally opening it to Level.ALL (quiet → WARNING, verbose → ALL,
default → INFO), which prevents a similar race for JUL-sourced events.
b4f3456 to
f8d3a2e
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit f8d3a2e ("Fix MavenITmng4387 flakiness: activate logging before creating terminal").
The root-cause analysis in the commit message is correct and the primary fix (swapping activateLogging() before createTerminal()) eliminates the race at its source. One correctness gap in the secondary hardening and a missing test.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| // MavenITmng4387QuietLoggingTest detects as a flaky failure. | ||
| java.util.logging.Level julRootLevel; | ||
| if (context.options().quiet().orElse(false)) { | ||
| julRootLevel = java.util.logging.Level.WARNING; |
There was a problem hiding this comment.
[medium] JUL quiet level should be SEVERE, not WARNING
In quiet mode, SLF4J is configured at ERROR level. The stated goal of the JUL root level assignment is to suppress events at source so that the brief window where newly-created SLF4J loggers are still at their default INFO level doesn't leak output. Using Level.WARNING only blocks INFO/DEBUG/FINE JUL events — WARNING events (JUL integer value 900) still pass the root filter and reach MavenJulHandler.publish(). Inside publish(), they hit isLevelEnabled(), which during that brief race window may see the SLF4J logger at INFO (not yet propagated to ERROR), allowing the WARNING event through to the console — exactly the flakiness pattern this fix is trying to prevent.
Level.SEVERE (integer value 1000) is the JUL equivalent of SLF4J ERROR and matches the quiet-mode intent precisely: only SEVERE-and-above JUL events pass the root filter.
| julRootLevel = java.util.logging.Level.WARNING; | |
| julRootLevel = java.util.logging.Level.SEVERE; |
There was a problem hiding this comment.
Fixed in 2fc5fc8: quiet-mode JUL root level changed from WARNING to SEVERE (the correct JUL equivalent of SLF4J ERROR). Updated the comment accordingly.
| } else { | ||
| julRootLevel = java.util.logging.Level.INFO; | ||
| } | ||
| java.util.logging.LogManager.getLogManager().getLogger("").setLevel(julRootLevel); |
There was a problem hiding this comment.
[low] Update comment to reference SEVERE instead of WARNING
If Level.SEVERE is adopted above, the comment on line 460 ("keep JUL root at WARNING") should be updated to say SEVERE to stay accurate.
There was a problem hiding this comment.
Fixed in 9cc0d82. The comment on line 460 now reads keep the JUL root at SEVERE so that WARNING/INFO/DEBUG — updated together with the Level.WARNING → Level.SEVERE change.
Two related issues caused flaky/broken behaviour in the logging foundation: 1. MavenStyleResolver reentrancy crash (IllegalStateException: Recursive update) During FastTerminal background thread initialization, JLine's TerminalBuilder logs via JUL (e.g. debug messages from StyleResolver.resolve()). MavenJulHandler routes these to SLF4J, which calls MavenSimpleLogger.renderLevel() to format the level prefix. renderLevel() lazily initialises styled level strings by calling JlineMessageBuilder.style() → MavenStyleResolver.resolve(), which enters computeIfAbsent on the styles ConcurrentHashMap — but the same map is already inside a computeIfAbsent on the same thread (from the outer StyleResolver.resolve() call that triggered the JUL debug event). ConcurrentHashMap detects the recursive update and throws IllegalStateException, crashing Maven. Fix: catch IllegalStateException in MavenStyleResolver.resolve() and return AttributedStyle.DEFAULT as a fallback. Subsequent calls (after lazy init completes on the next invocation) will hit the populated cache normally. 2. JUL root logger level not scoped to effective log level (MavenITmng4387 flakiness) activateLogging() unconditionally set the JUL root to Level.ALL, even in quiet mode (-q). While MavenJulHandler.publish() checks isLevelEnabled() via SLF4J, this check is racy: SLF4J loggers are initialized lazily, and a newly created logger may briefly see the default INFO level before quiet-mode propagation completes. During that window, INFO JUL events slip through to the output, causing MavenITmng4387QuietLoggingTest to fail intermittently. Fix: set the JUL root level to match the effective Maven log level: - quiet (-q): WARNING — stops INFO/DEBUG JUL events at source - verbose (-X): ALL — allows FINE/FINEST events for debug output - default: INFO — consistent with the JUL root default
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 1eefbf0 ("Fix MavenITmng4387 flakiness: guard JUL root level + CHM reentrancy").
The IN_PUBLISH reentrancy guard, the MavenStyleResolver.resolve() catch block, and the pendingEarlyLogs deferred-drain are all correct. One finding carried over from the previous review that was not addressed.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| // MavenITmng4387QuietLoggingTest detects as a flaky failure. | ||
| java.util.logging.Level julRootLevel; | ||
| if (context.options().quiet().orElse(false)) { | ||
| julRootLevel = java.util.logging.Level.WARNING; |
There was a problem hiding this comment.
[medium] Quiet-mode JUL root level should be SEVERE, not WARNING (carried over from previous review — still unaddressed)
In quiet mode Maven sets SLF4J to ERROR level (Slf4jConfiguration.Level.ERROR, line 296). The stated goal of this JUL root level assignment is to suppress events at source so that newly-created SLF4J loggers (briefly at INFO before propagation) don't leak output. But Level.WARNING (integer 900) blocks only INFO/DEBUG/FINE JUL events — WARNING-and-above events still pass the root filter and reach MavenJulHandler.publish(). During the race window where a new SLF4J logger hasn't had ERROR propagated yet, a JUL WARNING event will slip through isLevelEnabled() (which sees INFO) and appear on console — exactly the flakiness pattern this is trying to fix.
Level.SEVERE (integer 1000) is the correct JUL equivalent of SLF4J ERROR. Update the code and the comment above:
| julRootLevel = java.util.logging.Level.WARNING; | |
| julRootLevel = java.util.logging.Level.SEVERE; |
In quiet mode, SLF4J is configured at ERROR level. The JUL root level must match: SEVERE (integer 1000) is the correct JUL equivalent of SLF4J ERROR. Using WARNING (integer 900) allowed WARNING-and-above JUL events to pass the root filter and reach MavenJulHandler.publish(), where they could slip through the isLevelEnabled() check during the race window before quiet-mode ERROR propagation completes — exactly the flakiness pattern this guard was introduced to prevent.
| if (context.pendingEarlyLogs != null) { | ||
| context.pendingEarlyLogs.forEach(e -> context.logger.log(e.level(), e.message(), e.error())); | ||
| context.pendingEarlyLogs = null; |
There was a problem hiding this comment.
[high] pendingEarlyLogs drain is dead code — activateLogging() has not run yet when this executes
doInvoke() calls createTerminal() at line 159, then activateLogging() at line 160. The drain block below executes at the end of createTerminal(), but context.pendingEarlyLogs is set by activateLogging() (line 518), which hasn't been called yet. So context.pendingEarlyLogs is always null here, the if branch is never taken, and the MavenITmng6065 fix (early messages reaching the log file) is completely non-functional.
The fix has two correct options:
Option A — move the drain to the start of activateLogging() after installing ProjectBuildLogAppender (but that's the wrong place since the appender is installed in createTerminal()).
Option B — move the drain call to occur after activateLogging() returns, i.e. call it from doInvoke() directly:
protected int doInvoke(C context) throws Exception {
...
configureLogging(context);
createTerminal(context); // installs ProjectBuildLogAppender / logSink
activateLogging(context); // sets context.pendingEarlyLogs
// Now that both the logSink AND pendingEarlyLogs are set, drain:
if (context.pendingEarlyLogs != null) {
context.pendingEarlyLogs.forEach(e -> context.logger.log(e.level(), e.message(), e.error()));
context.pendingEarlyLogs = null;
}
helpOrVersionAndMayExit(context);
...
}The drain block currently in createTerminal() should be removed.
There was a problem hiding this comment.
Fixed in 63ed04e. Moved the drain into activateLogging() (after the context.logger.drain() / logger swap), where createTerminal() has already installed ProjectBuildLogAppender. The dead drain block in createTerminal() has been removed, and the pendingEarlyLogs field in LookupContext is gone too.
| * | ||
| * @param content the message supplier | ||
| */ | ||
| default void trace(Supplier<String> content) {} |
There was a problem hiding this comment.
[low] Missing @param content tag — inconsistent with all other overloads
The diff removes @param content the message supplier from trace(Supplier<String> content). Every other supplier-based method in this interface (debug, info, warn, error) retains its @param content and @param error tags. The removal makes the Javadoc incomplete and inconsistent.
| default void trace(Supplier<String> content) {} | |
| /** | |
| * Sends a lazily-computed message at the <b>trace</b> error level. | |
| * The supplier is only evaluated if trace is enabled. | |
| * <p> | |
| * The default implementation is a no-op for backward compatibility | |
| * with existing {@code Log} implementations. | |
| * | |
| * @param content the message supplier | |
| */ | |
| default void trace(Supplier<String> content) {} |
There was a problem hiding this comment.
Fixed in 63ed04e. Added @param content the message supplier to the Javadoc for trace(Supplier<String>).
| * @param content the message supplier | ||
| * @param error the error that caused this log | ||
| */ | ||
| default void trace(Supplier<String> content, Throwable error) {} |
There was a problem hiding this comment.
[low] Missing @param content and @param error tags — inconsistent with all other overloads
Same issue: the diff removes both @param tags from trace(Supplier<String>, Throwable). Every other supplier+throwable method in this interface documents both parameters.
| default void trace(Supplier<String> content, Throwable error) {} | |
| /** | |
| * Sends a lazily-computed message (and accompanying exception) at the <b>trace</b> error level. | |
| * The supplier is only evaluated if trace is enabled. | |
| * <p> | |
| * The default implementation is a no-op for backward compatibility | |
| * with existing {@code Log} implementations. | |
| * | |
| * @param content the message supplier | |
| * @param error the error that caused this log | |
| */ | |
| default void trace(Supplier<String> content, Throwable error) {} |
There was a problem hiding this comment.
Fixed in 63ed04e. Added both @param content the message supplier and @param error the error that caused this log to the Javadoc for trace(Supplier<String>, Throwable).
…createTerminal(); add missing @PARAM tags
…nStyleResolver
ConcurrentHashMap.computeIfAbsent throws IllegalStateException("Recursive update")
when called re-entrantly on the same map from the same thread — even for different
keys, and even when the nested call is wrapped in try/catch (because the nested
call's exception corrupts the outer call's bin-reservation state, causing the outer
computeIfAbsent to also throw).
The reentrancy chain when running ITs with -X (debug mode, JUL root level = ALL):
outer computeIfAbsent(spec, doResolve)
-> doResolve() -> super.resolve() -> JLine StyleResolver.resolve()
-> System.Logger TRACE -> JUL -> MavenJulHandler.publish()
-> IN_PUBLISH not yet set (first entry) -> logToSlf4j()
-> MavenSimpleLogger.renderLevel() -> style() -> resolve()
-> inner computeIfAbsent(spec, doResolve) → ISE("Recursive update")
-> exception propagates through outer computeIfAbsent bin state -> outer also throws
The try/catch(IllegalStateException) in resolve() was not sufficient because the
inner exception corrupts the outer computeIfAbsent's ReservationNode state, causing
a second ISE from the outer call that bypasses the catch.
Fix: replace computeIfAbsent with the lock-free get+compute+putIfAbsent pattern.
This avoids holding the CHM bin lock during doResolve(), so any re-entrant resolve()
call runs independently without triggering the reentrancy detection. At worst, two
threads compute the same style and one result is discarded — harmless for a cache.
Also move IN_PUBLISH.set() before METADATA.set() in MavenJulHandler.publish() for
consistency (both are set in the same guarded section).
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commits 63ed04e ("Fix MavenITmng6065: drain pendingEarlyLogs in activateLogging()") and caeeeb4 ("Fix IT failures: replace computeIfAbsent with get+putIfAbsent in MavenStyleResolver").
Both findings from the previous REQUEST_CHANGES review are confirmed addressed:
-
pendingEarlyLogsdrain — correctly moved intoactivateLogging()(aftercreateTerminal()has installedProjectBuildLogAppender), captured beforecontext.loggeris swapped, then replayed through the new logger. The ordering is now correct: drain → swap → replay. -
Missing
@paramtags —trace(Supplier<String> content)andtrace(Supplier<String> content, Throwable error)both now carry their@paramtags matching the other overloads.
Three low-severity observations (not blocking merge):
-
publishIsReentrantSafetest comment — the Javadoc says "Install a custom SLF4J logger that fires a second JUL event" but the test never does that; it only externally setsIN_PUBLISH=truethen callspublish(). The assertion is correct and the guard is verified, but the comment misleads future readers about what scenario is actually being tested. -
DefaultLog.child(name)blank-name guard —requireNonNullprevents null but not blank. The previous interface@paramsaid "must not benullor blank"; the updated doc just says "must not benull". A blank string produces a trailing-dot logger name ("com.example.Mojo. "). Harmless in practice but worth aPreconditions.checkArgument(!name.isBlank(), ...)if the contract is meant to exclude blanks. -
LogEvent.formattedMessage()Javadoc — says "the fully formatted log line" but when a throwable is present,MavenSimpleLogger.write()appends the ANSI-styled stack trace, soformattedMessageis actually a multi-line block. The doc could say "including any throwable rendering" to be precise.
None of these block this PR.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
…el() setRootLoggerLevel() sets the system property maven.logger.defaultLogLevel but CONFIG_PARAMS.defaultLogLevel is only updated by reconfigure() (called from activate()). The FastTerminal background thread starts during createTerminal(), before activate() runs. Loggers created on that thread read CONFIG_PARAMS.defaultLogLevel at construction time and get currentLogLevel=INFO instead of ERROR, allowing JLine [DEBUG] messages (from TerminalBuilder.build()) to pass isDebugEnabled() and leak into the log output — consistently on Windows. Fix: call reconfigure() eagerly in setRootLoggerLevel() so that CONFIG_PARAMS.defaultLogLevel is updated in-memory before the background thread starts. activate() calling reconfigure() a second time is harmless.
…ing() The FastTerminal background thread (started during createTerminal()) can fire JUL FINE/FINER events from TerminalBuilder and ProcessBuilder while MavenJulHandler is being installed in activateLogging(). On Windows, ExecTerminalProvider probes for POSIX streams by running external programs (test.exe, tty.exe) via ProcessBuilder, which JDK 21+ logs at FINE level. TerminalBuilder itself logs 'Available providers', 'Using terminal', and 'Grapheme cluster mode' at FINE level via JLine's Log.debug(). Previous fixes tried to guard at the SLF4J layer (reconfigure() in setRootLoggerLevel(), CHM putIfAbsent in MavenStyleResolver) but those are inherently racy: loggers created on the background thread between setRootLoggerLevel() and activate()/reconfigure() may briefly see the pre-quiet defaultLogLevel and pass isDebugEnabled(). The correct fix is to close the gate at JUL source — before createTerminal() starts the background thread. Set JUL root to SEVERE (for -q) or INFO (for normal) in configureLogging(), which runs before createTerminal(). The Thread.start() call in FastTerminal establishes a happens-before edge, so the background thread is guaranteed to see the updated JUL root level. SEVERE (1000) blocks FINE (500) at the JUL root, so no FINE events can reach MavenJulHandler regardless of when it is installed or when reconfigure() runs. For verbose mode (-X), JUL root is still set to ALL in activateLogging() after SLF4J is fully bootstrapped — setting ALL before MavenJulHandler is installed would flood events through the default ConsoleHandler and risk CHM reentrancy during SLF4J logger initialization. Also revert the setRootLoggerLevel() reconfigure() call added in the previous commit — it was compensating for the missing early JUL gate and is no longer needed.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commits 0c6eb24f ("Fix MavenITmng4387 Windows: eagerly reconfigure() in setRootLoggerLevel()") and 65cff258 ("Fix MavenITmng4387 Windows: set JUL root level early in configureLogging()").
Both commits are correct. Moving the JUL root level assignment for quiet/normal modes from activateLogging() into configureLogging() (before createTerminal() starts the FastTerminal background thread) closes the racy window cleanly. The verbose path staying deferred to activateLogging() is correct — setting ALL before MavenJulHandler is installed would flood the default ConsoleHandler. The if/else reorder in activateLogging() (verbose-first) is also cleaner. No new issues.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
Summary
Master-only logging infrastructure additions, layered on top of the forward-port (#12929) of the shared Log API enhancements from 4.0.x (#12690).
What's included (master-only)
Structured LogEvent (
maven-api-core,maven-core)LogEvent/LogLevelAPI for structured log event representationsourceClassName(),sourceMethodName(),threadId()— populated for both Log API events (viaDefaultLog.withMetadata()) and JUL events (viaMavenJulHandler);null/-1for direct SLF4J loggingCustom JUL Handler (
maven-logging)MavenJulHandlerreplacesSLF4JBridgeHandler— all JUL events always route through SLF4J so thatMavenSimpleLoggerproduces a consistentformattedMessage(with timestamp, logger name, and ANSI styling) regardless of originsourceClassName,sourceMethodName,threadId) is stashed in a ThreadLocal before the SLF4J call and read byProjectBuildLogAppenderduring the same synchronous call chain — no metadata is lostloggerNameguard per JUL spec (falls back to root logger)MavenJulHandler→ SLF4J — all converge on the same structuredLogEventStructured LogSink (
maven-logging,maven-core)MavenSimpleLogger.LogSink— structured callback with(level, loggerName, cleanMessage, formattedMessage, throwable)replacing the oldConsumer<String>sinkwrite()reuses the existingwriteThrowable()method instead of duplicating rendering logicProjectBuildLogAppenderproducesLogEventobjects (with source metadata when available) instead of raw stringsBuildEventListener.projectLogMessage()now takesLogEventinstead ofStringLog API metadata (
maven-core)DefaultLog.withMetadata()— captures caller class/method/thread via StackWalker, gated behindProjectBuildLogAppender.hasReportCapture()for zero overhead in normal builds (~1-5μs per-call cost only when build report capture is active)Shared with 4.0.x (via forward-port #12929)
The following changes are in the forward-port commit and are not duplicated in this PR's diff:
Log.trace()— default no-op methods preventingAbstractMethodErrorLog.child(name)— hierarchical sub-loggersmaven.mojo.id) — fork-aware save/restoreDefaultLogwarn bug fix +isXxxEnabled()guardsDefaultLogTest— 6 tests (warn regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compat)PR chain
mvnlogviewerRelated
maven-4.0.x(milestone 4.1.0)Test plan
DefaultLogTest— 6 tests: warn/supplier regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compatMavenJulHandlerTest— 10 tests: parameterized JUL→SLF4J level mapping, FINEST→TRACE, CONFIG→INFO, metadata null checkmvn test -pl impl/maven-core,impl/maven-logging— all tests pass🤖 Generated with Claude Code