diff --git a/log4j-api-to-jul/pom.xml b/log4j-api-to-jul/pom.xml
new file mode 100644
index 0000000..ed2b71d
--- /dev/null
+++ b/log4j-api-to-jul/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ 4.0.0
+
+ org.apache.logging.log4j
+ log4j-jdk-parent
+ ${revision}
+ ../parent
+
+
+ log4j-api-to-jul
+ jar
+ Log4j API to JUL logging bridge
+ The Apache Log4j binding between Log4j 2 API and java.util.logging (JUL).
+
+ 2022
+
+
+
+ 1.8.0
+
+
+
+
+
+ org.osgi
+ org.osgi.framework
+ ${osgi.framework.version}
+ provided
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+ com.google.guava
+ guava-testlib
+ 33.3.1-jre
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/Activator.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/Activator.java
new file mode 100644
index 0000000..11c7c14
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/Activator.java
@@ -0,0 +1,30 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import org.apache.logging.log4j.util.ProviderActivator;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@Header(name = Constants.BUNDLE_ACTIVATIONPOLICY, value = Constants.ACTIVATION_LAZY)
+public class Activator extends ProviderActivator {
+
+ public Activator() {
+ super(new JULProvider());
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLogger.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLogger.java
new file mode 100644
index 0000000..1108107
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLogger.java
@@ -0,0 +1,331 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.logging.Logger;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.spi.AbstractLogger;
+
+/**
+ * Implementation of {@link org.apache.logging.log4j.Logger} that's backed by a {@link Logger}.
+ *
+ * This implementation currently ignores {@link Marker}.
+ *
+ * @author Michael Vorburger.ch for Google
+ */
+final class JULLogger extends AbstractLogger {
+ private static final long serialVersionUID = 1L;
+
+ private final Logger logger;
+
+ // This implementation is inspired by org.apache.logging.slf4j.SLF4JLogger
+
+ public JULLogger(final String name, final MessageFactory messageFactory, final Logger logger) {
+ super(name, messageFactory);
+ this.logger = requireNonNull(logger, "logger");
+ }
+
+ public JULLogger(final String name, final Logger logger) {
+ super(name);
+ this.logger = requireNonNull(logger, "logger");
+ }
+
+ public Logger getWrappedLogger() {
+ return logger;
+ }
+
+ @Override
+ public void logMessage(
+ final String fqcn, final Level level, final Marker marker, final Message message, final Throwable t) {
+ final java.util.logging.Level julLevel = convertLevel(level);
+ if (!logger.isLoggable(julLevel)) {
+ return;
+ }
+ final LazyLog4jLogRecord record =
+ new LazyLog4jLogRecord(fqcn, julLevel, message.getFormattedMessage()); // NOT getFormat()
+ // NOT record.setParameters(message.getParameters()); BECAUSE getFormattedMessage() NOT getFormat()
+ record.setLoggerName(getName());
+ record.setThrown(t == null ? message.getThrowable() : t);
+ logger.log(record);
+ }
+
+ // Convert Level in Log4j scale to JUL scale.
+ // See getLevel() for the mapping. Note that JUL's FINEST & CONFIG are never returned because Log4j has no such
+ // levels, and
+ // that Log4j's FATAL is simply mapped to JUL's SEVERE as is Log4j's ERROR because JUL does not distinguish between
+ // ERROR and FATAL.
+ private java.util.logging.Level convertLevel(final Level level) {
+ switch (level.getStandardLevel()) {
+ // Test in logical order of likely frequency of use
+ // Must be kept in sync with #getLevel()
+ case ALL:
+ return java.util.logging.Level.ALL;
+ case TRACE:
+ return java.util.logging.Level.FINER;
+ case DEBUG:
+ return java.util.logging.Level.FINE;
+ case INFO:
+ return java.util.logging.Level.INFO;
+ case WARN:
+ return java.util.logging.Level.WARNING;
+ case ERROR:
+ return java.util.logging.Level.SEVERE;
+ case FATAL:
+ return java.util.logging.Level.SEVERE;
+ case OFF:
+ return java.util.logging.Level.OFF;
+ default:
+ // This is tempting: throw new IllegalStateException("Impossible Log4j Level encountered: " +
+ // level.intLevel());
+ // But it's not a great idea, security wise. If an attacker *SOMEHOW* managed to create a Log4j Level
+ // instance
+ // with an unexpected level (through JVM de-serialization, despite readResolve() { return
+ // Level.valueOf(this.name); },
+ // or whatever other means), then we would blow up in a very unexpected place and way. Let us therefore
+ // instead just
+ // return SEVERE for unexpected values, because that's more likely to be noticed than a FINER.
+ // Greetings, Michael Vorburger.ch , for Google, on 2021.12.24.
+ return java.util.logging.Level.SEVERE;
+ }
+ }
+
+ /**
+ * Level in Log4j scale.
+ * JUL Levels are mapped as follows:
+ *
+ * - OFF => OFF
+ *
- SEVERE => ERROR
+ *
- WARNING => WARN
+ *
- INFO => INFO
+ *
- CONFIG => INFO
+ *
- FINE => DEBUG
+ *
- FINER => TRACE (as in https://github.com/apache/logging-log4j2/blob/a58a06bf2365165ac5abdde931bb4ecd1adf0b3c/log4j-jul/src/main/java/org/apache/logging/log4j/jul/DefaultLevelConverter.java#L55-L75)
+ *
- FINEST => TRACE
+ *
- ALL => ALL
+ *
+ *
+ * Numeric JUL Levels that don't match the known levels are matched to the closest one.
+ * For example, anything between OFF (Integer.MAX_VALUE) and SEVERE (1000) is returned as a Log4j FATAL.
+ */
+ @Override
+ public Level getLevel() {
+ final int julLevel = getEffectiveJULLevel().intValue();
+ // Test in logical order of likely frequency of use
+ // Must be kept in sync with #convertLevel()
+ if (julLevel == java.util.logging.Level.ALL.intValue()) {
+ return Level.ALL;
+ }
+ if (julLevel <= java.util.logging.Level.FINER.intValue()) {
+ return Level.TRACE;
+ }
+ if (julLevel <= java.util.logging.Level.FINE.intValue()) { // includes FINER
+ return Level.DEBUG;
+ }
+ if (julLevel <= java.util.logging.Level.INFO.intValue()) { // includes CONFIG
+ return Level.INFO;
+ }
+ if (julLevel <= java.util.logging.Level.WARNING.intValue()) {
+ return Level.WARN;
+ }
+ if (julLevel <= java.util.logging.Level.SEVERE.intValue()) {
+ return Level.ERROR;
+ }
+ return Level.OFF;
+ }
+
+ private java.util.logging.Level getEffectiveJULLevel() {
+ Logger current = logger;
+ while (current.getLevel() == null && current.getParent() != null) {
+ current = current.getParent();
+ }
+ if (current.getLevel() != null) {
+ return current.getLevel();
+ }
+ // This is a safety fallback that is typically never reached, because usually the root Logger.getLogger("") has
+ // a Level.
+ // Since JDK 8 the LogManager$RootLogger does not have a default level, just a default effective level of INFO.
+ return java.util.logging.Level.INFO;
+ }
+
+ private boolean isEnabledFor(final Level level, final Marker marker) {
+ // E.g. we're logging WARN and more, so getLevel() is 300, if we're asked if we're
+ // enabled for level ERROR which is 200, isLessSpecificThan() tests for >= so return true.
+ return getLevel().isLessSpecificThan(level);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final Message data, final Throwable t) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final CharSequence data, final Throwable t) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final Object data, final Throwable t) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final String data) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final String data, final Object... p1) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final String message, final Object p0) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level, final Marker marker, final String message, final Object p0, final Object p1) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7,
+ final Object p8) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(
+ final Level level,
+ final Marker marker,
+ final String message,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7,
+ final Object p8,
+ final Object p9) {
+ return isEnabledFor(level, marker);
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, final Marker marker, final String data, final Throwable t) {
+ return isEnabledFor(level, marker);
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContext.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContext.java
new file mode 100644
index 0000000..ae6697f
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContext.java
@@ -0,0 +1,84 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import java.util.logging.Logger;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.ParameterizedMessageFactory;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.apache.logging.log4j.spi.LoggerContext;
+import org.apache.logging.log4j.spi.LoggerRegistry;
+
+/**
+ * Implementation of Log4j {@link LoggerContext} SPI.
+ * This is a factory to produce {@link JULLogger} instances.
+ *
+ * @author Michael Vorburger.ch for Google
+ */
+class JULLoggerContext implements LoggerContext {
+
+ private final LoggerRegistry loggerRegistry = new LoggerRegistry<>();
+
+ private static final MessageFactory DEFAULT_MESSAGE_FACTORY = ParameterizedMessageFactory.INSTANCE;
+
+ // This implementation is strongly inspired by org.apache.logging.slf4j.SLF4JLoggerContext
+
+ @Override
+ public Object getExternalContext() {
+ return null;
+ }
+
+ @Override
+ public ExtendedLogger getLogger(final String name) {
+ return getLogger(name, DEFAULT_MESSAGE_FACTORY);
+ }
+
+ @Override
+ public ExtendedLogger getLogger(final String name, final MessageFactory messageFactory) {
+ final MessageFactory effectiveMessageFactory =
+ messageFactory != null ? messageFactory : DEFAULT_MESSAGE_FACTORY;
+ final ExtendedLogger oldLogger = loggerRegistry.getLogger(name, effectiveMessageFactory);
+ if (oldLogger != null) {
+ return oldLogger;
+ }
+ final ExtendedLogger newLogger = createLogger(name, effectiveMessageFactory);
+ loggerRegistry.putIfAbsent(name, effectiveMessageFactory, newLogger);
+ return loggerRegistry.getLogger(name, effectiveMessageFactory);
+ }
+
+ private static ExtendedLogger createLogger(final String name, final MessageFactory messageFactory) {
+ final Logger logger = Logger.getLogger(name);
+ return new JULLogger(name, messageFactory, logger);
+ }
+
+ @Override
+ public boolean hasLogger(final String name) {
+ return loggerRegistry.hasLogger(name, DEFAULT_MESSAGE_FACTORY);
+ }
+
+ @Override
+ public boolean hasLogger(final String name, final MessageFactory messageFactory) {
+ final MessageFactory effectiveMessageFactory =
+ messageFactory != null ? messageFactory : DEFAULT_MESSAGE_FACTORY;
+ return loggerRegistry.hasLogger(name, effectiveMessageFactory);
+ }
+
+ @Override
+ public boolean hasLogger(final String name, final Class extends MessageFactory> messageFactoryClass) {
+ return loggerRegistry.hasLogger(name, messageFactoryClass);
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContextFactory.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContextFactory.java
new file mode 100644
index 0000000..f241e07
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULLoggerContextFactory.java
@@ -0,0 +1,76 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import java.net.URI;
+import org.apache.logging.log4j.spi.LoggerContext;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
+
+/**
+ * Implementation of Log4j {@link LoggerContextFactory} SPI.
+ * This is a factory to produce the (one and only) {@link JULLoggerContext} instance.
+ *
+ * @author Michael Vorburger.ch for Google
+ */
+public class JULLoggerContextFactory implements LoggerContextFactory {
+ private static final StatusLogger LOGGER = StatusLogger.getLogger();
+ private static final LoggerContext context = new JULLoggerContext();
+
+ // This implementation is strongly inspired by org.apache.logging.slf4j.SLF4JLoggerContextFactory
+
+ public JULLoggerContextFactory() {
+ boolean misconfigured = false;
+ try {
+ LoaderUtil.loadClass("org.apache.logging.log4j.jul.LogManager");
+ misconfigured = true;
+ } catch (final ClassNotFoundException classNotFoundIsGood) {
+ LOGGER.debug("org.apache.logging.log4j.jul.LogManager is not on classpath. Good!");
+ }
+ if (misconfigured) {
+ throw new IllegalStateException("log4j-jul JAR is mutually exclusive with the log4j-to-jul JAR"
+ + "(the first routes calls from Log4j to JUL, the second from Log4j to JUL)");
+ }
+ }
+
+ @Override
+ public LoggerContext getContext(
+ final String fqcn, final ClassLoader loader, final Object externalContext, final boolean currentContext) {
+ return context;
+ }
+
+ @Override
+ public LoggerContext getContext(
+ final String fqcn,
+ final ClassLoader loader,
+ final Object externalContext,
+ final boolean currentContext,
+ final URI configLocation,
+ final String name) {
+ return context;
+ }
+
+ @Override
+ public void removeContext(final LoggerContext ignored) {}
+
+ @Override
+ public boolean isClassLoaderDependent() {
+ // context is always used
+ return false;
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
new file mode 100644
index 0000000..7497e48
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
@@ -0,0 +1,51 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.spi.NoOpThreadContextMap;
+import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Bind the Log4j API to JUL.
+ *
+ * @author Michael Vorburger.ch for Google
+ */
+@NullMarked
+@ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
+public class JULProvider extends Provider {
+ private static final LoggerContextFactory CONTEXT_FACTORY = new JULLoggerContextFactory();
+
+ public JULProvider() {
+ super(20, CURRENT_VERSION, JULLoggerContextFactory.class, NoOpThreadContextMap.class);
+ }
+
+ @Override
+ public LoggerContextFactory getLoggerContextFactory() {
+ return CONTEXT_FACTORY;
+ }
+
+ @Override
+ public ThreadContextMap getThreadContextMapInstance() {
+ // JUL does not provide an MDC implementation
+ return NoOpThreadContextMap.INSTANCE;
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/LazyLog4jLogRecord.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/LazyLog4jLogRecord.java
new file mode 100644
index 0000000..014aa8c
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/LazyLog4jLogRecord.java
@@ -0,0 +1,70 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.apache.logging.log4j.util.StackLocatorUtil;
+
+/**
+ * Extension of {@link java.util.logging.LogRecord} with lazy get source related methods based on Log4j's {@link StackLocatorUtil#calcLocation(String)}.
+ */
+final class LazyLog4jLogRecord extends LogRecord {
+
+ private static final long serialVersionUID = 6798134264543826471L;
+
+ // parent class LogRecord already has a needToInferCaller but it's private
+ private transient boolean inferCaller = true;
+
+ private final String fqcn;
+
+ LazyLog4jLogRecord(final String fqcn, final Level level, final String msg) {
+ super(level, msg);
+ this.fqcn = fqcn;
+ }
+
+ @Override
+ public String getSourceClassName() {
+ if (inferCaller) {
+ inferCaller();
+ }
+ return super.getSourceClassName();
+ }
+
+ @Override
+ public String getSourceMethodName() {
+ if (inferCaller) {
+ inferCaller();
+ }
+ return super.getSourceMethodName();
+ }
+
+ private void inferCaller() {
+ StackTraceElement location = null;
+ if (fqcn != null) {
+ location = StackLocatorUtil.calcLocation(fqcn);
+ }
+ if (location != null) {
+ setSourceClassName(location.getClassName());
+ setSourceMethodName(location.getMethodName());
+ } else {
+ setSourceClassName(null);
+ setSourceMethodName(null);
+ }
+ inferCaller = false;
+ }
+}
diff --git a/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/package-info.java b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/package-info.java
new file mode 100644
index 0000000..8308d5c
--- /dev/null
+++ b/log4j-api-to-jul/src/main/java/org/apache/logging/log4j/tojul/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+/**
+ * Java JDK java.util.logging (JUL) bridge.
+ * This sends all Log4j logs to JUL (not the other way around, there is another module for the opposite direction).
+ *
+ * @author Michael Vorburger.ch for Google
+ */
+@Export
+@Version("2.24.1")
+package org.apache.logging.log4j.tojul;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-api-to-jul/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider b/log4j-api-to-jul/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider
new file mode 100644
index 0000000..2ac36b5
--- /dev/null
+++ b/log4j-api-to-jul/src/main/resources/META-INF/services/org.apache.logging.log4j.spi.Provider
@@ -0,0 +1,18 @@
+# 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.
+
+org.apache.logging.log4j.tojul.JULProvider
diff --git a/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/JULLoggerTest.java b/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/JULLoggerTest.java
new file mode 100644
index 0000000..9f70a01
--- /dev/null
+++ b/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/JULLoggerTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.logging.log4j.Level;
+import org.junit.jupiter.api.Test;
+
+public class JULLoggerTest {
+
+ @Test
+ public void testNotNullEffectiveLevel() {
+ // Emulates the root logger found in Tomcat, with a null level
+ // See: https://bz.apache.org/bugzilla/show_bug.cgi?id=66184
+ final java.util.logging.Logger julLogger = new java.util.logging.Logger("", null) {};
+ final JULLogger logger = new JULLogger("", julLogger);
+ assertEquals(Level.INFO, logger.getLevel());
+ }
+}
diff --git a/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/LoggerTest.java b/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/LoggerTest.java
new file mode 100644
index 0000000..c2d6519
--- /dev/null
+++ b/log4j-api-to-jul/src/test/java/org/apache/logging/log4j/tojul/LoggerTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.logging.log4j.tojul;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.testing.TestLogHandler;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.apache.logging.log4j.LogManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class LoggerTest {
+
+ // Save levels so that we can reset them @After clearLogs()
+ private static final java.util.logging.Logger globalLogger = java.util.logging.Logger.getGlobal();
+ private static final java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("");
+ private static final Level globalLevel = globalLogger.getLevel();
+ private static final Level rootLevel = rootLogger.getLevel();
+
+ private org.apache.logging.log4j.Logger log4jLogger;
+ private java.util.logging.Logger julLogger;
+ private Level julLoggerDefaultLevel;
+
+ // https://javadoc.io/doc/com.google.guava/guava-testlib/latest/com/google/common/testing/TestLogHandler.html
+ private TestLogHandler handler;
+
+ @BeforeEach
+ public void setupLogCapture() {
+ handler = new TestLogHandler();
+ // Beware, the order here should not be changed!
+ // Let the bridge do whatever it does BEFORE we create a JUL Logger (which SHOULD be the same)
+ log4jLogger = LogManager.getLogger(getClass());
+ assertThat(log4jLogger).isInstanceOf(JULLogger.class);
+ julLogger = java.util.logging.Logger.getLogger(getClass().getName());
+ assertThat(julLogger).isSameAs(((JULLogger) log4jLogger).getWrappedLogger());
+ julLogger.addHandler(handler);
+
+ julLoggerDefaultLevel = julLogger.getLevel();
+
+ // Check that there is no configuration file which invalidates our assumption that the root logger is the parent
+ // of our julLogger
+ assertThat(julLogger.getParent()).isEqualTo(rootLogger);
+ }
+
+ @AfterEach
+ public void clearLogs() {
+ julLogger.removeHandler(handler);
+ // Reset all Levels what any tests set anymore
+ julLogger.setLevel(julLoggerDefaultLevel);
+ rootLogger.setLevel(rootLevel);
+ globalLogger.setLevel(globalLevel);
+ }
+
+ @Test
+ public void infoAtInfo() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.info("hello, world");
+
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ assertThat(log1.getLoggerName()).isEqualTo(getClass().getName());
+ assertThat(log1.getLevel()).isEqualTo(java.util.logging.Level.INFO);
+ assertThat(log1.getMessage()).isEqualTo("hello, world");
+ assertThat(log1.getParameters()).isNull();
+ assertThat(log1.getThrown()).isNull();
+ assertThat(log1.getSourceClassName()).isEqualTo(getClass().getName());
+ assertThat(log1.getSourceMethodName()).isEqualTo("infoAtInfo");
+ }
+
+ @Test
+ public void infoAtInfoWithParameters() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.info("hello, {}", "world");
+
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ assertThat(log1.getMessage()).isEqualTo("hello, world");
+ assertThat(log1.getParameters()).isNull();
+ assertThat(log1.getThrown()).isNull();
+ }
+
+ @Test
+ public void errorAtSevereWithException() {
+ julLogger.setLevel(Level.SEVERE);
+ log4jLogger.error("hello, {}", "world", new IOException("Testing, testing"));
+
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ assertThat(log1.getMessage()).isEqualTo("hello, world");
+ assertThat(log1.getParameters()).isNull();
+ assertThat(log1.getThrown()).isInstanceOf(IOException.class);
+ }
+
+ @Test
+ public void infoAtInfoWithLogBuilder() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.atInfo().log("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void infoAtInfoOnParent() {
+ julLogger.getParent().setLevel(Level.INFO);
+ log4jLogger.info("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void infoWithoutAnyLevel() {
+ // We're not setting any level.
+ log4jLogger.info("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void debugAtInfo() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.debug("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+ }
+
+ @Test
+ public void debugAtFiner() {
+ julLogger.setLevel(Level.FINER);
+ log4jLogger.debug("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void traceAtFine() {
+ julLogger.setLevel(Level.FINE);
+ log4jLogger.trace("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+ }
+
+ @Test
+ public void traceAtAllOnParent() {
+ julLogger.getParent().setLevel(Level.ALL);
+ log4jLogger.trace("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void fatalAtOff() {
+ julLogger.getParent().setLevel(Level.OFF);
+ log4jLogger.fatal("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+ }
+
+ @Test
+ public void fatalAtSevere() {
+ julLogger.getParent().setLevel(Level.SEVERE);
+ log4jLogger.atFatal().log("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @Test
+ public void warnAtFatal() {
+ julLogger.getParent().setLevel(Level.SEVERE);
+ log4jLogger.atWarn().log("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+ }
+
+ @Test
+ public void customLevelJustUnderWarning() {
+ julLogger.getParent().setLevel(new CustomLevel("Just under Warning", Level.WARNING.intValue() - 1));
+
+ log4jLogger.info("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+
+ log4jLogger.warn("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+
+ log4jLogger.error("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(2);
+ }
+
+ @Test
+ public void customLevelJustAboveWarning() {
+ julLogger.getParent().setLevel(new CustomLevel("Just above Warning", Level.WARNING.intValue() + 1));
+
+ log4jLogger.info("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+
+ log4jLogger.warn("hello, world");
+ assertThat(handler.getStoredLogRecords()).isEmpty();
+
+ log4jLogger.error("hello, world");
+ assertThat(handler.getStoredLogRecords()).hasSize(1);
+ }
+
+ @SuppressWarnings("serial")
+ private static class CustomLevel extends Level {
+ CustomLevel(final String name, final int value) {
+ super(name, value);
+ }
+ }
+
+ /**
+ * Test that the {@link LogRecord#getSourceClassName()}, which we already tested above in infoAtInfo()
+ * also works as expected if the logging happened in a class that we have called (indirect), not in the test method itself.
+ */
+ @Test
+ public void indirectSource() {
+ java.util.logging.Logger.getLogger(Another.class.getName()).setLevel(Level.INFO);
+ new Another(handler);
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ assertThat(log1.getSourceClassName()).isEqualTo(Another.class.getName());
+ assertThat(log1.getSourceMethodName()).isEqualTo("");
+ }
+
+ static class Another {
+ org.apache.logging.log4j.Logger anotherLog4jLogger = LogManager.getLogger(getClass());
+ java.util.logging.Logger anotherJULLogger =
+ java.util.logging.Logger.getLogger(getClass().getName());
+
+ Another(final TestLogHandler handler) {
+ anotherJULLogger.addHandler(handler);
+ anotherLog4jLogger.info("hello, another world");
+ }
+ }
+
+ @Test
+ public void placeholdersInFormat() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.info("hello, {0} {}", "world");
+
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ final String formattedMessage = new java.util.logging.SimpleFormatter().formatMessage(log1);
+ assertThat(formattedMessage).isEqualTo("hello, {0} world");
+ }
+
+ @Test
+ public void placeholdersInFormattedMessage() {
+ julLogger.setLevel(Level.INFO);
+ log4jLogger.info("hello, {}", "{0} world");
+
+ final List logs = handler.getStoredLogRecords();
+ assertThat(logs).hasSize(1);
+ final LogRecord log1 = logs.get(0);
+ final String formattedMessage = new java.util.logging.SimpleFormatter().formatMessage(log1);
+ assertThat(formattedMessage).isEqualTo("hello, {0} world");
+ }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index b7a4e2d..65886d1 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -31,22 +31,85 @@
https://logging.apache.org/log4j/2.x/manual/installation.html
+
+ false
+ 3.26.3
+ 7.0.0
+ 2.2
+ 1.0.0
+ 5.11.3
2.24.1
+ 2.0.0
+ 1.1.2
+
+ org.hamcrest
+ hamcrest
+ ${hamcrest.version}
+
+
+
+ org.assertj
+ assertj-bom
+ ${assertj.version}
+ pom
+ import
+
+
org.apache.logging.log4j
- log4j-api
+ log4j-bom
${log4j.version}
+ pom
+ import
+
+
+
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
+
+
+
+ biz.aQute.bnd
+ biz.aQute.bnd.annotation
+ ${bnd.annotation.version}
+ provided
+
+
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+ ${osgi.bundle.version}
+ provided
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ ${osgi.versioning.version}
+
+
+
+
diff --git a/pom.xml b/pom.xml
index ddca29e..65a200b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,7 @@
parent
+ log4j-api-to-jul
@@ -97,7 +98,15 @@
-
+
+
+
+ org.apache.logging.log4j
+ log4j-api-to-jul
+ ${project.version}
+
+
+
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
new file mode 100644
index 0000000..b0c83f0
--- /dev/null
+++ b/spotbugs-exclude.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+