Index: log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java (revision 1600802) +++ log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java (working copy) @@ -17,6 +17,8 @@ package org.apache.logging.log4j.simple; import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; import java.io.PrintStream; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -34,7 +36,7 @@ /** * This is the default logger that is used when no suitable logging implementation is available. */ -public class SimpleLogger extends AbstractLogger { +public class SimpleLogger extends AbstractLogger implements Closeable { private static final long serialVersionUID = 1L; @@ -178,4 +180,9 @@ this.stream = stream; } + @Override + public void close() throws IOException { + this.stream.close(); + } + } Index: log4j-api/src/main/java/org/apache/logging/log4j/status/StatusListener.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/status/StatusListener.java (revision 1600802) +++ log4j-api/src/main/java/org/apache/logging/log4j/status/StatusListener.java (working copy) @@ -16,24 +16,14 @@ */ package org.apache.logging.log4j.status; -import java.io.Closeable; - -import org.apache.logging.log4j.Level; - /** * Interface that allows implementers to be notified of events in the logging system. */ -public interface StatusListener extends Closeable { +public interface StatusListener { /** * Called as events occur to process the StatusData. * @param data The StatusData for the event. */ void log(StatusData data); - - /** - * Return the Log Level that this listener wants included. - * @return the Log Level. - */ - Level getStatusLevel(); } Index: log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java (revision 1600802) +++ log4j-api/src/main/java/org/apache/logging/log4j/status/StatusData.java (working copy) @@ -23,18 +23,20 @@ import java.util.Date; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.message.Message; /** * The Status data. */ -public class StatusData implements Serializable { +public class StatusData implements Serializable { // TODO: can this be removed? private static final long serialVersionUID = -4341916115118014017L; private final long timestamp; private final StackTraceElement caller; private final Level level; private final Message msg; + private final Marker marker; private final Throwable throwable; /** @@ -44,10 +46,11 @@ * @param msg The message String. * @param t The Error or Exception that occurred. */ - public StatusData(final StackTraceElement caller, final Level level, final Message msg, final Throwable t) { + public StatusData(final StackTraceElement caller, final Level level, final Marker marker, final Message msg, final Throwable t) { this.timestamp = System.currentTimeMillis(); this.caller = caller; this.level = level; + this.marker = marker; this.msg = msg; this.throwable = t; } @@ -75,6 +78,14 @@ public Level getLevel() { return level; } + + /** + * Returns the logging marker for the event. + * @return The logging marker. + */ + public Marker getMarker() { + return marker; + } /** * Returns the message associated with the event. Index: log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java (revision 1600802) +++ log4j-api/src/main/java/org/apache/logging/log4j/status/StatusConsoleListener.java (working copy) @@ -1,113 +0,0 @@ -/* - * 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.status; - -import java.io.IOException; -import java.io.PrintStream; - -import org.apache.logging.log4j.Level; - -/** - * StatusListener that writes to the Console. - */ -@SuppressWarnings("UseOfSystemOutOrSystemErr") -public class StatusConsoleListener implements StatusListener { - - private Level level = Level.FATAL; - private String[] filters = null; - private final PrintStream stream; - - /** - * Creates the StatusConsoleListener using the supplied Level. - * @param level The Level of status messages that should appear on the console. - */ - public StatusConsoleListener(final Level level) { - this(level, System.out); - } - - /** - * Creates the StatusConsoleListener using the supplied Level. Make sure not to use a logger stream of some sort - * to avoid creating an infinite loop of indirection! - * @param level The Level of status messages that should appear on the console. - * @param stream The PrintStream to write to. - * @throws IllegalArgumentException if the PrintStream argument is {@code null}. - */ - public StatusConsoleListener(final Level level, final PrintStream stream) { - if (stream == null) { - throw new IllegalArgumentException("You must provide a stream to use for this listener."); - } - this.level = level; - this.stream = stream; - } - - /** - * Sets the level to a new value. - * @param level The new Level. - */ - public void setLevel(final Level level) { - this.level = level; - } - - /** - * Return the Log Level for which the Listener should receive events. - * @return the Log Level. - */ - @Override - public Level getStatusLevel() { - return this.level; - } - - /** - * Writes status messages to the console. - * @param data The StatusData. - */ - @Override - public void log(final StatusData data) { - if (!filtered(data)) { - stream.println(data.getFormattedStatus()); - } - } - - /** - * Adds package name filters to exclude. - * @param filters An array of package names to exclude. - */ - public void setFilters(final String... filters) { - this.filters = filters; - } - - private boolean filtered(final StatusData data) { - if (filters == null) { - return false; - } - final String caller = data.getStackTraceElement().getClassName(); - for (final String filter : filters) { - if (caller.startsWith(filter)) { - return true; - } - } - return false; - } - - @Override - public void close() throws IOException { - // only want to close non-system streams - if (this.stream != System.out && this.stream != System.err) { - this.stream.close(); - } - } -} Index: log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java (revision 1600802) +++ log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java (working copy) @@ -17,17 +17,21 @@ package org.apache.logging.log4j.status; import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; @@ -45,39 +49,35 @@ private static final long serialVersionUID = 2L; /** - * System property that can be configured with the number of entries in the queue. Once the limit - * is reached older entries will be removed as new entries are added. + * System property that can be configured with the number of entries in the queue. Once the + * limit is reached older entries will be removed as new entries are added. */ public static final String MAX_STATUS_ENTRIES = "log4j2.status.entries"; private static final String NOT_AVAIL = "?"; - private static final PropertiesUtil PROPS = new PropertiesUtil("log4j2.StatusLogger.properties"); - private static final int MAX_ENTRIES = PROPS.getIntegerProperty(MAX_STATUS_ENTRIES, 200); - private static final String DEFAULT_STATUS_LEVEL = PROPS.getStringProperty("log4j2.StatusLogger.level"); - private static final StatusLogger STATUS_LOGGER = new StatusLogger(); - private final SimpleLogger logger; - - private final Collection listeners = new CopyOnWriteArrayList(); - private final ReadWriteLock listenersLock = new ReentrantReadWriteLock(); - + private final Lock lock = new ReentrantLock(); + private final LoggerFilters sysOut; + private final LoggerFilters sysErr; + private final Map files = new HashMap(); + private final Collection listeners = new CopyOnWriteArrayList(); private final Queue messages = new BoundedQueue(MAX_ENTRIES); - private final Lock msgLock = new ReentrantLock(); - private int listenersLevel; + private Level defaultLogLevel = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN); + private Level registeredLogLevel = null; private StatusLogger() { - this.logger = new SimpleLogger("StatusLogger", Level.ERROR, false, true, false, false, Strings.EMPTY, null, PROPS, - System.err); - this.listenersLevel = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel(); + sysErr = new LoggerFilters("System.err", defaultLogLevel, System.err); + sysOut = new LoggerFilters("System.out", Level.OFF, System.out); } /** * Retrieve the StatusLogger. + * * @return The StatusLogger. */ public static StatusLogger getLogger() { @@ -85,68 +85,169 @@ } public void setLevel(final Level level) { - logger.setLevel(level); + lock.lock(); + try { + defaultLogLevel = level; + if (!hasRegistrations()) { + sysErr.logger.setLevel(level); + } + } finally { + lock.unlock(); + } + } + + public void registerSystemOutFilter(final StatusFilter filter, final Level level) { + registerFilter(sysOut, filter, level); + } + + public boolean removeSystemOutFilter(final StatusFilter filter) { + return removeFilter(sysOut, filter); + } + + public void registerSystemErrFilter(final StatusFilter filter, final Level level) { + registerFilter(sysErr, filter, level); + } + + public boolean removeSystemErrFilter(final StatusFilter filter) { + return removeFilter(sysErr, filter); + } + + public void registerFileFilter(final File file, final StatusFilter filter, final Level level) throws FileNotFoundException { + lock.lock(); + try { + LoggerFilters filtered = files.get(file); + if (filtered == null) { + filtered = new LoggerFilters(file.toString(), level, new PrintStream(file)); + files.put(file, filtered); + } + registerFilter(filtered, filter, level); + } finally { + lock.unlock(); + } + } + + public boolean removeFileFilter(final File file, final StatusFilter filter) { + lock.lock(); + try { + LoggerFilters filtered = files.get(file); + if (filtered != null && filtered.remove(filter)) { + if (filtered.isEmpty()) { + files.remove(file); + closeSilently(filtered); + } + updateRegisteredLevel(); + return true; + } + } finally { + lock.unlock(); + } + return false; + } + + private void registerFilter(final LoggerFilters filtered, final StatusFilter filter, final Level level) { + lock.lock(); + try { + filtered.add(filter, level); + updateRegisteredLevel(level); + } finally { + lock.unlock(); + } + } + + private boolean removeFilter(final LoggerFilters filtered, final StatusFilter filter) { + lock.lock(); + try { + if (filtered.remove(filter)) { + updateRegisteredLevel(); + return true; + } + } finally { + lock.unlock(); + } + return false; } /** * Register a new listener. + * * @param listener The StatusListener to register. */ - public void registerListener(final StatusListener listener) { - listenersLock.writeLock().lock(); + public void registerListener(final StatusListener listener, final Level level) { + lock.lock(); try { - listeners.add(listener); - Level lvl = listener.getStatusLevel(); - if (listenersLevel < lvl.intLevel()) { - listenersLevel = lvl.intLevel(); - } + listeners.add(new ListenerLevel(listener, level)); + updateRegisteredLevel(level); } finally { - listenersLock.writeLock().unlock(); + lock.unlock(); } } /** * Remove a StatusListener. + * * @param listener The StatusListener to remove. */ - public void removeListener(final StatusListener listener) { - closeSilently(listener); - listenersLock.writeLock().lock(); + public boolean removeListener(final StatusListener listener) { + lock.lock(); try { - listeners.remove(listener); - int lowest = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel(); - for (StatusListener l : listeners) { - int level = l.getStatusLevel().intLevel(); - if (lowest < level) { - lowest = level; + for (final Iterator i = listeners.iterator(); i.hasNext(); ) { + if (i.next().listener == listener) { + i.remove(); + updateRegisteredLevel(); + return true; } } - listenersLevel = lowest; } finally { - listenersLock.writeLock().unlock(); + lock.unlock(); } + return false; } - /** - * Returns a thread safe Iterable for the StatusListener. - * @return An Iterable for the list of StatusListeners. - */ - public Iterable getListeners() { - return listeners; + private boolean hasRegistrations() { + return registeredLogLevel != null; + } + + private void updateRegisteredLevel(Level level) { + registeredLogLevel = leastSpecificOf(level, registeredLogLevel); + } + + private void updateRegisteredLevel() { + Level level = null; + for (ListenerLevel listener : listeners) { + level = leastSpecificOf(level, listener.level); + } + level = leastSpecificOf(level, sysOut.leastSpecificLevel()); + level = leastSpecificOf(level, sysErr.leastSpecificLevel()); + for (LoggerFilters file : files.values()) { + level = leastSpecificOf(level, file.leastSpecificLevel()); + } + registeredLogLevel = level; + } + + private static Level leastSpecificOf(Level first, Level second) { + if (first == null) { + return second; + } else if (second == null || first.isLessSpecificThan(second)) { + return first; + } + return second; } /** - * Clears the list of status events and listeners. + * Clears the list of status events, listeners and filters. */ public void reset() { - listenersLock.writeLock().lock(); + lock.lock(); try { - for (final StatusListener listener : listeners) { - closeSilently(listener); + listeners.clear(); + sysOut.filters.clear(); + sysErr.filters.clear(); + for (LoggerFilters file : files.values()) { + closeSilently(file); } + files.clear(); } finally { - listeners.clear(); - listenersLock.writeLock().unlock(); + lock.unlock(); // note this should certainly come after the unlock to avoid unnecessary nested locking clear(); } @@ -161,14 +262,15 @@ /** * Returns a List of all events as StatusData objects. + * * @return The list of StatusData objects. */ public List getStatusData() { - msgLock.lock(); + lock.lock(); try { return new ArrayList(messages); } finally { - msgLock.unlock(); + lock.unlock(); } } @@ -176,26 +278,27 @@ * Clears the list of status events. */ public void clear() { - msgLock.lock(); + lock.lock(); try { messages.clear(); } finally { - msgLock.unlock(); + lock.unlock(); } } @Override public Level getLevel() { - return logger.getLevel(); + return registeredLogLevel; } /** * Add an event. + * * @param marker The Marker - * @param fqcn The fully qualified class name of the caller - * @param level The logging level - * @param msg The message associated with the event. - * @param t A Throwable or null. + * @param fqcn The fully qualified class name of the caller + * @param level The logging level + * @param msg The message associated with the event. + * @param t A Throwable or null. */ @Override public void logMessage(final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable t) { @@ -203,21 +306,37 @@ if (fqcn != null) { element = getStackTraceElement(fqcn, Thread.currentThread().getStackTrace()); } - final StatusData data = new StatusData(element, level, msg, t); - msgLock.lock(); + final StatusData data = new StatusData(element, level, marker, msg, t); + lock.lock(); try { messages.add(data); } finally { - msgLock.unlock(); + lock.unlock(); } - if (listeners.size() > 0) { - for (final StatusListener listener : listeners) { - if (data.getLevel().isMoreSpecificThan(listener.getStatusLevel())) { - listener.log(data); + if (hasRegistrations()) { + logMessage(sysOut, fqcn, data); + logMessage(sysErr, fqcn, data); + for (LoggerFilters file : files.values()) { + logMessage(file, fqcn, data); + } + for (final ListenerLevel listener : listeners) { + if (listener.level.isLessSpecificThan(level)) { + listener.listener.log(data); } } } else { - logger.logMessage(fqcn, level, marker, msg, t); + sysErr.logger.logMessage(fqcn, level, marker, msg, t); + } + } + + private void logMessage(final LoggerFilters filtered, final String fqcn, final StatusData data) { + if (filtered.logger.isEnabled(data.getLevel())) { + for (FilterLevel filter : filtered.filters) { + if (filter.filter.isEnabled(data)) { + filtered.logger.logMessage(fqcn, data.getLevel(), data.getMarker(), data.getMessage(), data.getThrowable()); + break; + } + } } } @@ -267,14 +386,15 @@ @Override public boolean isEnabled(final Level level, final Marker marker) { - if (listeners.size() > 0) { - return listenersLevel >= level.intLevel(); + if (hasRegistrations()) { + return level.isMoreSpecificThan(registeredLogLevel); } - return logger.isEnabled(level, marker); + return sysErr.logger.isEnabled(level, marker); } /** * Queue for status events. + * * @param Object type to be stored in the queue. */ private class BoundedQueue extends ConcurrentLinkedQueue { @@ -289,10 +409,77 @@ @Override public boolean add(final E object) { - while (messages.size() > size) { - messages.poll(); + while (size() > size) { + poll(); } return super.add(object); } } + + private static class LoggerFilters implements Closeable { + final SimpleLogger logger; + final Collection filters = new CopyOnWriteArrayList(); + + LoggerFilters(final String name, final Level level, final PrintStream out) { + logger = new SimpleLogger(name, level, false, true, false, false, Strings.EMPTY, null, PROPS, out); + } + + boolean isEmpty() { + return filters.isEmpty(); + } + + public void close() throws IOException { + filters.clear(); + logger.close(); + } + + void add(final StatusFilter filter, final Level level) { + filters.add(new FilterLevel(filter, level)); + if (level.isLessSpecificThan(logger.getLevel())) { + logger.setLevel(level); + } + } + + boolean remove(final StatusFilter filter) { + for (final Iterator i = filters.iterator(); i.hasNext();) { + final FilterLevel existing = i.next(); + if (existing.filter == filter) { + i.remove(); + logger.setLevel(leastSpecificLevel()); + return true; + } + } + return false; + } + + Level leastSpecificLevel() { + Level level = Level.OFF; + for (FilterLevel existing : filters) { + if (existing.level.isLessSpecificThan(level)) { + level = existing.level; + } + } + return level; + } + } + + private static class FilterLevel { + final StatusFilter filter; + final Level level; + + FilterLevel(final StatusFilter filter, final Level level) { + this.filter = filter; + this.level = level; + } + } + + private static class ListenerLevel { + final StatusListener listener; + final Level level; + + ListenerLevel(final StatusListener listener, final Level level) { + this.listener = listener; + this.level = level; + } + } } Index: log4j-api/src/main/java/org/apache/logging/log4j/status/StatusFilter.java =================================================================== --- log4j-api/src/main/java/org/apache/logging/log4j/status/StatusFilter.java (revision 0) +++ log4j-api/src/main/java/org/apache/logging/log4j/status/StatusFilter.java (revision 0) @@ -0,0 +1,5 @@ +package org.apache.logging.log4j.status; + +public interface StatusFilter { + boolean isEnabled(StatusData data); +} Index: log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java (revision 1600802) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/jmx/StatusLoggerAdmin.java (working copy) @@ -16,7 +16,6 @@ */ package org.apache.logging.log4j.core.jmx; -import java.io.IOException; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; @@ -63,7 +62,7 @@ } catch (final Exception e) { throw new IllegalStateException(e); } - StatusLogger.getLogger().registerListener(this); + StatusLogger.getLogger().registerListener(this, level); } private static MBeanNotificationInfo createNotificationInfo() { @@ -95,13 +94,10 @@ } @Override - public Level getStatusLevel() { - return this.level; - } - - @Override public void setLevel(final String level) { + StatusLogger.getLogger().removeListener(this); this.level = Level.toLevel(level, Level.ERROR); + StatusLogger.getLogger().registerListener(this, this.level); } @Override @@ -145,9 +141,4 @@ private long now() { return System.currentTimeMillis(); } - - @Override - public void close() throws IOException { - // nothing to close here - } } Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java (revision 1600802) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java (working copy) @@ -36,7 +36,7 @@ import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; -import org.apache.logging.log4j.core.config.AbstractConfiguration; +import org.apache.logging.log4j.core.config.AbstractStatusLoggingConfiguration; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.FileConfigurationMonitor; @@ -44,8 +44,6 @@ import org.apache.logging.log4j.core.config.Reconfigurable; import org.apache.logging.log4j.core.config.plugins.util.PluginManager; import org.apache.logging.log4j.core.config.plugins.util.PluginType; -import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil; -import org.apache.logging.log4j.core.config.status.StatusConfiguration; import org.apache.logging.log4j.core.util.Loader; import org.apache.logging.log4j.core.util.Patterns; import org.w3c.dom.Attr; @@ -60,16 +58,15 @@ /** * Creates a Node hierarchy from an XML file. */ -public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable { +public class XmlConfiguration extends AbstractStatusLoggingConfiguration implements Reconfigurable { private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language"; private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris"; - private static final String[] VERBOSE_CLASSES = new String[] {ResolverUtil.class.getName()}; - private static final String LOG4J_XSD = "Log4j-config.xsd"; + // TODO: why do we record status messages here, rather than just logging them immediately? private final List status = new ArrayList(); private Element rootElement; @@ -143,20 +140,12 @@ final Document document = newDocumentBuilder().parse(source); rootElement = document.getDocumentElement(); final Map attrs = processAttributes(rootNode, rootElement); - final StatusConfiguration statusConfig = new StatusConfiguration() - .withVerboseClasses(VERBOSE_CLASSES) - .withStatus(getDefaultStatus()); + configureStatusLogger(attrs); for (final Map.Entry entry : attrs.entrySet()) { final String key = entry.getKey(); final String value = getStrSubstitutor().replace(entry.getValue()); - if ("status".equalsIgnoreCase(key)) { - statusConfig.withStatus(value); - } else if ("dest".equalsIgnoreCase(key)) { - statusConfig.withDestination(value); - } else if ("shutdownHook".equalsIgnoreCase(key)) { + if ("shutdownHook".equalsIgnoreCase(key)) { isShutdownHookEnabled = !"disable".equalsIgnoreCase(value); - } else if ("verbose".equalsIgnoreCase(key)) { - statusConfig.withVerbosity(value); } else if ("packages".equalsIgnoreCase(key)) { final String[] packages = value.split(Patterns.COMMA_SEPARATOR); for (final String p : packages) { @@ -177,7 +166,6 @@ createAdvertiser(value, configSource, buffer, "text/xml"); } } - statusConfig.initialize(); } catch (final SAXException domEx) { LOGGER.error("Error parsing " + configSource.getLocation(), domEx); } catch (final IOException ioe) { Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java (revision 1600802) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java (working copy) @@ -26,7 +26,7 @@ import java.util.List; import java.util.Map; -import org.apache.logging.log4j.core.config.AbstractConfiguration; +import org.apache.logging.log4j.core.config.AbstractStatusLoggingConfiguration; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.FileConfigurationMonitor; @@ -35,7 +35,6 @@ import org.apache.logging.log4j.core.config.plugins.util.PluginManager; import org.apache.logging.log4j.core.config.plugins.util.PluginType; import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil; -import org.apache.logging.log4j.core.config.status.StatusConfiguration; import org.apache.logging.log4j.core.util.Patterns; import com.fasterxml.jackson.core.JsonParser; @@ -45,10 +44,11 @@ /** * Creates a Node hierarchy from a JSON file. */ -public class JsonConfiguration extends AbstractConfiguration implements Reconfigurable { +public class JsonConfiguration extends AbstractStatusLoggingConfiguration implements Reconfigurable { private static final String[] VERBOSE_CLASSES = new String[]{ResolverUtil.class.getName()}; + // TODO: why do we record status messages here, rather than just logging them immediately? private final List status = new ArrayList(); private JsonNode root; @@ -73,20 +73,12 @@ } } processAttributes(rootNode, root); - final StatusConfiguration statusConfig = new StatusConfiguration() - .withVerboseClasses(VERBOSE_CLASSES) - .withStatus(getDefaultStatus()); + configureStatusLogger(rootNode.getAttributes()); for (final Map.Entry entry : rootNode.getAttributes().entrySet()) { final String key = entry.getKey(); final String value = getStrSubstitutor().replace(entry.getValue()); - if ("status".equalsIgnoreCase(key)) { - statusConfig.withStatus(value); - } else if ("dest".equalsIgnoreCase(key)) { - statusConfig.withDestination(value); - } else if ("shutdownHook".equalsIgnoreCase(key)) { + if ("shutdownHook".equalsIgnoreCase(key)) { isShutdownHookEnabled = !"disable".equalsIgnoreCase(value); - } else if ("verbose".equalsIgnoreCase(entry.getKey())) { - statusConfig.withVerbosity(value); } else if ("packages".equalsIgnoreCase(key)) { final String[] packages = value.split(Patterns.COMMA_SEPARATOR); for (final String p : packages) { @@ -103,7 +95,6 @@ createAdvertiser(value, configSource, buffer, "application/json"); } } - statusConfig.initialize(); if (getName() == null) { setName(configSource.getLocation()); } Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractStatusLoggingConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractStatusLoggingConfiguration.java (revision 0) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractStatusLoggingConfiguration.java (revision 0) @@ -0,0 +1,215 @@ +/* + * 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.config; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil; +import org.apache.logging.log4j.core.util.FileUtils; +import org.apache.logging.log4j.status.StatusData; +import org.apache.logging.log4j.status.StatusFilter; +import org.apache.logging.log4j.status.StatusLogger; + +/** + * Configuration that can listen to status messages and log them + */ +public abstract class AbstractStatusLoggingConfiguration extends AbstractConfiguration { + private static final String[] VERBOSE_CLASSES = { ResolverUtil.class.getName() }; + + private final Lock statusLock = new ReentrantLock(); + private final Collection errorMessages = Collections.synchronizedCollection(new LinkedList()); + private final StatusLogger logger = StatusLogger.getLogger(); + + private StatusFilter filter; + private boolean statusFilterSysOut; + private boolean statusFilterSysErr; + private File statusFilterFile; + + /** + * Logs an error message to the StatusLogger. If the StatusLogger hasn't been set up yet, queues + * the message to be logged after initialization. + * + * @param message error message to log. + */ + protected void logStatusError(final String message) { + statusLock.lock(); + try { + if (filter == null) { + this.errorMessages.add(message); + } else { + this.logger.error(message); + } + } finally { + statusLock.unlock(); + } + } + + protected void configureStatusLogger(Map attrs) { + Level status = getDefaultStatus(); + String[] verboseClasses = VERBOSE_CLASSES; + String destination = null; + + for (Map.Entry entry : attrs.entrySet()) { + final String key = entry.getKey(); + final String value = getStrSubstitutor().replace(entry.getValue()); + if ("status".equalsIgnoreCase(key)) { + status = parseStatus(value); + } else if ("verbose".equalsIgnoreCase(key)) { + verboseClasses = Boolean.parseBoolean(value) ? null : VERBOSE_CLASSES; + } else if ("dest".equalsIgnoreCase(key)) { + destination = value; + } + } + registerStatusFilter(status, verboseClasses, destination); + } + + private Level parseStatus(final String status) { + Level result = Level.toLevel(status, null); + if (result == null) { + logStatusError("Invalid status level specified: " + status + ". Defaulting to ERROR."); + result = Level.ERROR; + } + return result; + } + + /** + * Specifies the destination for StatusLogger events. This can be {@code out} (default) for + * using {@link System#out standard out}, {@code err} for using {@link System#err standard + * error}, or a file URI to which log events will be written. If the provided URI is invalid, + * then the default destination of standard out will be used. + * + * @param destination where status log messages should be output. + * @return {@code this} + */ + protected void registerStatusFilter(final Level level, final String[] verboseClasses, final String destination) { + statusLock.lock(); + try { + unregisterStatusFilter(); + filter = new VerboseClassesFilter(verboseClasses); + if (level == Level.OFF) { + // nothing to register + } else if (destination == null || destination.equalsIgnoreCase("out")) { + registerStatusFilterSysOut(level); + } else if (destination.equalsIgnoreCase("err")) { + registerStatusFilterSysErr(level); + } else { + registerDestinationFile(destination, level); + } + } finally { + statusLock.unlock(); + } + } + + private void registerStatusFilterSysOut(final Level level) { + logger.registerSystemOutFilter(filter, level); + statusFilterSysOut = true; + migrateSavedLogMessages(); + } + + private void registerStatusFilterSysErr(final Level level) { + logger.registerSystemErrFilter(filter, level); + statusFilterSysErr = true; + migrateSavedLogMessages(); + } + + private void registerDestinationFile(final String name, final Level level) { + File output = null; + try { + final URI destination = FileUtils.getCorrectedFilePathUri(name); + output = FileUtils.fileFromUri(destination); + if (output != null) { + logger.registerFileFilter(output, filter, level); + statusFilterFile = output; + migrateSavedLogMessages(); + } else { + logStatusError("Invalid file [" + name + "]. Falling back to default of stdout."); + registerStatusFilterSysOut(level); + } + } catch (URISyntaxException e) { + logStatusError("Could not parse URI [" + name + "]. Falling back to default of stdout."); + registerStatusFilterSysOut(level); + } catch (FileNotFoundException e) { + logStatusError("Could not find file [" + name + "]. Falling back to default of stdout."); + registerStatusFilterSysOut(level); + } + } + + private void unregisterStatusFilter() { + if (statusFilterSysOut) { + logger.removeSystemOutFilter(filter); + statusFilterSysOut = false; + } + if (statusFilterSysErr) { + logger.removeSystemErrFilter(filter); + statusFilterSysErr = false; + } + if (statusFilterFile != null) { + logger.removeFileFilter(statusFilterFile, filter); + statusFilterFile = null; + } + } + + private void migrateSavedLogMessages() { + for (final String message : this.errorMessages) { + logger.error(message); + } + errorMessages.clear(); + } + + public void stop() { + statusLock.lock(); + try { + unregisterStatusFilter(); + } finally { + statusLock.unlock(); + } + super.stop(); + } + + private class VerboseClassesFilter implements StatusFilter { + private final String[] verboseClasses; + + public VerboseClassesFilter(final String[] verboseClasses) { + this.verboseClasses = verboseClasses; + } + + @Override + public boolean isEnabled(StatusData data) { + if (verboseClasses == null) { + return true; + } + final String caller = data.getStackTraceElement().getClassName(); + for (final String verboseClass : verboseClasses) { + if (caller.startsWith(verboseClass)) { + return true; + } + } + return false; + } + } +} \ No newline at end of file Index: log4j-core/src/test/java/org/apache/logging/log4j/core/config/CustomConfigurationTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/config/CustomConfigurationTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/config/CustomConfigurationTest.java (working copy) @@ -28,7 +28,6 @@ import org.apache.logging.log4j.core.config.xml.XmlConfiguration; import org.apache.logging.log4j.core.layout.PatternLayout; import org.apache.logging.log4j.junit.InitialLoggerContext; -import org.apache.logging.log4j.status.StatusConsoleListener; import org.apache.logging.log4j.status.StatusListener; import org.apache.logging.log4j.status.StatusLogger; import org.junit.Before; @@ -66,12 +65,13 @@ ctx.reconfigure(); final Configuration config = ctx.getConfiguration(); assertTrue("Configuration is not an XmlConfiguration", config instanceof XmlConfiguration); - for (StatusListener listener : StatusLogger.getLogger().getListeners()) { - if (listener instanceof StatusConsoleListener) { - assertSame(listener.getStatusLevel(), Level.INFO); - break; - } - } + // TODO: test that the filters are registered +// for (StatusListener listener : StatusLogger.getLogger().getListeners()) { +// if (listener instanceof AbstractFilteredStatusListener) { +// assertSame(listener.getStatusLevel(), Level.INFO); +// break; +// } +// } final Layout layout = PatternLayout.newBuilder() .withPattern(PatternLayout.SIMPLE_CONVERSION_PATTERN) .withConfiguration(config) Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueAppenderTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueAppenderTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueAppenderTest.java (working copy) @@ -28,7 +28,8 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; -import org.apache.logging.log4j.status.StatusConsoleListener; +import org.apache.logging.log4j.status.StatusData; +import org.apache.logging.log4j.status.StatusListener; import org.apache.logging.log4j.status.StatusLogger; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -58,11 +59,10 @@ @BeforeClass public static void setupClass() throws Exception { // MockContextFactory becomes the primary JNDI provider - final StatusConsoleListener listener = new StatusConsoleListener(Level.ERROR); - StatusLogger.getLogger().registerListener(listener); + StatusLogger.getLogger().registerListener(new JmsTestStatusListener(), Level.ERROR); MockContextFactory.setAsInitial(); context = new InitialContext(); - context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl()); + context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl() ); context.rebind(QUEUE_NAME, new MockQueue(QUEUE_NAME)); System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, CONFIG); receiver = new JmsQueueReceiver(FACTORY_NAME, QUEUE_NAME, null, null); Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicTest.java (working copy) @@ -34,7 +34,6 @@ import org.apache.logging.log4j.core.filter.AbstractFilter; import org.apache.logging.log4j.core.filter.CompositeFilter; import org.apache.logging.log4j.core.layout.PatternLayout; -import org.apache.logging.log4j.status.StatusConsoleListener; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.test.appender.ListAppender; import org.junit.After; @@ -64,12 +63,11 @@ @BeforeClass public static void setupClass() throws Exception { // MockContextFactory becomes the primary JNDI provider - final StatusConsoleListener listener = new StatusConsoleListener(Level.ERROR); - StatusLogger.getLogger().registerListener(listener); + StatusLogger.getLogger().registerListener(new JmsTestStatusListener(), Level.ERROR); MockContextFactory.setAsInitial(); context = new InitialContext(); - context.rebind(FACTORY_NAME, new TopicConnectionFactoryImpl()); - context.rebind(TOPIC_NAME, new MockTopic(TOPIC_NAME)); + context.rebind(FACTORY_NAME, new TopicConnectionFactoryImpl() ); + context.rebind(TOPIC_NAME, new MockTopic(TOPIC_NAME) ); ((LoggerContext) LogManager.getContext()).reconfigure(); receiver = new JmsTopicReceiver(FACTORY_NAME, TOPIC_NAME, null, null); } Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicFailoverTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicFailoverTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTopicFailoverTest.java (working copy) @@ -31,7 +31,6 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; -import org.apache.logging.log4j.status.StatusConsoleListener; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.test.appender.ListAppender; import org.junit.AfterClass; @@ -57,7 +56,7 @@ private static final String CONFIG = "log4j-jmstopic-failover.xml"; private static Configuration config; - private static ListAppender listAppender; + private static ListAppender app; private static LoggerContext ctx; @BeforeClass @@ -77,19 +76,23 @@ @Before public void before() { config = ctx.getConfiguration(); - listAppender = (ListAppender) config.getAppender("List"); - assertNotNull("No Appender", listAppender); - listAppender.clear(); + for (final Map.Entry entry : config.getAppenders().entrySet()) { + if (entry.getKey().equals("List")) { + app = (ListAppender) entry.getValue(); + break; + } + } + assertNotNull("No Appender", app); + app.clear(); ThreadContext.clearMap(); } private static void setupQueue() throws Exception { // MockContextFactory becomes the primary JNDI provider - final StatusConsoleListener listener = new StatusConsoleListener(Level.ERROR); - StatusLogger.getLogger().registerListener(listener); + StatusLogger.getLogger().registerListener(new JmsTestStatusListener(), Level.ERROR); MockContextFactory.setAsInitial(); context = new InitialContext(); - context.rebind(FACTORY_NAME, new TopicConnectionFactoryImpl()); + context.rebind(FACTORY_NAME, new TopicConnectionFactoryImpl() ); //context.rebind(QUEUE_NAME, new MockQueue(QUEUE_NAME)); //System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, CONFIG); //receiver = new JmsQueueReceiver(FACTORY_NAME, QUEUE_NAME, null, null); @@ -100,7 +103,7 @@ ThreadContext.put("appender", "Failover"); final Logger logger = LogManager.getLogger(JmsTopicFailoverTest.class); logger.debug("Test Message"); - final List events = listAppender.getEvents(); + final List events = app.getEvents(); assertNotNull("No events returned", events); assertTrue("No events returned", events.size() > 0); assertTrue("Incorrect event", "Test Message".equals(events.get(0).getMessage().getFormattedMessage())); @@ -113,7 +116,7 @@ ThreadContext.put("appender", "Failover"); final Logger logger = LogManager.getLogger(JmsTopicFailoverTest.class); logger.debug("Test Message"); - final List events = listAppender.getEvents(); + final List events = app.getEvents(); assertNotNull("No events returned", events); assertTrue("No events returned", events.size() > 0); assertTrue("Incorrect event", "Test Message".equals(events.get(0).getMessage().getFormattedMessage())); Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueTest.java (working copy) @@ -34,7 +34,6 @@ import org.apache.logging.log4j.core.filter.AbstractFilter; import org.apache.logging.log4j.core.filter.CompositeFilter; import org.apache.logging.log4j.core.layout.PatternLayout; -import org.apache.logging.log4j.status.StatusConsoleListener; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.test.appender.ListAppender; import org.junit.After; @@ -64,11 +63,10 @@ @BeforeClass public static void setupClass() throws Exception { // MockContextFactory becomes the primary JNDI provider - final StatusConsoleListener listener = new StatusConsoleListener(Level.ERROR); - StatusLogger.getLogger().registerListener(listener); + StatusLogger.getLogger().registerListener(new JmsTestStatusListener(), Level.ERROR); MockContextFactory.setAsInitial(); context = new InitialContext(); - context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl()); + context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl() ); context.rebind(QUEUE_NAME, new MockQueue(QUEUE_NAME)); ((LoggerContext) LogManager.getContext()).reconfigure(); receiver = new JmsQueueReceiver(FACTORY_NAME, QUEUE_NAME, null, null); Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTestStatusListener.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTestStatusListener.java (revision 0) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsTestStatusListener.java (revision 0) @@ -0,0 +1,11 @@ +package org.apache.logging.log4j.core.net.jms; + +import org.apache.logging.log4j.status.StatusData; +import org.apache.logging.log4j.status.StatusListener; + +public class JmsTestStatusListener implements StatusListener { + @Override + public void log(StatusData data) { + // no need + } +} \ No newline at end of file Index: log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueFailoverTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueFailoverTest.java (revision 1600802) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/net/jms/JmsQueueFailoverTest.java (working copy) @@ -31,7 +31,6 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; -import org.apache.logging.log4j.status.StatusConsoleListener; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.test.appender.ListAppender; import org.junit.AfterClass; @@ -58,7 +57,7 @@ private static final String CONFIG = "log4j-jmsqueue-failover.xml"; private static Configuration config; - private static ListAppender listAppender; + private static ListAppender app; private static LoggerContext ctx; @BeforeClass @@ -78,19 +77,23 @@ @Before public void before() { config = ctx.getConfiguration(); - listAppender = (ListAppender) config.getAppender("List"); - assertNotNull("No Appender", listAppender); - listAppender.clear(); + for (final Map.Entry entry : config.getAppenders().entrySet()) { + if (entry.getKey().equals("List")) { + app = (ListAppender) entry.getValue(); + break; + } + } + assertNotNull("No Appender", app); + app.clear(); ThreadContext.clearMap(); } private static void setupQueue() throws Exception { // MockContextFactory becomes the primary JNDI provider - final StatusConsoleListener listener = new StatusConsoleListener(Level.ERROR); - StatusLogger.getLogger().registerListener(listener); + StatusLogger.getLogger().registerListener(new JmsTestStatusListener(), Level.ERROR); MockContextFactory.setAsInitial(); context = new InitialContext(); - context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl()); + context.rebind(FACTORY_NAME, new QueueConnectionFactoryImpl() ); //context.rebind(QUEUE_NAME, new MockQueue(QUEUE_NAME)); //System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, CONFIG); //receiver = new JmsQueueReceiver(FACTORY_NAME, QUEUE_NAME, null, null); @@ -101,7 +104,7 @@ ThreadContext.put("appender", "Failover"); final Logger logger = LogManager.getLogger(JmsQueueFailoverTest.class); logger.debug("Test Message"); - final List events = listAppender.getEvents(); + final List events = app.getEvents(); assertNotNull("No events returned", events); assertTrue("No events returned", events.size() > 0); assertTrue("Incorrect event", "Test Message".equals(events.get(0).getMessage().getFormattedMessage())); @@ -114,7 +117,7 @@ ThreadContext.put("appender", "Failover"); final Logger logger = LogManager.getLogger(JmsQueueFailoverTest.class); logger.debug("Test Message"); - final List events = listAppender.getEvents(); + final List events = app.getEvents(); assertNotNull("No events returned", events); assertTrue("No events returned", events.size() > 0); assertTrue("Incorrect event", "Test Message".equals(events.get(0).getMessage().getFormattedMessage()));