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: + * + * + * 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 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 @@ + + + + + + + + + + + + + + + + + +