diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java index 53512451e35..2cd36d8ab78 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java @@ -27,6 +27,8 @@ /** * Responsible for initializing the context data of LogEvents. Context data is data that is set by the application to be * included in all subsequent log events. + *

NOTE: It is no longer recommended that custom implementations of this interface be provided as it is + * difficult to do. Instead, provide a custom ContextDataProvider.

*

* The source of the context data is implementation-specific. The default source for context data is the ThreadContext. *

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java index 050d1e0eb07..74925f6cb90 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java @@ -48,6 +48,10 @@ public class ContextDataInjectorFactory { * {@code ContextDataInjector} classes defined in {@link ThreadContextDataInjector} which is most appropriate for * the ThreadContext implementation. *

+ * Note: It is no longer recommended that users provide a custom implementation of the ContextDataInjector. + * Instead, provide a {@code ContextDataProvider}. + *

+ *

* Users may use this system property to specify the fully qualified class name of a class that implements the * {@code ContextDataInjector} interface. *

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java index a24fcb9e35f..f1cd4bc1add 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java @@ -30,7 +30,7 @@ /** * Provides a read-only {@code StringMap} view of a {@code Map}. */ -class JdkMapAdapterStringMap implements StringMap { +public class JdkMapAdapterStringMap implements StringMap { private static final long serialVersionUID = -7348247784983193612L; private static final String FROZEN = "Frozen collection cannot be modified"; private static final Comparator NULL_FIRST_COMPARATOR = new Comparator() { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java index 25f1d031419..0522ecac11a 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java @@ -16,14 +16,22 @@ */ package org.apache.logging.log4j.core.impl; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentLinkedDeque; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.ContextDataInjector; import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.util.ContextDataProvider; import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.LoaderUtil; import org.apache.logging.log4j.util.ReadOnlyStringMap; import org.apache.logging.log4j.util.StringMap; @@ -44,6 +52,14 @@ */ public class ThreadContextDataInjector { + private static Logger LOGGER = StatusLogger.getLogger(); + + /** + * ContextDataProviders loaded via OSGi. + */ + public static Collection contextDataProviders = + new ConcurrentLinkedDeque<>(); + /** * Default {@code ContextDataInjector} for the legacy {@code Map}-based ThreadContext (which is * also the ThreadContext implementation used for web applications). @@ -52,24 +68,39 @@ public class ThreadContextDataInjector { */ public static class ForDefaultThreadContextMap implements ContextDataInjector { + private final List providers; + + public ForDefaultThreadContextMap() { + providers = getProviders(); + } + /** * Puts key-value pairs from both the specified list of properties as well as the thread context into the * specified reusable StringMap. * * @param props list of configuration properties, may be {@code null} - * @param ignore a {@code StringMap} instance from the log event + * @param contextData a {@code StringMap} instance from the log event * @return a {@code StringMap} combining configuration properties with thread context data */ @Override - public StringMap injectContextData(final List props, final StringMap ignore) { + public StringMap injectContextData(final List props, final StringMap contextData) { - final Map copy = ThreadContext.getImmutableContext(); + final Map copy; + + if (providers.size() == 1) { + copy = providers.get(0).supplyContextData(); + } else { + copy = new HashMap<>(); + for (ContextDataProvider provider : providers) { + copy.putAll(provider.supplyContextData()); + } + } // The DefaultThreadContextMap stores context data in a Map. // This is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy. - // If there are no configuration properties returning a thin wrapper around the copy + // If there are no configuration properties or providers returning a thin wrapper around the copy // is faster than copying the elements into the LogEvent's reusable StringMap. - if (props == null || props.isEmpty()) { + if ((props == null || props.isEmpty())) { // this will replace the LogEvent's context data with the returned instance. // NOTE: must mark as frozen or downstream components may attempt to modify (UnsupportedOperationEx) return copy.isEmpty() ? ContextDataFactory.emptyFrozenContextData() : frozenStringMap(copy); @@ -114,6 +145,12 @@ public ReadOnlyStringMap rawContextData() { * This injector always puts key-value pairs into the specified reusable StringMap. */ public static class ForGarbageFreeThreadContextMap implements ContextDataInjector { + private final List providers; + + public ForGarbageFreeThreadContextMap() { + this.providers = getProviders(); + } + /** * Puts key-value pairs from both the specified list of properties as well as the thread context into the * specified reusable StringMap. @@ -128,9 +165,9 @@ public StringMap injectContextData(final List props, final StringMap r // StringMap. We cannot return the ThreadContext's internal data structure because it may be modified later // and such modifications should not be reflected in the log event. copyProperties(props, reusable); - - final ReadOnlyStringMap immutableCopy = ThreadContext.getThreadContextMap().getReadOnlyContextData(); - reusable.putAll(immutableCopy); + for (int i = 0; i < providers.size(); ++i) { + reusable.putAll(providers.get(i).supplyStringMap()); + } return reusable; } @@ -149,6 +186,11 @@ public ReadOnlyStringMap rawContextData() { * specified reusable StringMap. */ public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector { + private final List providers; + + public ForCopyOnWriteThreadContextMap() { + this.providers = getProviders(); + } /** * If there are no configuration properties, this injector will return the thread context's internal data * structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the @@ -162,17 +204,25 @@ public static class ForCopyOnWriteThreadContextMap implements ContextDataInjecto public StringMap injectContextData(final List props, final StringMap ignore) { // If there are no configuration properties we want to just return the ThreadContext's StringMap: // it is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy. - final StringMap immutableCopy = ThreadContext.getThreadContextMap().getReadOnlyContextData(); - if (props == null || props.isEmpty()) { - return immutableCopy; // this will replace the LogEvent's context data with the returned instance + if (providers.size() == 1 && (props == null || props.isEmpty())) { + // this will replace the LogEvent's context data with the returned instance + return providers.get(0).supplyStringMap(); + } + int count = props.size(); + StringMap[] maps = new StringMap[providers.size()]; + for (int i = 0; i < providers.size(); ++i) { + maps[i] = providers.get(i).supplyStringMap(); + count += maps[i].size(); } // However, if the list of Properties is non-empty we need to combine the properties and the ThreadContext // data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined // and others not, so the LogEvent's context data may have been replaced with an immutable copy from // the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it. - final StringMap result = ContextDataFactory.createContextData(props.size() + immutableCopy.size()); + final StringMap result = ContextDataFactory.createContextData(count); copyProperties(props, result); - result.putAll(immutableCopy); + for (StringMap map : maps) { + result.putAll(map); + } return result; } @@ -196,4 +246,20 @@ public static void copyProperties(final List properties, final StringM } } } + + private static List getProviders() { + final List providers = new ArrayList<>(contextDataProviders); + for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) { + try { + for (final ContextDataProvider provider : ServiceLoader.load(ContextDataProvider.class, classLoader)) { + if (providers.stream().noneMatch((p) -> p.getClass().isAssignableFrom(provider.getClass()))) { + providers.add(provider); + } + } + } catch (final Throwable ex) { + LOGGER.debug("Unable to access Context Data Providers {}", ex.getMessage()); + } + } + return providers; + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java new file mode 100644 index 00000000000..230123e3cbd --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java @@ -0,0 +1,39 @@ +/* + * 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.core.impl; + +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.util.ContextDataProvider; +import org.apache.logging.log4j.util.StringMap; + +import java.util.Map; + +/** + * ContextDataProvider for ThreadContext data. + */ +public class ThreadContextDataProvider implements ContextDataProvider { + + @Override + public Map supplyContextData() { + return ThreadContext.getImmutableContext(); + } + + @Override + public StringMap supplyStringMap() { + return ThreadContext.getThreadContextMap().getReadOnlyContextData(); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java index 4671f6332d1..97aea84fa3f 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java @@ -17,20 +17,26 @@ package org.apache.logging.log4j.core.osgi; +import java.util.Collection; import java.util.Hashtable; import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.impl.Log4jProvider; +import org.apache.logging.log4j.core.impl.ThreadContextDataInjector; +import org.apache.logging.log4j.core.impl.ThreadContextDataProvider; import org.apache.logging.log4j.core.plugins.Log4jPlugins; import org.apache.logging.log4j.core.util.Constants; +import org.apache.logging.log4j.core.util.ContextDataProvider; import org.apache.logging.log4j.plugins.processor.PluginService; import org.apache.logging.log4j.spi.Provider; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.PropertiesUtil; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; /** @@ -44,6 +50,7 @@ public final class Activator implements BundleActivator { ServiceRegistration provideRegistration = null; ServiceRegistration pluginRegistration = null; + ServiceRegistration contextDataRegistration = null; @Override public void start(final BundleContext context) throws Exception { @@ -52,7 +59,11 @@ public void start(final BundleContext context) throws Exception { final Provider provider = new Log4jProvider(); final Hashtable props = new Hashtable<>(); props.put("APIVersion", "2.60"); + final ContextDataProvider threadContextProvider = new ThreadContextDataProvider(); provideRegistration = context.registerService(Provider.class.getName(), provider, props); + contextDataRegistration = context.registerService(ContextDataProvider.class.getName(), threadContextProvider, + null); + loadContextProviders(context); // allow the user to override the default ContextSelector (e.g., by using BasicContextSelector for a global cfg) if (PropertiesUtil.getProperties().getStringProperty(Constants.LOG4J_CONTEXT_SELECTOR) == null) { System.setProperty(Constants.LOG4J_CONTEXT_SELECTOR, BundleContextSelector.class.getName()); @@ -64,7 +75,21 @@ public void start(final BundleContext context) throws Exception { public void stop(final BundleContext context) throws Exception { provideRegistration.unregister(); pluginRegistration.unregister(); + contextDataRegistration.unregister(); this.contextRef.compareAndSet(context, null); LogManager.shutdown(false, true); } + + private static void loadContextProviders(final BundleContext bundleContext) { + try { + final Collection> serviceReferences = + bundleContext.getServiceReferences(ContextDataProvider.class, null); + for (final ServiceReference serviceReference : serviceReferences) { + final ContextDataProvider provider = bundleContext.getService(serviceReference); + ThreadContextDataInjector.contextDataProviders.add(provider); + } + } catch (final InvalidSyntaxException ex) { + LOGGER.error("Error accessing context data provider", ex); + } + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java new file mode 100644 index 00000000000..d302cf8404b --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java @@ -0,0 +1,42 @@ +/* + * 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.core.util; + +import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap; +import org.apache.logging.log4j.util.StringMap; + +import java.util.Map; + +/** + * Source of context data to be added to each log event. + */ +public interface ContextDataProvider { + + /** + * Returns a Map containing context data to be injected into the event or null if no context data is to be added. + * @return A Map containing the context data or null. + */ + Map supplyContextData(); + + /** + * Returns the context data as a StringMap. + * @return the context data in a StringMap. + */ + default StringMap supplyStringMap() { + return new JdkMapAdapterStringMap(supplyContextData()); + } +} diff --git a/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider b/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider new file mode 100644 index 00000000000..7917658d6db --- /dev/null +++ b/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider @@ -0,0 +1 @@ +org.apache.logging.log4j.core.impl.ThreadContextDataProvider \ No newline at end of file diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java new file mode 100644 index 00000000000..f52c87dcbdc --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java @@ -0,0 +1,74 @@ +/* + * 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.core.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.core.impl.ThreadContextDataInjector; +import org.apache.logging.log4j.test.appender.ListAppender; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * + */ +public class ContextDataProviderTest { + + private static Logger logger; + private static ListAppender appender; + + @BeforeClass + public static void beforeClass() { + ThreadContextDataInjector.contextDataProviders.add(new TestContextDataProvider()); + System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "target/test-classes/log4j-contextData.xml"); + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + logger = loggerContext.getLogger(ContextDataProviderTest.class.getName()); + appender = loggerContext.getConfiguration().getAppender("List"); + assertNotNull("No List appender", appender); + } + + @Test + public void testContextProvider() throws Exception { + ThreadContext.put("loginId", "jdoe"); + logger.debug("This is a test"); + List messages = appender.getMessages(); + assertEquals("Incorrect number of messages", 1, messages.size()); + assertTrue("Context data missing", messages.get(0).contains("testKey=testValue")); + } + + private static class TestContextDataProvider implements ContextDataProvider { + + @Override + public Map supplyContextData() { + Map contextData = new HashMap<>(); + contextData.put("testKey", "testValue"); + return contextData; + } + + } +} diff --git a/log4j-core/src/test/resources/log4j-list.xml b/log4j-core/src/test/resources/log4j-contextData.xml similarity index 87% rename from log4j-core/src/test/resources/log4j-list.xml rename to log4j-core/src/test/resources/log4j-contextData.xml index c2a92c3054f..80e83eefb08 100644 --- a/log4j-core/src/test/resources/log4j-list.xml +++ b/log4j-core/src/test/resources/log4j-contextData.xml @@ -15,10 +15,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + - + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7c6e6533f08..7bcbf2eea16 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -165,7 +165,12 @@ Update Apache Flume from 1.8.0 to 1.9.0. - + + + Add ContextDataProviders as an alternative to having to implement a ContextDataInjector. + + + Slow initialization on Windows due to accessing network interfaces. diff --git a/src/site/asciidoc/manual/extending.adoc b/src/site/asciidoc/manual/extending.adoc index c68668bc4e3..6997928a7ae 100644 --- a/src/site/asciidoc/manual/extending.adoc +++ b/src/site/asciidoc/manual/extending.adoc @@ -567,25 +567,17 @@ ListAppender list1 = ListAppender.createAppender("List1", true, false, null, nul ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLine(true).build(); ---- -[#Custom_ContextDataInjector] -== Custom ContextDataInjector - -The `ContextDataInjector` (introduced in Log4j 2.7) is responsible for -populating the LogEvent's -link:../log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextData()[context -data] with key-value pairs or replacing it completely. The default -implementation is `ThreadContextDataInjector`, which obtains context -attributes from the ThreadContext. - -Applications may replace the default `ContextDataInjector` by setting the -value of the system property `log4j2.contextDataInjector` to the name of -the custom `ContextDataInjector` class. - -Implementors should be aware there are some subtleties related to -thread-safety and implementing a context data injector in a garbage-free -manner. See the -link:../log4j-core/apidocs/org/apache/logging/log4j/core/ContextDataInjector.html[`ContextDataInjector`] -javadoc for detail. +[#Custom_ContextDataProvider] +== Custom ContextDataProvider + + +The `ContextDataProvider` (introduced in Log4j 2.13.2) is an interface applications and libraries can use to inject +additional key-value pairs into the LogEvent's context data. Log4j's `ThreadContextDataInjector` uses +`java.util.ServiceLoader` to locate and load `ContextDataProvider` instances. Log4j itself adds the ThreadContext data +to the LogEvent using `org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations +should implement the `org.apache.logging.log4j.core.util.ContextDataProvider`interfaceand declare it as a service by +defining the implmentation class in a file named +`META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider`. == Custom ThreadContextMap implementations