package dmg.util.logback;
import ch.qos.logback.classic.Level;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import org.slf4j.Logger;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* This class maintains a set of filter thresholds. Two types of inheritance
* are supported:
*
* - Inheritance from a parent FilterThresholds
* - Inheritance from parent loggers
*
* If a threshold is not defined for a given combination of logger and
* appender, then the parent FilterThresholds is consulted recursively.
* If not defined in the parent, the threshold of the parent logger is
* used recursively.
*
* The thus calculated effective log level is cached. The cache is invalidated
* if the set of thresholds is modified, but it is not invalidated if the
* thresholds in the parent set are modified. Thus once cached, only newly
* created cells will inherit the thresholds from its parent cell (this is mostly
* to simplify the design).
*/
public class FilterThresholdSet
{
private final FilterThresholdSet _parent;
private final Set<String> _appenders = Sets.newHashSet();
private final Set<LoggerName> _roots = new HashSet<>();
/* Logger x Appender -> Level */
private final Table<LoggerName, String, Level> _rules = HashBasedTable.create();
/* Logger -> (Appender -> Level) */
private final LoadingCache<String,Map<String,Level>> _effectiveMaps =
CacheBuilder.newBuilder().build(CacheLoader.from(
logger -> computeEffectiveMap(LoggerName.getInstance(logger))));
/* Logger -> Level */
private final LoadingCache<Logger,Optional<Level>> _effectiveLevels =
CacheBuilder.newBuilder().build(CacheLoader.from(
logger -> {
try {
Map<String,Level> map = _effectiveMaps.get(logger.getName());
return map.isEmpty()
? Optional.<Level>absent()
: Optional.of(Collections.min(map.values(), LEVEL_ORDER));
} catch (ExecutionException e) {
Throwables.throwIfUnchecked(e.getCause());
throw new RuntimeException(e.getCause());
}
}));
private static final Comparator<Level> LEVEL_ORDER =
(o1, o2) -> Integer.compare(o1.toInt(), o2.toInt());
public FilterThresholdSet()
{
this(null);
}
public FilterThresholdSet(FilterThresholdSet parent)
{
_parent = parent;
}
/**
* Adds an appender, which will become available for threshold definitions.
*/
public synchronized void addAppender(String name)
{
checkNotNull(name);
_appenders.add(name);
}
/**
* Returns the list of appenders available for threshold definitions. This
* is the union of the appenders of the parents thresholds and the appenders
* of these thresholds.
*/
public synchronized Collection<String> getAppenders()
{
if (_parent == null) {
return Lists.newArrayList(_appenders);
} else {
Collection<String> appenders = _parent.getAppenders();
appenders.addAll(_appenders);
return appenders;
}
}
/**
* Returns whether the appender is valid is valid for use in a threshold
* definition.
*/
public synchronized boolean hasAppender(String appender)
{
return _appenders.contains(appender) ||
(_parent != null && _parent.hasAppender(appender));
}
/**
* Returns the threshold of the given logger and appender combination. Neither the
* parent thresholds nor the parent loggers are taken into account.
*/
public synchronized Level get(LoggerName logger, String appender)
{
return _rules.get(logger, appender);
}
/**
* Sets a threshold for the given logger and appender.
*/
public synchronized void setThreshold(LoggerName logger, String appender, Level level)
{
checkNotNull(logger);
checkNotNull(level);
checkArgument(hasAppender(appender));
_rules.put(logger, appender, level);
clearCache();
}
/**
* Returns whether the logger is marked additive.
*/
public synchronized boolean isRoot(LoggerName logger)
{
return _parent != null && _parent.isRoot(logger) || _roots.contains(logger);
}
/**
* Sets whether a logger is additive. Non-additive loggers form roots of the logging
* hierarchy as log messages do not propagate to appenders attached to any of the
* parent loggers.
*/
public synchronized void setRoot(LoggerName logger, boolean isRoot)
{
if (isRoot) {
_roots.add(logger);
} else {
_roots.remove(logger);
}
clearCache();
}
/**
* Removes the threshold of the given logger and appender combination in this
* threshold set. The new effective threshold will be derived from the regular
* inheritance rules.
*/
public synchronized void remove(LoggerName logger, String appender)
{
if (_rules.remove(logger, appender) != null) {
clearCache();
}
}
/**
* Removes all thresholds from this set.
*/
public synchronized void clear()
{
_rules.clear();
clearCache();
}
/**
* Wipes the cache of computed effective thresholds. Called whenever any of the thresholds
* have been updated.
*/
private void clearCache()
{
_effectiveMaps.invalidateAll();
_effectiveLevels.invalidateAll();
}
/**
* Returns a map from appenders to levels for a logger.
*
* The map contains inherited levels from parent filter threshold sets.
*/
public synchronized Map<String,Level> getInheritedMap(LoggerName logger)
{
if (_parent == null) {
return Maps.newHashMap(_rules.row(logger));
} else {
Map<String,Level> map = _parent.getInheritedMap(logger);
map.putAll(_rules.row(logger));
return map;
}
}
/**
* Returns a map from appenders to levels for a logger.
*
* The map contains the effective log levels, that is, the levels
* used for filtering log events.
*/
private synchronized Map<String,Level> computeEffectiveMap(LoggerName logger)
{
Map<String,Level> inheritedMap = getInheritedMap(logger);
if (!isRoot(logger)) {
LoggerName parent = logger.getParent();
if (parent != null) {
Map<String, Level> mergedMap = computeEffectiveMap(parent);
mergedMap.putAll(inheritedMap);
return mergedMap;
}
}
return inheritedMap;
}
/**
* Returns the effective log threshold for a given pair of logger and appender.
*/
public Level getThreshold(LoggerName logger, String appender)
{
return getThreshold(logger.toString(), appender);
}
/**
* Returns the effective log threshold for a given pair of logger and appender.
*/
public Level getThreshold(String logger, String appender)
{
try {
return _effectiveMaps.get(logger).get(appender);
} catch (ExecutionException e) {
Throwables.throwIfUnchecked(e.getCause());
throw new RuntimeException(e.getCause());
}
}
/**
* Gets the effective minimum threshold for a given pair regardless of appender.
*/
public Level getThreshold(Logger logger)
{
try {
return _effectiveLevels.get(logger).orNull();
} catch (ExecutionException e) {
Throwables.throwIfUnchecked(e.getCause());
throw new RuntimeException(e.getCause());
}
}
}