Skip to content

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

Open
gnodet wants to merge 1 commit into
masterfrom
feature/logging-foundation
Open

Logging foundation: structured LogEvent, JUL handler, Log API enhancements#12694
gnodet wants to merge 1 commit into
masterfrom
feature/logging-foundation

Conversation

@gnodet

@gnodet gnodet commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Focused, reviewable PR that lays the logging infrastructure foundation for the build report feature chain. Each feature has been split into its own PR for focused review and discussion.

What's included

Log API enhancements (maven-api-core)

  • Log.trace() — 5 overloads + isTraceEnabled() to separate Maven core internals (trace) from user-facing debug output (debug). Maps to SLF4J TRACE / JUL FINEST.
  • Log.child(name) — hierarchical sub-loggers for plugins that want to separate concerns while keeping level control. Default implementation returns this for backward compatibility.

Structured LogEvent (maven-api-core, maven-core)

  • LogEvent / LogLevel API for structured log event representation
  • Optional JUL metadata fields: sourceClassName(), sourceMethodName(), threadId() — populated only for events originating from java.util.logging, null for SLF4J/Log API events

Custom JUL Handler (maven-logging)

  • MavenJulHandler replaces SLF4JBridgeHandler — reimplements JUL→SLF4J bridging while preserving the full LogRecord metadata that the standard bridge silently drops
  • Uses a ThreadLocal to pass JUL metadata synchronously to ProjectBuildLogAppender during the same publish() call
  • Three code paths, 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
  • ProjectBuildLogAppender produces LogEvent objects (with JUL metadata when available) instead of raw strings
  • BuildEventListener.projectLogMessage() now takes LogEvent instead of String

Mojo MDC & logger name (maven-core)

  • maven.mojo.id MDC key set during mojo execution (format: prefix:goal@executionId)
  • Logger name changed from getFullGoalName() ("compiler:compile") to getImplementation() (FQCN like org.apache.maven.plugins.compiler.CompilerMojo) for proper hierarchical SLF4J level configuration

Bug fix

  • DefaultLog.warn(Supplier<String>, Throwable) was calling logger.info() instead of logger.warn()

PR chain

# PR Feature
1 This PR Logging foundation
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

  • mvn test -pl impl/maven-core,impl/maven-logging — 580 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>
…ments

- Log.trace() — 5 overloads + isTraceEnabled() for core-internal tracing
- Log.child(name) — hierarchical sub-loggers for plugins
- LogEvent / LogLevel API for structured log event representation
- MavenJulHandler replaces SLF4JBridgeHandler, preserving full LogRecord metadata
- MavenSimpleLogger.LogSink structured callback replaces Consumer<String>
- ProjectBuildLogAppender produces LogEvent objects instead of raw strings
- maven.mojo.id MDC key set during mojo execution
- Logger name changed to FQCN for proper hierarchical SLF4J level configuration
- Fix: DefaultLog.warn(Supplier, Throwable) was calling logger.info()
@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

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

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

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

*
* @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.

}

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.

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.

* @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.

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.

2 participants