Logging foundation: structured LogEvent, JUL handler, Log API enhancements - #12694
Logging foundation: structured LogEvent, JUL handler, Log API enhancements#12694gnodet wants to merge 1 commit into
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>
…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()
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 |
| * 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.
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.)
| * 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.
| * | ||
| * @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.
| } | ||
|
|
||
| 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.
| 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.
| * @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.
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 returnsthisfor backward compatibility.Structured LogEvent (
maven-api-core,maven-core)LogEvent/LogLevelAPI for structured log event representationsourceClassName(),sourceMethodName(),threadId()— populated only for events originating fromjava.util.logging,nullfor SLF4J/Log API eventsCustom JUL Handler (
maven-logging)MavenJulHandlerreplacesSLF4JBridgeHandler— reimplements JUL→SLF4J bridging while preserving the fullLogRecordmetadata that the standard bridge silently dropsProjectBuildLogAppenderduring the samepublish()callMavenJulHandler→ 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>sinkProjectBuildLogAppenderproducesLogEventobjects (with JUL metadata when available) instead of raw stringsBuildEventListener.projectLogMessage()now takesLogEventinstead ofStringMojo MDC & logger name (
maven-core)maven.mojo.idMDC key set during mojo execution (format:prefix:goal@executionId)getFullGoalName()("compiler:compile") togetImplementation()(FQCN likeorg.apache.maven.plugins.compiler.CompilerMojo) for proper hierarchical SLF4J level configurationBug fix
DefaultLog.warn(Supplier<String>, Throwable)was callinglogger.info()instead oflogger.warn()PR chain
mvnlogviewerRelated
Test plan
mvn test -pl impl/maven-core,impl/maven-logging— 580 tests pass🤖 Generated with Claude Code