diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java
index 50627efd86e3..23175d07b1f7 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java
@@ -36,6 +36,73 @@
@Experimental
@Provider
public interface Log {
+ /**
+ * {@return true if the trace error level is enabled}
+ *
+ * The default implementation returns {@code false} for backward
+ * compatibility with existing {@code Log} implementations.
+ */
+ default boolean isTraceEnabled() {
+ return false;
+ }
+
+ /**
+ * Sends a message to the user at the trace error level.
+ *
+ * Trace is the most verbose level, intended for Maven core internals
+ * such as resolver negotiation, model interpolation, and lifecycle
+ * ordering details. Use {@link #debug(CharSequence)} instead for
+ * messages that help users investigate their build
+ * (for instance, why a module was recompiled).
+ *
+ * The default implementation is a no-op for backward compatibility.
+ *
+ * @param content the message to log
+ */
+ default void trace(CharSequence content) {}
+
+ /**
+ * Sends a message (and accompanying exception) to the user at the trace error level.
+ * The error's stacktrace will be output when this error level is enabled.
+ *
+ * The default implementation is a no-op for backward compatibility.
+ *
+ * @param content the message to log
+ * @param error the error that caused this log
+ */
+ default void trace(CharSequence content, Throwable error) {}
+
+ /**
+ * Sends an exception to the user at the trace error level.
+ * The stack trace for this exception will be output when this error level is enabled.
+ *
+ * The default implementation is a no-op for backward compatibility.
+ *
+ * @param error the error that caused this log
+ */
+ default void trace(Throwable error) {}
+
+ /**
+ * Sends a lazily-computed message at the trace error level.
+ * The supplier is only evaluated if trace is enabled.
+ *
+ * The default implementation is a no-op for backward compatibility.
+ *
+ * @param content the message supplier
+ */
+ default void trace(Supplier content) {}
+
+ /**
+ * Sends a lazily-computed message (and accompanying exception) at the trace error level.
+ * The supplier is only evaluated if trace is enabled.
+ *
+ * The default implementation is a no-op for backward compatibility.
+ *
+ * @param content the message supplier
+ * @param error the error that caused this log
+ */
+ default void trace(Supplier content, Throwable error) {}
+
/**
* {@return true if the debug error level is enabled}
*/
@@ -43,6 +110,11 @@ public interface Log {
/**
* Sends a message to the user in the debug error level.
+ *
+ * Debug is intended for messages that help users investigate
+ * their build — for example, why a module was recompiled or what
+ * classpath was resolved. For Maven core internals, use
+ * {@link #trace(CharSequence)} instead.
*
* @param content the message to log
*/
@@ -167,4 +239,27 @@ public interface Log {
void error(Supplier content);
void error(Supplier content, Throwable error);
+
+ /**
+ * Returns a child logger whose name is derived from this logger's name
+ * by appending a dot and the given suffix.
+ *
+ * For example, if a plugin's logger is named
+ * {@code "org.apache.maven.plugins.compiler.CompilerMojo"},
+ * then {@code child("diagnostics")} returns a logger named
+ * {@code "org.apache.maven.plugins.compiler.CompilerMojo.diagnostics"}.
+ * This lets sub-components log under an independently filterable name
+ * without requiring a separate injection point.
+ *
+ * The default implementation returns {@code this}, so existing
+ * {@code Log} implementations continue to work without changes.
+ * Implementations that wrap a hierarchical logging backend (such as
+ * SLF4J) should override this to create a real child logger.
+ *
+ * @param name the suffix to append (must not be {@code null} or blank)
+ * @return a child logger — never {@code null}
+ */
+ default Log child(String name) {
+ return this;
+ }
}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java
index 1a11fe46fdb5..b1cf40cc4059 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java
@@ -22,6 +22,7 @@
import org.apache.maven.api.plugin.Log;
import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static java.util.Objects.requireNonNull;
@@ -32,6 +33,46 @@ public DefaultLog(Logger logger) {
this.logger = requireNonNull(logger);
}
+ @Override
+ public boolean isTraceEnabled() {
+ return logger.isTraceEnabled();
+ }
+
+ @Override
+ public void trace(CharSequence content) {
+ if (isTraceEnabled()) {
+ logger.trace(toString(content));
+ }
+ }
+
+ @Override
+ public void trace(CharSequence content, Throwable error) {
+ if (isTraceEnabled()) {
+ logger.trace(toString(content), error);
+ }
+ }
+
+ @Override
+ public void trace(Throwable error) {
+ if (isTraceEnabled()) {
+ logger.trace("", error);
+ }
+ }
+
+ @Override
+ public void trace(Supplier content) {
+ if (isTraceEnabled()) {
+ logger.trace(content.get());
+ }
+ }
+
+ @Override
+ public void trace(Supplier content, Throwable error) {
+ if (isTraceEnabled()) {
+ logger.trace(content.get(), error);
+ }
+ }
+
@Override
public void debug(CharSequence content) {
if (isDebugEnabled()) {
@@ -48,7 +89,9 @@ public void debug(CharSequence content, Throwable error) {
@Override
public void debug(Throwable error) {
- logger.debug("", error);
+ if (isDebugEnabled()) {
+ logger.debug("", error);
+ }
}
@Override
@@ -81,7 +124,9 @@ public void info(CharSequence content, Throwable error) {
@Override
public void info(Throwable error) {
- logger.info("", error);
+ if (isInfoEnabled()) {
+ logger.info("", error);
+ }
}
@Override
@@ -114,7 +159,9 @@ public void warn(CharSequence content, Throwable error) {
@Override
public void warn(Throwable error) {
- logger.warn("", error);
+ if (isWarnEnabled()) {
+ logger.warn("", error);
+ }
}
@Override
@@ -127,7 +174,7 @@ public void warn(Supplier content) {
@Override
public void warn(Supplier content, Throwable error) {
if (isWarnEnabled()) {
- logger.info(content.get(), error);
+ logger.warn(content.get(), error);
}
}
@@ -147,7 +194,9 @@ public void error(CharSequence content, Throwable error) {
@Override
public void error(Throwable error) {
- logger.error("", error);
+ if (isErrorEnabled()) {
+ logger.error("", error);
+ }
}
@Override
@@ -184,6 +233,12 @@ public boolean isErrorEnabled() {
return logger.isErrorEnabled();
}
+ @Override
+ public Log child(String name) {
+ requireNonNull(name, "name");
+ return new DefaultLog(LoggerFactory.getLogger(logger.getName() + "." + name));
+ }
+
private String toString(CharSequence content) {
return content != null ? content.toString() : "";
}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java
index 040a481455e2..700834eb80df 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java
@@ -121,6 +121,7 @@ public void projectSkipped(ExecutionEvent event) {
@Override
public void mojoStarted(ExecutionEvent event) {
setMdc(event);
+ setMojoMdc(event);
buildEventListener.mojoStarted(event);
delegate.mojoStarted(event);
}
@@ -129,18 +130,21 @@ public void mojoStarted(ExecutionEvent event) {
public void mojoSucceeded(ExecutionEvent event) {
setMdc(event);
delegate.mojoSucceeded(event);
+ ProjectBuildLogAppender.setMojoId(null);
}
@Override
public void mojoFailed(ExecutionEvent event) {
setMdc(event);
delegate.mojoFailed(event);
+ ProjectBuildLogAppender.setMojoId(null);
}
@Override
public void mojoSkipped(ExecutionEvent event) {
setMdc(event);
delegate.mojoSkipped(event);
+ ProjectBuildLogAppender.setMojoId(null);
}
@Override
@@ -148,18 +152,22 @@ public void forkStarted(ExecutionEvent event) {
setMdc(event);
delegate.forkStarted(event);
ProjectBuildLogAppender.setForkingProjectId(event.getProject().getArtifactId());
+ // Save the forking mojo's ID so it can be restored when the fork completes
+ ProjectBuildLogAppender.setForkingMojoId(ProjectBuildLogAppender.getMojoId());
}
@Override
public void forkSucceeded(ExecutionEvent event) {
delegate.forkSucceeded(event);
ProjectBuildLogAppender.setForkingProjectId(null);
+ ProjectBuildLogAppender.setForkingMojoId(null);
}
@Override
public void forkFailed(ExecutionEvent event) {
delegate.forkFailed(event);
ProjectBuildLogAppender.setForkingProjectId(null);
+ ProjectBuildLogAppender.setForkingMojoId(null);
}
@Override
@@ -187,4 +195,12 @@ private void setMdc(ExecutionEvent event) {
ProjectBuildLogAppender.setProjectId(event.getProject().getArtifactId());
}
}
+
+ private void setMojoMdc(ExecutionEvent event) {
+ if (event.getMojoExecution() != null) {
+ String mojoId = event.getMojoExecution().getMojoDescriptor().getFullGoalName() + "@"
+ + event.getMojoExecution().getExecutionId();
+ ProjectBuildLogAppender.setMojoId(mojoId);
+ }
+ }
}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java
index 8465df0cf060..dc82a2f89848 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java
@@ -27,8 +27,11 @@
public class ProjectBuildLogAppender implements AutoCloseable {
private static final String KEY_PROJECT_ID = "maven.project.id";
+ private static final String KEY_MOJO_ID = "maven.mojo.id";
private static final ThreadLocal PROJECT_ID = new InheritableThreadLocal<>();
+ private static final ThreadLocal MOJO_ID = new InheritableThreadLocal<>();
private static final ThreadLocal FORKING_PROJECT_ID = new InheritableThreadLocal<>();
+ private static final ThreadLocal FORKING_MOJO_ID = new InheritableThreadLocal<>();
public static String getProjectId() {
return PROJECT_ID.get();
@@ -52,6 +55,37 @@ public static void setProjectId(String projectId) {
}
}
+ public static String getMojoId() {
+ return MOJO_ID.get();
+ }
+
+ /**
+ * Sets or clears the mojo execution identifier in both the thread-local
+ * and the SLF4J MDC. When clearing ({@code null}), if a forking mojo ID
+ * was saved, it is restored — mirroring the fork-aware project ID pattern.
+ *
+ * Format: {@code "prefix:goal@executionId"}
+ * (e.g. {@code "compiler:compile@default-compile"}).
+ *
+ * @param mojoId the mojo identifier, or {@code null} to clear
+ */
+ public static void setMojoId(String mojoId) {
+ if (mojoId != null) {
+ MOJO_ID.set(mojoId);
+ MDC.put(KEY_MOJO_ID, mojoId);
+ } else {
+ // Restore the forking mojo's ID if one was saved
+ String forkingMojoId = FORKING_MOJO_ID.get();
+ if (forkingMojoId != null) {
+ MOJO_ID.set(forkingMojoId);
+ MDC.put(KEY_MOJO_ID, forkingMojoId);
+ } else {
+ MOJO_ID.remove();
+ MDC.remove(KEY_MOJO_ID);
+ }
+ }
+ }
+
public static void setForkingProjectId(String forkingProjectId) {
if (forkingProjectId != null) {
FORKING_PROJECT_ID.set(forkingProjectId);
@@ -60,6 +94,21 @@ public static void setForkingProjectId(String forkingProjectId) {
}
}
+ /**
+ * Saves or clears the mojo ID of the forking mojo, so it can be
+ * restored when the fork completes. Mirrors the {@link #setForkingProjectId}
+ * pattern for project IDs.
+ *
+ * @param forkingMojoId the forking mojo identifier, or {@code null} to clear
+ */
+ public static void setForkingMojoId(String forkingMojoId) {
+ if (forkingMojoId != null) {
+ FORKING_MOJO_ID.set(forkingMojoId);
+ } else {
+ FORKING_MOJO_ID.remove();
+ }
+ }
+
public static void updateMdc() {
String id = getProjectId();
if (id != null) {
diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java
index e395d1ed000b..476f2aadcdd3 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java
@@ -125,7 +125,7 @@ public void executeMojo(MavenSession session, MojoExecution mojoExecution)
scope.seed(
org.apache.maven.api.plugin.Log.class,
new DefaultLog(LoggerFactory.getLogger(
- mojoExecution.getMojoDescriptor().getFullGoalName())));
+ mojoExecution.getMojoDescriptor().getImplementation())));
InternalMavenSession sessionV4 = InternalMavenSession.from(session.getSession());
scope.seed(Project.class, sessionV4.getProject(project));
scope.seed(org.apache.maven.api.MojoExecution.class, new DefaultMojoExecution(sessionV4, mojoExecution));
diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
index 1af5e5effdd2..915d3d996822 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
@@ -556,7 +556,7 @@ private T loadV4Mojo(
org.apache.maven.api.MojoExecution execution = new DefaultMojoExecution(sessionV4, mojoExecution);
org.apache.maven.api.plugin.Log log = new DefaultLog(
- LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getFullGoalName()));
+ LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getImplementation()));
try {
Injector injector = Injector.create();
injector.discover(pluginRealm);
diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java
new file mode 100644
index 000000000000..eb290734fb98
--- /dev/null
+++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.internal.impl;
+
+import org.apache.maven.api.plugin.Log;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link DefaultLog}.
+ */
+class DefaultLogTest {
+
+ /**
+ * Regression test: {@code warn(Supplier, Throwable)} was incorrectly
+ * calling {@code logger.info()} instead of {@code logger.warn()}.
+ */
+ @Test
+ void warnWithSupplierAndThrowableDelegatesToWarn() {
+ Logger mockLogger = mock(Logger.class);
+ when(mockLogger.isWarnEnabled()).thenReturn(true);
+
+ DefaultLog log = new DefaultLog(mockLogger);
+ RuntimeException ex = new RuntimeException("test");
+ log.warn(() -> "warning message", ex);
+
+ verify(mockLogger).warn("warning message", ex);
+ }
+
+ /**
+ * Verify trace methods delegate to the SLF4J logger correctly.
+ */
+ @Test
+ void traceMethodsDelegateToSlf4jTrace() {
+ Logger mockLogger = mock(Logger.class);
+ when(mockLogger.isTraceEnabled()).thenReturn(true);
+
+ DefaultLog log = new DefaultLog(mockLogger);
+ log.trace("trace message");
+
+ verify(mockLogger).trace("trace message");
+ }
+
+ /**
+ * Verify that trace methods are no-ops when trace is disabled.
+ */
+ @Test
+ void traceIsNoOpWhenDisabled() {
+ Logger mockLogger = mock(Logger.class);
+ when(mockLogger.isTraceEnabled()).thenReturn(false);
+
+ DefaultLog log = new DefaultLog(mockLogger);
+ log.trace("should not be logged");
+
+ verify(mockLogger).isTraceEnabled();
+ verifyNoMoreInteractions(mockLogger);
+ }
+
+ /**
+ * Verify that {@code child()} creates a new logger with the
+ * expected hierarchical name.
+ */
+ @Test
+ void childCreatesSubLogger() {
+ Logger mockLogger = mock(Logger.class);
+ when(mockLogger.getName()).thenReturn("org.apache.maven.plugins.compiler.CompilerMojo");
+
+ DefaultLog parent = new DefaultLog(mockLogger);
+ Log child = parent.child("diagnostics");
+
+ assertNotSame(parent, child);
+ assertTrue(child instanceof DefaultLog, "child should be a DefaultLog");
+ }
+
+ /**
+ * Verify that the default {@code Log.isTraceEnabled()} returns false,
+ * preventing {@code AbstractMethodError} for third-party implementors.
+ */
+ @Test
+ void defaultTraceIsDisabled() {
+ // Use a minimal Log implementation that relies on defaults
+ Log minimal = new Log() {
+ @Override
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ @Override
+ public void debug(CharSequence c) {}
+
+ @Override
+ public void debug(CharSequence c, Throwable e) {}
+
+ @Override
+ public void debug(Throwable e) {}
+
+ @Override
+ public void debug(java.util.function.Supplier c) {}
+
+ @Override
+ public void debug(java.util.function.Supplier c, Throwable e) {}
+
+ @Override
+ public boolean isInfoEnabled() {
+ return false;
+ }
+
+ @Override
+ public void info(CharSequence c) {}
+
+ @Override
+ public void info(CharSequence c, Throwable e) {}
+
+ @Override
+ public void info(Throwable e) {}
+
+ @Override
+ public void info(java.util.function.Supplier c) {}
+
+ @Override
+ public void info(java.util.function.Supplier c, Throwable e) {}
+
+ @Override
+ public boolean isWarnEnabled() {
+ return false;
+ }
+
+ @Override
+ public void warn(CharSequence c) {}
+
+ @Override
+ public void warn(CharSequence c, Throwable e) {}
+
+ @Override
+ public void warn(Throwable e) {}
+
+ @Override
+ public void warn(java.util.function.Supplier c) {}
+
+ @Override
+ public void warn(java.util.function.Supplier c, Throwable e) {}
+
+ @Override
+ public boolean isErrorEnabled() {
+ return false;
+ }
+
+ @Override
+ public void error(CharSequence c) {}
+
+ @Override
+ public void error(CharSequence c, Throwable e) {}
+
+ @Override
+ public void error(Throwable e) {}
+
+ @Override
+ public void error(java.util.function.Supplier c) {}
+
+ @Override
+ public void error(java.util.function.Supplier c, Throwable e) {}
+ };
+
+ // These should NOT throw AbstractMethodError — they use defaults
+ assertEquals(false, minimal.isTraceEnabled());
+ minimal.trace("should be a no-op");
+ minimal.trace("no-op", new RuntimeException());
+ minimal.trace(new RuntimeException());
+ minimal.trace(() -> "no-op");
+ minimal.trace(() -> "no-op", new RuntimeException());
+ }
+}