+ * Each event carries the log level, timestamp, message, and optionally + * the logger name and a stack trace. This replaces raw log line strings + * in the build report, enabling programmatic filtering by level and + * correlation by timestamp. + *
+ * Events originating from the Maven Log API or from JUL + * ({@code java.util.logging}) carry additional source metadata: the + * source class name, source method name, and thread identifier. + * For Log API events the source class name is the mojo implementation + * FQCN; for JUL events it comes from {@code LogRecord}. Events from + * direct SLF4J logging have these fields set to {@code null}. + * + * @since 4.1.0 + */ +@Experimental +public interface LogEvent { + + /** + * When this log event was produced (wall-clock time). + * + * @return the event instant, never {@code null} + */ + @Nonnull + Instant timestamp(); + + /** + * The severity level of this log event. + * + * @return the log level, never {@code null} + */ + @Nonnull + LogLevel level(); + + /** + * The log message, without level prefix or timestamp formatting. + * + * @return the formatted message, never {@code null} + */ + @Nonnull + String message(); + + /** + * The name of the logger that produced this event + * (e.g. {@code "org.apache.maven.plugins.compiler.CompilerMojo"}). + * + * @return the logger name, or {@code null} if unavailable + */ + @Nullable + String loggerName(); + + /** + * The stack trace associated with this event, if an exception was logged. + *
+ * The trace is formatted as a multi-line string and may be truncated + * for very deep stack traces. + * + * @return the stack trace string, or {@code null} if no exception was logged + */ + @Nullable + String stackTrace(); + + /** + * The fully formatted log line as rendered for console output, including + * the level prefix, timestamp, and any ANSI styling applied by the logger. + *
+ * This is the string that would be printed to the terminal in verbose mode. + * Console renderers that just need pass-through output can use this directly, + * while renderers that apply custom formatting (e.g. rich mode) can use the + * structured fields ({@link #level()}, {@link #message()}) instead. + *
+ * May be {@code null} if the event was created outside the SLF4J pipeline + * (e.g. in tests or by programmatic construction). + * + * @return the formatted log line, or {@code null} + */ + @Nullable + String formattedMessage(); + + // ---- Source metadata (populated for Log API and JUL events) ---- + + /** + * The fully qualified class name of the source that issued the log call. + *
+ * For Maven Log API events this is the mojo implementation class name. + * For JUL events it is the value from {@code LogRecord.getSourceClassName()}. + * For direct SLF4J logging it is {@code null}. + * + * @return the source class name, or {@code null} + * @since 4.1.0 + */ + @Nullable + default String sourceClassName() { + return null; + } + + /** + * The method name of the source that issued the log call. + *
+ * For Maven Log API events this is resolved via {@link StackWalker}. + * For JUL events it is the value from {@code LogRecord.getSourceMethodName()}. + * For direct SLF4J logging it is {@code null}. + * + * @return the source method name, or {@code null} + * @since 4.1.0 + */ + @Nullable + default String sourceMethodName() { + return null; + } + + /** + * The thread identifier from which this log event originated. + *
+ * Populated for both Log API and JUL events. Returns {@code -1} + * if the thread ID is not available (i.e. for direct SLF4J events). + * + * @return the thread ID, or {@code -1} if unavailable + * @since 4.1.0 + */ + default long threadId() { + return -1; + } + + /** + * A monotonically increasing sequence number for total ordering of + * log events, useful when multiple events share the same timestamp. + *
+ * Assigned by the logging pipeline when the event is captured, + * providing a global ordering across all event sources (Log API, + * JUL, and direct SLF4J). + * + * @return the sequence number, always non-negative + * @since 4.1.0 + */ + default long sequenceNumber() { + return -1; + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/logging/impl/Log4j2Configuration.java b/api/maven-api-core/src/main/java/org/apache/maven/api/build/report/LogLevel.java similarity index 56% rename from impl/maven-cli/src/main/java/org/apache/maven/cling/logging/impl/Log4j2Configuration.java rename to api/maven-api-core/src/main/java/org/apache/maven/api/build/report/LogLevel.java index bbd487fa5f87..684ea610a5bc 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/logging/impl/Log4j2Configuration.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/build/report/LogLevel.java @@ -16,29 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.cling.logging.impl; +package org.apache.maven.api.build.report; -import org.apache.maven.cling.logging.BaseSlf4jConfiguration; +import org.apache.maven.api.annotations.Experimental; /** - * Configuration for slf4j-log4j2. + * Log severity levels, mirroring the standard SLF4J levels. * - * @since 3.1.0 + * @since 4.1.0 + * @see LogEvent#level() */ -public class Log4j2Configuration extends BaseSlf4jConfiguration { - @Override - public void setRootLoggerLevel(Level level) { - String value = - switch (level) { - case DEBUG -> "debug"; - case INFO -> "info"; - default -> "error"; - }; - System.setProperty("maven.logging.root.level", value); - } - - @Override - public void activate() { - // no op - } +@Experimental +public enum LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/build/report/package-info.java b/api/maven-api-core/src/main/java/org/apache/maven/api/build/report/package-info.java new file mode 100644 index 000000000000..8aee68ff901e --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/build/report/package-info.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/** + * Structured build report data model. + *
+ * This package provides structured representations of build execution + * data, including log events and (in future) full build reports. + * {@link org.apache.maven.api.build.report.LogEvent} is the foundational + * type representing a single structured log entry captured during the build. + * + * @since 4.1.0 + */ +@Experimental +package org.apache.maven.api.build.report; + +import org.apache.maven.api.annotations.Experimental; 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..9c020506f151 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,51 @@ @Experimental @Provider public interface Log { + /** + * {@return true if the trace error level is enabled} + * @since 4.1.0 + */ + boolean isTraceEnabled(); + + /** + * Sends a message to the user in the trace error level. + *
+ * Trace is intended for Maven core internals and low-level framework
+ * diagnostics. Plugin authors should normally use {@link #debug} for
+ * developer-facing diagnostic output.
+ *
+ * @param content the message to log
+ * @since 4.1.0
+ */
+ void trace(CharSequence content);
+
+ /**
+ * Sends a message (and accompanying exception) to the user at the trace error level.
+ *
+ * @param content the message to log
+ * @param error the error that caused this log
+ * @since 4.1.0
+ */
+ void trace(CharSequence content, Throwable error);
+
+ /**
+ * Sends an exception to the user in the trace error level.
+ *
+ * @param error the error that caused this log
+ * @since 4.1.0
+ */
+ void trace(Throwable error);
+
+ /**
+ * @since 4.1.0
+ */
+ void trace(Supplier
+ * Debug is the recommended level for diagnostic output that helps
+ * plugin users troubleshoot build problems (e.g. resolved paths,
+ * computed values). For Maven core internals, prefer {@link #trace}.
*
* @param content the message to log
*/
@@ -167,4 +216,21 @@ public interface Log {
void error(Supplier
+ * The default implementation returns {@code this} so that existing
+ * implementations continue to work without changes.
+ *
+ * @param name the child logger name segment (must not be {@code null})
+ * @return a child {@code Log}, never {@code null}
+ * @since 4.1.0
+ */
+ default Log child(String name) {
+ return this;
+ }
}
diff --git a/compat/maven-embedder/pom.xml b/compat/maven-embedder/pom.xml
index 2df8588ec116..56730ccb4326 100644
--- a/compat/maven-embedder/pom.xml
+++ b/compat/maven-embedder/pom.xml
@@ -163,11 +163,6 @@ under the License.
+ * Public to allow construction from other packages within the Maven
+ * implementation (e.g. {@code ProjectBuildLogAppender}).
+ *
+ * @param timestamp when the event was produced
+ * @param level the severity level
+ * @param message the clean log message (without level prefix or ANSI)
+ * @param loggerName the name of the logger, or {@code null}
+ * @param stackTrace the stack trace string, or {@code null}
+ * @param formattedMessage the fully formatted console line, or {@code null}
+ * @param sourceClassName the source class name (Log API mojo FQCN or JUL source), or {@code null}
+ * @param sourceMethodName the source method name (via StackWalker or JUL), or {@code null}
+ * @param threadId the originating thread ID, or {@code -1} if unavailable
+ * @param sequenceNumber the JUL sequence number for ordering, or {@code -1} if unavailable
+ */
+public record DefaultLogEvent(
+ Instant timestamp,
+ LogLevel level,
+ String message,
+ String loggerName,
+ String stackTrace,
+ String formattedMessage,
+ String sourceClassName,
+ String sourceMethodName,
+ long threadId,
+ long sequenceNumber)
+ implements LogEvent {
+
+ /**
+ * Convenience constructor for events without source metadata
+ * (i.e. direct SLF4J events).
+ */
+ public DefaultLogEvent(
+ Instant timestamp,
+ LogLevel level,
+ String message,
+ String loggerName,
+ String stackTrace,
+ String formattedMessage) {
+ this(timestamp, level, message, loggerName, stackTrace, formattedMessage, null, null, -1, -1);
+ }
+
+ /**
+ * Convenience constructor for events created without a formatted message
+ * (e.g. in tests or programmatic construction).
+ */
+ DefaultLogEvent(Instant timestamp, LogLevel level, String message, String loggerName, String stackTrace) {
+ this(timestamp, level, message, loggerName, stackTrace, null, null, null, -1, -1);
+ }
+}
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..581227c76d62 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
@@ -18,149 +18,251 @@
*/
package org.apache.maven.internal.impl;
+import java.lang.StackWalker.StackFrame;
import java.util.function.Supplier;
import org.apache.maven.api.plugin.Log;
import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static java.util.Objects.requireNonNull;
public class DefaultLog implements Log {
+
+ /**
+ * Metadata captured from Log API calls, mirroring the JUL metadata
+ * pattern in {@code MavenJulHandler}.
+ *
+ * @param sourceClassName the fully qualified class name of the caller
+ * @param sourceMethodName the method that issued the log call
+ * @param threadId the originating thread ID
+ */
+ public record LogApiMetadata(String sourceClassName, String sourceMethodName, long threadId) {}
+
+ private static final ThreadLocal
+ * Called from {@code ProjectBuildLogAppender.accept()} to populate
+ * {@code LogEvent.sourceClassName()} and {@code LogEvent.sourceMethodName()}.
+ *
+ * @return the current Log API metadata, or {@code null}
+ */
+ public static LogApiMetadata getLogApiMetadata() {
+ return LOG_API_METADATA.get();
+ }
+
private final Logger logger;
public DefaultLog(Logger logger) {
this.logger = requireNonNull(logger);
}
+ /**
+ * Wraps a logging call with Log API metadata: captures the caller's
+ * method name via {@link StackWalker}, sets the ThreadLocal, executes
+ * the actual SLF4J call, and clears the ThreadLocal.
+ *
+ * The source class name is taken from the SLF4J logger name (which
+ * is the mojo implementation FQCN, set at injection time). The
+ * source method name is resolved by walking the stack past this class
+ * to find the first external caller frame.
+ */
+ private void withMetadata(Runnable logAction) {
+ String callerMethodName = WALKER.walk(frames -> frames.dropWhile(f -> THIS_CLASS.equals(f.getClassName()))
+ .findFirst()
+ .map(StackFrame::getMethodName)
+ .orElse(null));
+ LOG_API_METADATA.set(new LogApiMetadata(
+ logger.getName(), callerMethodName, Thread.currentThread().getId()));
+ try {
+ logAction.run();
+ } finally {
+ LOG_API_METADATA.remove();
+ }
+ }
+
+ @Override
+ public boolean isTraceEnabled() {
+ return logger.isTraceEnabled();
+ }
+
+ @Override
+ public void trace(CharSequence content) {
+ if (isTraceEnabled()) {
+ withMetadata(() -> logger.trace(toString(content)));
+ }
+ }
+
+ @Override
+ public void trace(CharSequence content, Throwable error) {
+ if (isTraceEnabled()) {
+ withMetadata(() -> logger.trace(toString(content), error));
+ }
+ }
+
+ @Override
+ public void trace(Throwable error) {
+ if (isTraceEnabled()) {
+ withMetadata(() -> logger.trace("", error));
+ }
+ }
+
+ @Override
+ public void trace(Supplier
+ * Installs itself as a {@link MavenSimpleLogger.LogSink} to intercept all
+ * SLF4J log output, enrich it with structured metadata (level, logger name,
+ * clean message, formatted output), and forward to the active
+ * {@link BuildEventListener}.
*/
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
+ * 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 {
+ MOJO_ID.remove();
+ MDC.remove(KEY_MOJO_ID);
+ }
+ }
+
public static void setForkingProjectId(String forkingProjectId) {
if (forkingProjectId != null) {
FORKING_PROJECT_ID.set(forkingProjectId);
@@ -69,6 +114,32 @@ public static void updateMdc() {
}
}
+ /**
+ * Global sequence counter for total ordering of log events across
+ * all sources (Log API, JUL, direct SLF4J). Incremented atomically
+ * in {@link #accept} which is called synchronously on the logging thread.
+ */
+ private static final AtomicLong SEQUENCE = new AtomicLong();
+
+ /**
+ * Callback for build report log capture. Receives the fully-formed
+ * {@link LogEvent} produced by {@link #accept}, eliminating the need
+ * for a second capture pipeline in {@code MavenSimpleLogger}.
+ *
+ * Set by {@code BuildReportCollector} at session start, cleared at
+ * session end. The callback runs synchronously on the logging thread.
+ */
+ private static volatile Consumer
+ * The default implementation delegates to {@link #write(StringBuilder, Throwable)}.
+ *
+ * @param level the SLF4J log level constant
+ * @param loggerName the name of the logger
+ * @param cleanMessage the formatted message without level/timestamp prefix
+ * @param formattedBuf the fully formatted log line
+ * @param t the throwable, may be {@code null}
+ */
+ protected void write(int level, String loggerName, String cleanMessage, StringBuilder formattedBuf, Throwable t) {
+ write(formattedBuf, t);
+ }
+
protected void writeThrowable(Throwable t, PrintStream targetStream) {
if (t != null) {
t.printStackTrace(targetStream);
@@ -375,7 +392,7 @@ private void innerHandleNormalizedLoggingCall(
// Append the message
buf.append(formattedMessage);
- write(buf, t);
+ write(level.toInt(), name, formattedMessage, buf, t);
}
protected String renderLevel(int levelInt) {
diff --git a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenJulHandler.java b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenJulHandler.java
new file mode 100644
index 000000000000..9be299faf0fd
--- /dev/null
+++ b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenJulHandler.java
@@ -0,0 +1,249 @@
+/*
+ * 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.slf4j;
+
+import java.text.MessageFormat;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.apache.maven.api.services.MessageBuilder;
+import org.slf4j.LoggerFactory;
+import org.slf4j.spi.LocationAwareLogger;
+
+import static org.apache.maven.jline.MessageUtils.builder;
+
+/**
+ * A JUL {@link Handler} that routes {@code java.util.logging} events into
+ * Maven's structured logging pipeline, preserving the rich {@link LogRecord}
+ * metadata that the standard {@code SLF4JBridgeHandler} silently drops
+ * (source class name, source method name, thread ID).
+ *
+ * When a {@link MavenSimpleLogger.LogSink LogSink} is installed (i.e. during
+ * a build), JUL events are sent directly to the sink — bypassing SLF4J
+ * entirely. The JUL metadata is stashed in a thread-local so downstream
+ * consumers (e.g. {@code ProjectBuildLogAppender}) can read it when
+ * constructing a structured {@code LogEvent}.
+ *
+ * When no LogSink is installed (e.g. during early bootstrap), the handler
+ * falls back to routing through SLF4J for console output.
+ *
+ * Usage — replace the standard SLF4J bridge in {@code LookupInvoker}:
+ *
+ * This method is intended to be called from within a
+ * {@link MavenSimpleLogger.LogSink} callback (e.g. in
+ * {@code ProjectBuildLogAppender.accept()}).
+ *
+ * @return the current JUL metadata, or {@code null}
+ */
+ public static JulMetadata getJulMetadata() {
+ return METADATA.get();
+ }
+
+ /**
+ * Installs this handler on the JUL root logger, removing any
+ * previously installed handlers. This replaces the standard
+ * {@code SLF4JBridgeHandler.install()} call.
+ */
+ public static void install() {
+ Logger rootLogger = LogManager.getLogManager().getLogger("");
+ // Remove all existing handlers (including any SLF4JBridgeHandler)
+ for (Handler handler : rootLogger.getHandlers()) {
+ rootLogger.removeHandler(handler);
+ }
+ rootLogger.addHandler(new MavenJulHandler());
+ // Accept all levels — filtering is done by SLF4J
+ rootLogger.setLevel(Level.ALL);
+ }
+
+ /**
+ * Returns {@code true} if a {@code MavenJulHandler} is installed
+ * on the JUL root logger.
+ */
+ public static boolean isInstalled() {
+ Logger rootLogger = LogManager.getLogManager().getLogger("");
+ for (Handler handler : rootLogger.getHandlers()) {
+ if (handler instanceof MavenJulHandler) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void publish(LogRecord record) {
+ if (record == null) {
+ return;
+ }
+
+ String loggerName = record.getLoggerName();
+ org.slf4j.Logger slf4jLogger = LoggerFactory.getLogger(loggerName);
+ int slf4jLevel = julLevelToSlf4j(record.getLevel());
+
+ // Quick exit if this level is not enabled
+ if (!isLevelEnabled(slf4jLogger, slf4jLevel)) {
+ return;
+ }
+
+ String message = formatMessage(record);
+ Throwable throwable = record.getThrown();
+
+ // If a LogSink is installed, bypass SLF4J entirely: call the sink
+ // directly with the JUL metadata so no information is lost in transit.
+ MavenSimpleLogger.LogSink sink = MavenSimpleLogger.getLogSink();
+ if (sink != null) {
+ METADATA.set(new JulMetadata(
+ record.getSourceClassName(), record.getSourceMethodName(), record.getLongThreadID()));
+ try {
+ String formatted = formatForConsole(slf4jLevel, message);
+ sink.accept(slf4jLevel, loggerName, message, formatted, throwable);
+ } finally {
+ METADATA.remove();
+ }
+ } else {
+ // No LogSink — fall through to SLF4J for console output
+ logToSlf4j(slf4jLogger, slf4jLevel, message, throwable);
+ }
+ }
+
+ @Override
+ public void flush() {
+ // nothing to flush
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ // nothing to close
+ }
+
+ /**
+ * Formats the log message, applying i18n resource bundle lookup and
+ * {@link MessageFormat} parameter substitution, matching the behavior
+ * of {@code SLF4JBridgeHandler}.
+ */
+ private static String formatMessage(LogRecord record) {
+ String message = record.getMessage();
+ if (message == null) {
+ return "";
+ }
+
+ // Try resource bundle lookup
+ ResourceBundle bundle = record.getResourceBundle();
+ if (bundle != null) {
+ try {
+ message = bundle.getString(message);
+ } catch (MissingResourceException e) {
+ // use raw message
+ }
+ }
+
+ // Apply MessageFormat parameters
+ Object[] params = record.getParameters();
+ if (params != null && params.length > 0) {
+ try {
+ message = MessageFormat.format(message, params);
+ } catch (IllegalArgumentException e) {
+ // use message as-is if formatting fails
+ }
+ }
+
+ return message;
+ }
+
+ /**
+ * Formats a JUL message for console output, matching the {@code [LEVEL] message}
+ * style used by MavenSimpleLogger with ANSI coloring when available.
+ */
+ private static String formatForConsole(int level, String message) {
+ MessageBuilder mb = builder();
+ String levelStr =
+ switch (level) {
+ case LocationAwareLogger.TRACE_INT -> mb.trace("TRACE").build();
+ case LocationAwareLogger.DEBUG_INT -> mb.debug("DEBUG").build();
+ case LocationAwareLogger.INFO_INT -> mb.info("INFO").build();
+ case LocationAwareLogger.WARN_INT -> mb.warning("WARNING").build();
+ default -> mb.error("ERROR").build();
+ };
+ return "[" + levelStr + "] " + message;
+ }
+
+ private static int julLevelToSlf4j(Level julLevel) {
+ int value = julLevel.intValue();
+ if (value <= Level.FINEST.intValue()) {
+ return LocationAwareLogger.TRACE_INT;
+ } else if (value <= Level.FINE.intValue()) {
+ return LocationAwareLogger.DEBUG_INT;
+ } else if (value <= Level.INFO.intValue()) {
+ return LocationAwareLogger.INFO_INT;
+ } else if (value <= Level.WARNING.intValue()) {
+ return LocationAwareLogger.WARN_INT;
+ } else {
+ return LocationAwareLogger.ERROR_INT;
+ }
+ }
+
+ private static boolean isLevelEnabled(org.slf4j.Logger logger, int level) {
+ return switch (level) {
+ case LocationAwareLogger.TRACE_INT -> logger.isTraceEnabled();
+ case LocationAwareLogger.DEBUG_INT -> logger.isDebugEnabled();
+ case LocationAwareLogger.INFO_INT -> logger.isInfoEnabled();
+ case LocationAwareLogger.WARN_INT -> logger.isWarnEnabled();
+ default -> logger.isErrorEnabled();
+ };
+ }
+
+ private static void logToSlf4j(org.slf4j.Logger logger, int level, String message, Throwable throwable) {
+ switch (level) {
+ case LocationAwareLogger.TRACE_INT -> logger.trace(message, throwable);
+ case LocationAwareLogger.DEBUG_INT -> logger.debug(message, throwable);
+ case LocationAwareLogger.INFO_INT -> logger.info(message, throwable);
+ case LocationAwareLogger.WARN_INT -> logger.warn(message, throwable);
+ default -> logger.error(message, throwable);
+ }
+ }
+}
diff --git a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenSimpleLogger.java b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenSimpleLogger.java
index 02767987e2a1..77fc8b1eb846 100644
--- a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenSimpleLogger.java
+++ b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenSimpleLogger.java
@@ -39,14 +39,45 @@ public class MavenSimpleLogger extends MavenBaseLogger {
private String warnRenderedLevel;
private String errorRenderedLevel;
- static Consumer
+ * This replaces the previous {@code Consumer
+ * MavenJulHandler.install();
+ *
+ *
+ * @since 4.1.0
+ * @see #install()
+ * @see #getJulMetadata()
+ */
+public class MavenJulHandler extends Handler {
+
+ /**
+ * JUL metadata captured from a {@link LogRecord} that would otherwise
+ * be lost when bridging to SLF4J.
+ *
+ * @param sourceClassName the source class, or {@code null}
+ * @param sourceMethodName the source method, or {@code null}
+ * @param threadId the originating thread ID
+ */
+ public record JulMetadata(String sourceClassName, String sourceMethodName, long threadId) {}
+
+ private static final ThreadLocal