Skip to content

Commit

Permalink
LOG4J2-2779 - Add ContextDataProviders as an alternative to having to…
Browse files Browse the repository at this point in the history
… implement a ContextDataInjector.
  • Loading branch information
rgoers committed Mar 3, 2020
1 parent 56c204d commit c4c2808
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 36 deletions.
Expand Up @@ -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.
* <p><b>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.</b></p>
* <p>
* The source of the context data is implementation-specific. The default source for context data is the ThreadContext.
* </p><p>
Expand Down
Expand Up @@ -48,6 +48,10 @@ public class ContextDataInjectorFactory {
* {@code ContextDataInjector} classes defined in {@link ThreadContextDataInjector} which is most appropriate for
* the ThreadContext implementation.
* <p>
* <b>Note:</b> It is no longer recommended that users provide a custom implementation of the ContextDataInjector.
* Instead, provide a {@code ContextDataProvider}.
* </p>
* <p>
* Users may use this system property to specify the fully qualified class name of a class that implements the
* {@code ContextDataInjector} interface.
* </p><p>
Expand Down
Expand Up @@ -30,7 +30,7 @@
/**
* Provides a read-only {@code StringMap} view of a {@code Map<String, String>}.
*/
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<? super String> NULL_FIRST_COMPARATOR = new Comparator<String>() {
Expand Down
Expand Up @@ -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;

Expand All @@ -44,6 +52,14 @@
*/
public class ThreadContextDataInjector {

private static Logger LOGGER = StatusLogger.getLogger();

/**
* ContextDataProviders loaded via OSGi.
*/
public static Collection<ContextDataProvider> contextDataProviders =
new ConcurrentLinkedDeque<>();

/**
* Default {@code ContextDataInjector} for the legacy {@code Map<String, String>}-based ThreadContext (which is
* also the ThreadContext implementation used for web applications).
Expand All @@ -52,24 +68,39 @@ public class ThreadContextDataInjector {
*/
public static class ForDefaultThreadContextMap implements ContextDataInjector {

private final List<ContextDataProvider> 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<Property> props, final StringMap ignore) {
public StringMap injectContextData(final List<Property> props, final StringMap contextData) {

final Map<String, String> copy = ThreadContext.getImmutableContext();
final Map<String, String> 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<String, String>.
// 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);
Expand Down Expand Up @@ -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<ContextDataProvider> 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.
Expand All @@ -128,9 +165,9 @@ public StringMap injectContextData(final List<Property> 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;
}

Expand All @@ -149,6 +186,11 @@ public ReadOnlyStringMap rawContextData() {
* specified reusable StringMap.
*/
public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector {
private final List<ContextDataProvider> 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
Expand All @@ -162,17 +204,25 @@ public static class ForCopyOnWriteThreadContextMap implements ContextDataInjecto
public StringMap injectContextData(final List<Property> 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;
}

Expand All @@ -196,4 +246,20 @@ public static void copyProperties(final List<Property> properties, final StringM
}
}
}

private static List<ContextDataProvider> getProviders() {
final List<ContextDataProvider> 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;
}
}
@@ -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<String, String> supplyContextData() {
return ThreadContext.getImmutableContext();
}

@Override
public StringMap supplyStringMap() {
return ThreadContext.getThreadContextMap().getReadOnlyContextData();
}
}
Expand Up @@ -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;

/**
Expand All @@ -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 {
Expand All @@ -52,7 +59,11 @@ public void start(final BundleContext context) throws Exception {
final Provider provider = new Log4jProvider();
final Hashtable<String, String> 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());
Expand All @@ -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<ServiceReference<ContextDataProvider>> serviceReferences =
bundleContext.getServiceReferences(ContextDataProvider.class, null);
for (final ServiceReference<ContextDataProvider> serviceReference : serviceReferences) {
final ContextDataProvider provider = bundleContext.getService(serviceReference);
ThreadContextDataInjector.contextDataProviders.add(provider);
}
} catch (final InvalidSyntaxException ex) {
LOGGER.error("Error accessing context data provider", ex);
}
}
}
@@ -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<String, String> supplyContextData();

/**
* Returns the context data as a StringMap.
* @return the context data in a StringMap.
*/
default StringMap supplyStringMap() {
return new JdkMapAdapterStringMap(supplyContextData());
}
}
@@ -0,0 +1 @@
org.apache.logging.log4j.core.impl.ThreadContextDataProvider

0 comments on commit c4c2808

Please sign in to comment.