Skip to content

Logging foundation: structured LogEvent, JUL handler, Log API enhancements - #12694

Open
gnodet wants to merge 9 commits into
masterfrom
feature/logging-foundation
Open

gnodet wants to merge 9 commits into
masterfrom
feature/logging-foundation

Conversation

@gnodet

@gnodet gnodet commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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 / LogLevel API for structured log event representation
  • Source metadata fields: sourceClassName(), sourceMethodName(), threadId() — populated for both Log API events (via DefaultLog.withMetadata()) and JUL events (via MavenJulHandler); null/-1 for direct SLF4J logging

Custom JUL Handler (maven-logging)

  • MavenJulHandler replaces SLF4JBridgeHandler — all JUL events always route through SLF4J so that MavenSimpleLogger produces a consistent formattedMessage (with timestamp, logger name, and ANSI styling) regardless of origin
  • JUL metadata (sourceClassName, sourceMethodName, threadId) is stashed in a ThreadLocal before the SLF4J call and read by ProjectBuildLogAppender during the same synchronous call chain — no metadata is lost
  • Null loggerName guard per JUL spec (falls back to root logger)
  • Three sources, one pipeline: Log API → SLF4J, SLF4J direct, JUL → MavenJulHandler → SLF4J — all converge on the same structured LogEvent

Structured LogSink (maven-logging, maven-core)

  • MavenSimpleLogger.LogSink — structured callback with (level, loggerName, cleanMessage, formattedMessage, throwable) replacing the old Consumer<String> sink
  • Throwable rendering unified: write() reuses the existing writeThrowable() method instead of duplicating rendering logic
  • ProjectBuildLogAppender produces LogEvent objects (with source metadata when available) instead of raw strings
  • BuildEventListener.projectLogMessage() now takes LogEvent instead of String

Log API metadata (maven-core)

  • DefaultLog.withMetadata() — captures caller class/method/thread via StackWalker, gated behind ProjectBuildLogAppender.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 preventing AbstractMethodError
  • Log.child(name) — hierarchical sub-loggers
  • Mojo MDC propagation (maven.mojo.id) — fork-aware save/restore
  • Logger name alignment (FQCN instead of goal name)
  • DefaultLog warn bug fix + isXxxEnabled() guards
  • DefaultLogTest — 6 tests (warn regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compat)

PR chain

# PR Feature
0 #12929 Forward-port of 4.0.x Log API enhancements
1 This PR Logging foundation (master-only additions)
2 #12695 Build report
3 #12697 Console modes
4 #12698 Warning mode + diagnostics
5 #12699 mvnlog viewer
6 #12702 Structured problems pipeline
7 #12714 TRACE level migration

Related

Test plan

  • DefaultLogTest — 6 tests: warn/supplier regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compat
  • MavenJulHandlerTest — 10 tests: parameterized JUL→SLF4J level mapping, FINEST→TRACE, CONFIG→INFO, metadata null check
  • mvn test -pl impl/maven-core,impl/maven-logging — all tests pass
  • Full CI validation
  • IT suite with JUL-using plugins (verify metadata preservation)

🤖 Generated with Claude Code

@gnodet
gnodet force-pushed the feature/logging-foundation branch from bae1db9 to 5a5af1e Compare August 8, 2026 01:19
gnodet added a commit that referenced this pull request Aug 8, 2026
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>
@gnodet
gnodet force-pushed the feature/logging-foundation branch from e64db31 to de8044a Compare August 8, 2026 12:14
gnodet added a commit that referenced this pull request Aug 8, 2026
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>
gnodet added a commit that referenced this pull request Aug 8, 2026
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>
gnodet added a commit that referenced this pull request Aug 8, 2026
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>
@gnodet
gnodet force-pushed the feature/logging-foundation branch from 8baa65a to 02ac855 Compare August 9, 2026 08:11
@gnodet
gnodet marked this pull request as ready for review August 9, 2026 08:11

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) calling logger.info() instead of logger.warn().
  • The logger name change from getFullGoalName() to getImplementation() (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;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Suggested change
}
* @return the sequence number, or {@code -1} if unavailable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — javadoc now reads @return the sequence number, or {@code -1} if unavailable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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}.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 ascheman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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, "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed (same as @gnodet's comment above) — javadoc now reads @return the sequence number, or {@code -1} if unavailable.

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

  1. [medium] sequenceNumber() javadoc/contract mismatchLogEvent.sequenceNumber() javadoc says @return the sequence number, always non-negative but the default implementation returns -1. The sibling method threadId() correctly documents or -1 if unavailable in its @return tag. The sequenceNumber() javadoc should follow the same pattern for consistency.

  2. [medium] Inconsistent formattedMessage format between JUL and SLF4J — When a LogSink is installed, JUL events' formattedMessage is built by formatForConsole() which produces a minimal [LEVEL] message string, while SLF4J events produce a full formatted string with timestamps, thread names, and logger names via MavenBaseLogger.innerHandleNormalizedLoggingCall(). The practical impact is limited since the clean message() field is available for consumers who need consistent content, but in SimpleBuildEventListener.projectLogMessage() which uses formattedMessage() for console output, JUL events will look noticeably different from SLF4J events.

  3. [low] Log4j2/Logback backend removal — The removal of Log4j2Configuration, LogbackConfiguration, and the logback-classic dependency 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 added a commit to gnodet/maven that referenced this pull request Aug 17, 2026
@gnodet gnodet added this to the 4.1.0 milestone Aug 23, 2026

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

  1. API contract contradiction (LogEvent.java line 188): sequenceNumber() Javadoc says "@return the sequence number, always non-negative" but the default implementation returns -1. Compare with threadId() 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.

  2. No test coverage: 1000+ lines of foundational code across 22 files with zero test files. LogEvent/DefaultLogEvent, MavenJulHandler (249 lines), DefaultLog.withMetadata/trace/child, LogSink interface, ProjectBuildLogAppender structured 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:

  1. StackWalker overhead (DefaultLog.java line 649): withMetadata() calls StackWalker.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.

  2. Logger name change (DefaultBuildPluginManager.java line 128): Logger name changed from getFullGoalName() (e.g., "compiler:compile") to getImplementation() (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.

  3. Dead code for future PR (ProjectBuildLogAppender.java line 130): reportCapture volatile 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:

  1. setMojoId(null) is called before delegate.mojoSucceeded/mojoFailed callbacks, inconsistent with the forkSucceeded/forkFailed pattern where cleanup happens after the delegate.

  2. The bug fix changing logger.info() to logger.warn() in warn(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

@gnodet
gnodet force-pushed the feature/logging-foundation branch from 02ac855 to 812a842 Compare August 28, 2026 09:28
@gnodet

gnodet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

All 8 review comments from @gnodet and @ascheman have been addressed in the latest force-push. Summary of changes:

Bug fixes

  • sequenceNumber() javadoc — corrected from "always non-negative" to "or -1 if unavailable"
  • warn(Supplier, Throwable) regression — was calling logger.info() instead of logger.warn() (added regression test)

Design improvements

  • MavenJulHandler simplified — removed formatForConsole() entirely; all JUL events now always route through SLF4J so MavenSimpleLogger produces a consistent formattedMessage regardless of origin
  • Log.trace() backward compatibility — all 6 trace methods changed from abstract to default implementations (no-ops) to prevent AbstractMethodError for existing third-party Log implementors
  • StackWalker gatedwithMetadata() only walks the stack when ProjectBuildLogAppender.hasReportCapture() is true; normal builds pay zero StackWalker cost
  • Fork-aware mojoId — added FORKING_MOJO_ID ThreadLocal mirroring FORKING_PROJECT_ID; forkStarted()/forkSucceeded()/forkFailed() save and restore mojo context
  • Throwable rendering unifiedMavenSimpleLogger.write() now reuses the existing writeThrowable() method; removed the duplicate appendFormattedThrowable()/appendStackTrace() methods
  • Null loggerName guardMavenJulHandler.publish() now handles null logger names per JUL spec

Tests added

  • DefaultLogTest (4 tests): warn/supplier regression, metadata lifecycle, trace delegation, trace no-op
  • MavenJulHandlerTest (10 tests): parameterized JUL→SLF4J level mapping, FINEST→TRACE, CONFIG→INFO, metadata null check

All 6 downstream PRs (#12695, #12697, #12698, #12699, #12702, #12714) have been rebased onto the updated commit.

@gnodet
gnodet force-pushed the feature/logging-foundation branch from 812a842 to 84568d2 Compare August 28, 2026 09:55
gnodet added a commit that referenced this pull request Aug 29, 2026
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>
@gnodet gnodet mentioned this pull request Aug 29, 2026
2 tasks
gnodet added a commit that referenced this pull request Aug 30, 2026
* 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>
@gnodet
gnodet force-pushed the feature/logging-foundation branch from 84568d2 to 42cde86 Compare August 30, 2026 09:35
gnodet added a commit that referenced this pull request Aug 31, 2026
* 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>
@gnodet
gnodet force-pushed the feature/logging-foundation branch from 42cde86 to 93accfb Compare August 31, 2026 08:55
@gnodet
gnodet force-pushed the feature/logging-foundation branch 3 times, most recently from b1f7e4c to d670691 Compare September 7, 2026 19:24
@gnodet
gnodet force-pushed the feature/logging-foundation branch from d670691 to 79328b4 Compare September 8, 2026 05:02

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All 8 findings from previous reviews have been verified as addressed in the latest commits:

  1. sequenceNumber() javadoc — fixed: now reads @return the sequence number, or {@code -1} if unavailable
  2. formatForConsole() inconsistency — fixed: removed entirely; all JUL events now route through SLF4J for consistent formattedMessage output
  3. Log.trace() backward compatibility — fixed: all 6 trace methods are default no-ops, preventing AbstractMethodError for existing implementors
  4. StackWalker overhead — fixed: gated behind ProjectBuildLogAppender.hasReportCapture(), zero cost during normal builds
  5. Fork-aware mojoId — fixed: FORKING_MOJO_ID ThreadLocal added, mirroring FORKING_PROJECT_ID save/restore pattern in forkStarted/forkSucceeded/forkFailed
  6. Null loggerName guard — fixed: MavenJulHandler.publish() now falls back to "" for null logger names per JUL spec
  7. Throwable rendering unified — fixed: MavenSimpleLogger.write() reuses writeThrowable(), removed duplicate appendFormattedThrowable/appendStackTrace
  8. mojoSucceeded/mojoFailed cleanup ordering — fixed: setMojoId(null) now runs after delegate.mojoSucceeded/mojoFailed callbacks

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.
@gnodet
gnodet force-pushed the feature/logging-foundation branch from b4f3456 to f8d3a2e Compare September 14, 2026 21:03

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Suggested change
julRootLevel = java.util.logging.Level.WARNING;
julRootLevel = java.util.logging.Level.SEVERE;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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:

Suggested change
julRootLevel = java.util.logging.Level.WARNING;
julRootLevel = java.util.logging.Level.SEVERE;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2fc5fc8.

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.
…ookupContext

The pendingEarlyLogs javadoc used {@link #createTerminal()} which resolves
against LookupContext itself — that method doesn't exist there, it lives on
LookupInvoker.  Replace with plain @code references to fix the javadoc build.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review of commit 3752425 ("Fix javadoc: remove broken @link to LookupInvoker#createTerminal in LookupContext").

Two findings. The MavenITmng6065 fix is non-functional.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment on lines +341 to +343
if (context.pendingEarlyLogs != null) {
context.pendingEarlyLogs.forEach(e -> context.logger.log(e.level(), e.message(), e.error()));
context.pendingEarlyLogs = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Suggested change
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) {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Suggested change
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) {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

…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 gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. pendingEarlyLogs drain — correctly moved into activateLogging() (after createTerminal() has installed ProjectBuildLogAppender), captured before context.logger is swapped, then replayed through the new logger. The ordering is now correct: drain → swap → replay.

  2. Missing @param tagstrace(Supplier<String> content) and trace(Supplier<String> content, Throwable error) both now carry their @param tags matching the other overloads.

Three low-severity observations (not blocking merge):

  • publishIsReentrantSafe test comment — the Javadoc says "Install a custom SLF4J logger that fires a second JUL event" but the test never does that; it only externally sets IN_PUBLISH=true then calls publish(). 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 guardrequireNonNull prevents null but not blank. The previous interface @param said "must not be null or blank"; the updated doc just says "must not be null". A blank string produces a trailing-dot logger name ("com.example.Mojo. "). Harmless in practice but worth a Preconditions.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, so formattedMessage is 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 gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

3 participants