Index: logback-classic/src/main/java/ch/qos/logback/classic/joran/JoranConfigurator.java =================================================================== --- logback-classic/src/main/java/ch/qos/logback/classic/joran/JoranConfigurator.java (revision 1945) +++ logback-classic/src/main/java/ch/qos/logback/classic/joran/JoranConfigurator.java (working copy) @@ -10,6 +10,7 @@ package ch.qos.logback.classic.joran; +import ch.qos.logback.classic.joran.action.AppenderRefAction; import ch.qos.logback.classic.joran.action.ConfigurationAction; import ch.qos.logback.classic.joran.action.ConsolePluginAction; import ch.qos.logback.classic.joran.action.EvaluatorAction; @@ -20,7 +21,6 @@ import ch.qos.logback.classic.joran.action.RootLoggerAction; import ch.qos.logback.classic.spi.PlatformInfo; import ch.qos.logback.core.joran.JoranConfiguratorBase; -import ch.qos.logback.core.joran.action.AppenderRefAction; import ch.qos.logback.core.joran.action.IncludeAction; import ch.qos.logback.core.joran.action.MatcherAction; import ch.qos.logback.core.joran.spi.Pattern; Index: logback-classic/src/main/java/ch/qos/logback/classic/joran/action/AppenderRefAction.java =================================================================== --- logback-classic/src/main/java/ch/qos/logback/classic/joran/action/AppenderRefAction.java (revision 0) +++ logback-classic/src/main/java/ch/qos/logback/classic/joran/action/AppenderRefAction.java (revision 0) @@ -0,0 +1,85 @@ +package ch.qos.logback.classic.joran.action; + +import java.util.HashMap; + +import org.xml.sax.Attributes; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.joran.action.Action; +import ch.qos.logback.core.joran.action.ActionConst; +import ch.qos.logback.core.joran.spi.InterpretationContext; +import ch.qos.logback.core.util.OptionHelper; + +public class AppenderRefAction extends Action { + public static final String LEVEL_ATTRIBUTE = "level"; + + boolean inError = false; + + @SuppressWarnings("unchecked") + public void begin(InterpretationContext ec, String tagName, Attributes attributes) { + // Let us forget about previous errors (in this object) + inError = false; + + // logger.debug("begin called"); + + Object o = ec.peekObject(); + + if (!(o instanceof Logger)) { + String errMsg = "Could not find a Logger at the top of execution stack. Near [" + + tagName + "] line " + getLineNumber(ec); + inError = true; + addError(errMsg); + return; + } + + Logger appenderAttachable = (Logger) o; + + String appenderName = attributes.getValue(ActionConst.REF_ATTRIBUTE); + + if (OptionHelper.isEmpty(appenderName)) { + // print a meaningful error message and return + String errMsg = "Missing appender ref attribute in tag."; + inError = true; + addError(errMsg); + + return; + } + + HashMap appenderBag = (HashMap) ec.getObjectMap().get( + ActionConst.APPENDER_BAG); + Appender appender = (Appender) appenderBag.get(appenderName); + + if (appender == null) { + String msg = "Could not find an appender named [" + appenderName + + "]. Did you define it below in the config file?"; + inError = true; + addError(msg); + addError("See " + CoreConstants.CODES_URL + + "#appender_order for more details."); + return; + } + + Level level = null; + String levelStr = attributes.getValue(LEVEL_ATTRIBUTE); + if (!OptionHelper.isEmpty(levelStr)) { + level = Level.toLevel(levelStr); + } + + if (level == null) { + addInfo("Attaching appender named [" + appenderName + "] to " + + appenderAttachable); + appenderAttachable.addAppender(appender); + } else { + addInfo("Attaching appender named [" + appenderName + "] to " + + appenderAttachable + " with level " + level); + appenderAttachable.addAppender(appender, level); + } + } + + public void end(InterpretationContext ec, String n) { + } + +} Index: logback-classic/src/main/java/ch/qos/logback/classic/Logger.java =================================================================== --- logback-classic/src/main/java/ch/qos/logback/classic/Logger.java (revision 1945) +++ logback-classic/src/main/java/ch/qos/logback/classic/Logger.java (working copy) @@ -13,9 +13,14 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.LoggerFactory; import org.slf4j.Marker; @@ -56,6 +61,8 @@ // The effective levelInt is the assigned levelInt and if null, a levelInt is // inherited form a parent. private int effectiveLevelInt; + + private int lowestAppenderLevelInt = Level.OFF_INT; /** * The parent of this category. All categories have at least one ancestor @@ -83,6 +90,10 @@ * check whether 'aai' is null

4) AppenderAttachableImpl is thread safe */ private transient AppenderAttachableImpl aai; + + private transient Map, Level> effectiveAppenderLevels; + + private transient Map, Level> appenderLevels; /** * Additivity is set to true by default, that is children inherit the * appenders of their ancestors by default. If this variable is set to @@ -235,6 +246,33 @@ } return aai.detachAppender(name); } + + public synchronized void addAppender(Appender newAppender, Level level) { + if (appenderLevels == null) { + appenderLevels = new HashMap, Level>(); + } + appenderLevels.put(newAppender, level); + doUpdateEffectiveAppenderLevels(newAppender, level); + } + + private void doUpdateEffectiveAppenderLevels(Appender newAppender, Level level) { + if (effectiveAppenderLevels == null) { + effectiveAppenderLevels = new HashMap, Level>(); + } + effectiveAppenderLevels.put(newAppender, level); + lowestAppenderLevelInt = level.levelInt < lowestAppenderLevelInt ? level.levelInt : lowestAppenderLevelInt; + if (childrenList != null) { + for (Logger childLogger : childrenList) { + childLogger.handleParentAppenderLevelChange(newAppender, level); + } + } + } + + private synchronized void handleParentAppenderLevelChange(Appender newAppender, Level level) { + if (appenderLevels == null || !appenderLevels.containsKey(newAppender)) { + doUpdateEffectiveAppenderLevels(newAppender, level); + } + } // this method MUST be synchronized. See comments on 'aai' field for further // details. @@ -241,7 +279,7 @@ public synchronized void addAppender(Appender newAppender) { if (aai == null) { aai = new AppenderAttachableImpl(); - } + } aai.addAppender(newAppender); } @@ -252,19 +290,35 @@ return aai.isAttached(appender); } - @SuppressWarnings("unchecked") public Iterator> iteratorForAppenders() { - if (aai == null) { - return Collections.EMPTY_LIST.iterator(); + Collection> allAppenders = new ArrayList>(); + if (aai != null) { + Iterator> iter = aai.iteratorForAppenders(); + while (iter.hasNext()) { + allAppenders.add(iter.next()); + } + } + if (appenderLevels != null) { + allAppenders.addAll(appenderLevels.keySet()); } - return aai.iteratorForAppenders(); + return allAppenders.iterator(); } public Appender getAppender(String name) { - if (aai == null) { - return null; + Appender normalAppender = null; + if (aai != null) { + normalAppender = aai.getAppender(name); + } + if (normalAppender != null) { + return normalAppender; + } else if (appenderLevels != null) { + for (Appender appender : appenderLevels.keySet()) { + if (appender.getName().equals(name)) { + return appender; + } + } } - return aai.getAppender(name); + return null; } /** @@ -275,11 +329,25 @@ */ public void callAppenders(LoggingEvent event) { int writes = 0; - for (Logger l = this; l != null; l = l.parent) { - writes += l.appendLoopOnAppenders(event); - if (!l.additive) { - break; - } + // Call appenders with level - no recursion as all appenders added to parent will be visible here + if (effectiveAppenderLevels != null && event.getLevel().levelInt >= lowestAppenderLevelInt) { + synchronized(this) { + for (Map.Entry, Level> appenderLevel : effectiveAppenderLevels.entrySet()) { + if (event.getLevel().levelInt >= appenderLevel.getValue().levelInt) { + appenderLevel.getKey().doAppend(event); + writes++; + } + } + } + } + // Call "normal" appenders + if (event.getLevel().levelInt >= effectiveLevelInt) { + for (Logger l = this; l != null; l = l.parent) { + writes += l.appendLoopOnAppenders(event); + if (!l.additive) { + break; + } + } } // No appenders in hierarchy if (writes == 0) { @@ -340,6 +408,10 @@ } childrenList.add(childLogger); childLogger.effectiveLevelInt = this.effectiveLevelInt; + if (this.effectiveAppenderLevels != null) { + childLogger.effectiveAppenderLevels = new HashMap, Level>(this.effectiveAppenderLevels); + } + childLogger.lowestAppenderLevelInt = this.lowestAppenderLevelInt; return childLogger; } @@ -377,6 +449,10 @@ childLogger = new Logger(childName, this, this.loggerContext); childrenList.add(childLogger); childLogger.effectiveLevelInt = this.effectiveLevelInt; + if (this.effectiveAppenderLevels != null) { + childLogger.effectiveAppenderLevels = new HashMap, Level>(this.effectiveAppenderLevels); + } + childLogger.lowestAppenderLevelInt = this.lowestAppenderLevelInt; return childLogger; } @@ -394,7 +470,7 @@ marker, this, level, msg, params, t); if (decision == FilterReply.NEUTRAL) { - if (effectiveLevelInt > level.levelInt) { + if (lowestAppenderLevelInt > level.levelInt && effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { @@ -412,7 +488,7 @@ marker, this, level, msg, param, t); if (decision == FilterReply.NEUTRAL) { - if (effectiveLevelInt > level.levelInt) { + if (lowestAppenderLevelInt > level.levelInt && effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { @@ -432,7 +508,7 @@ marker, this, level, msg, param1, param2, t); if (decision == FilterReply.NEUTRAL) { - if (effectiveLevelInt > level.levelInt) { + if (lowestAppenderLevelInt > level.levelInt && effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { @@ -500,7 +576,7 @@ final public boolean isDebugEnabled(Marker marker) { final FilterReply decision = callTurboFilters(marker, Level.DEBUG); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= Level.DEBUG_INT; + return effectiveLevelInt <= Level.DEBUG_INT || lowestAppenderLevelInt <= Level.DEBUG_INT; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { @@ -597,7 +673,7 @@ public boolean isInfoEnabled(Marker marker) { FilterReply decision = callTurboFilters(marker, Level.INFO); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= Level.INFO_INT; + return effectiveLevelInt <= Level.INFO_INT || lowestAppenderLevelInt <= Level.INFO_INT; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { @@ -654,7 +730,7 @@ public boolean isTraceEnabled(Marker marker) { final FilterReply decision = callTurboFilters(marker, Level.TRACE); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= Level.TRACE_INT; + return effectiveLevelInt <= Level.TRACE_INT || lowestAppenderLevelInt <= Level.TRACE_INT; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { @@ -673,7 +749,7 @@ public boolean isErrorEnabled(Marker marker) { FilterReply decision = callTurboFilters(marker, Level.ERROR); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= Level.ERROR_INT; + return effectiveLevelInt <= Level.ERROR_INT || lowestAppenderLevelInt <= Level.ERROR_INT; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { @@ -691,7 +767,7 @@ public boolean isWarnEnabled(Marker marker) { FilterReply decision = callTurboFilters(marker, Level.WARN); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= Level.WARN_INT; + return effectiveLevelInt <= Level.WARN_INT || lowestAppenderLevelInt <= Level.WARN_INT; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { @@ -705,7 +781,7 @@ public boolean isEnabledFor(Marker marker, Level level) { FilterReply decision = callTurboFilters(marker, level); if (decision == FilterReply.NEUTRAL) { - return effectiveLevelInt <= level.levelInt; + return effectiveLevelInt <= level.levelInt || lowestAppenderLevelInt <= level.levelInt; } else if (decision == FilterReply.DENY) { return false; } else if (decision == FilterReply.ACCEPT) { Index: logback-classic/src/test/java/ch/qos/logback/classic/AppenderWithLevelTest.java =================================================================== --- logback-classic/src/test/java/ch/qos/logback/classic/AppenderWithLevelTest.java (revision 0) +++ logback-classic/src/test/java/ch/qos/logback/classic/AppenderWithLevelTest.java (revision 0) @@ -0,0 +1,232 @@ +package ch.qos.logback.classic; + +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import junit.framework.TestCase; + +public class AppenderWithLevelTest extends TestCase { + + public void testLoggerWithAppenderWithLowerLevel() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + ListAppender listDebugAppender = new ListAppender(); + listAppender.start(); + listDebugAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + root.addAppender(listAppender); + root.addAppender(listDebugAppender, Level.DEBUG); + + root.debug("hello"); + // Should have logger's level of info, so nothing appended + assertEquals(0, listAppender.list.size()); + // Should have own level of debug, so appended + assertEquals(1, listDebugAppender.list.size()); + } + + public void testSettingLevelOnLoggerAfterAddingAppenderWithLowerLevel() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + ListAppender listDebugAppender = new ListAppender(); + listAppender.start(); + listDebugAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.addAppender(listAppender); + root.addAppender(listDebugAppender, Level.DEBUG); + root.setLevel(Level.INFO); + + root.debug("hello"); + // Should have logger's level of info, so nothing appended + assertEquals(0, listAppender.list.size()); + // Should have own level of debug, so appended + assertEquals(1, listDebugAppender.list.size()); + } + + public void testLoggerWithAppenderWithHigherLevel() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + ListAppender listInfoAppender = new ListAppender(); + listAppender.start(); + listInfoAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.DEBUG); + root.addAppender(listAppender); + root.addAppender(listInfoAppender, Level.INFO); + + root.debug("hello"); + // Should have logger's level of debug, so appended + assertEquals(1, listAppender.list.size()); + // Should have own level of info, so nothing appended + assertEquals(0, listInfoAppender.list.size()); + } + + public void testSettingLevelOnLoggerAfterAddingAppenderWithHigherLevel() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + ListAppender listInfoAppender = new ListAppender(); + listAppender.start(); + listInfoAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.addAppender(listAppender); + root.addAppender(listInfoAppender, Level.INFO); + root.setLevel(Level.DEBUG); + + root.debug("hello"); + // Should have logger's level of debug, so appended + assertEquals(1, listAppender.list.size()); + // Should have own level of info, so nothing appended + assertEquals(0, listInfoAppender.list.size()); + } + + public void testLoggerLevelIrreleventToAppendersWithLevelOnParentLogger() { + LoggerContext lc = new LoggerContext(); + ListAppender debugListAppender = new ListAppender(); + debugListAppender.start(); + ListAppender errorListAppender = new ListAppender(); + errorListAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.DEBUG); + root.addAppender(debugListAppender, Level.DEBUG); + root.addAppender(errorListAppender, Level.ERROR); + + Logger x = lc.getLogger("x"); + x.setLevel(Level.INFO); + + x.debug("hello"); + // Debug appender should get this, error appender not + assertEquals(1, debugListAppender.list.size()); + assertEquals(0, errorListAppender.list.size()); + + x.info("hello"); + // Debug appender should get this, error appender not + assertEquals(2, debugListAppender.list.size()); + assertEquals(0, errorListAppender.list.size()); + + x.error("hello"); + // Both appenders should get this + assertEquals(3, debugListAppender.list.size()); + assertEquals(1, errorListAppender.list.size()); + } + + public void testAppenderWithLevelOnParentDoesNotChangeBehaviourOfNormalAppender() { + LoggerContext lc = new LoggerContext(); + ListAppender listDebugAppender = new ListAppender(); + listDebugAppender.start(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + root.addAppender(listDebugAppender, Level.DEBUG); + root.addAppender(listAppender); + + Logger x = lc.getLogger("x"); + x.setLevel(Level.ERROR); + + x.debug("hello"); + + assertEquals(0, listAppender.list.size()); + assertEquals(1, listDebugAppender.list.size()); + } + + public void testLoggerWithLowerLevelOverridesAppenderWithLevelOnParentLogger() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + root.addAppender(listAppender, Level.INFO); + + Logger x = lc.getLogger("x"); + x.setLevel(Level.DEBUG); + + x.debug("hello"); + // Should have appender's level of info, so not appended + assertEquals(0, listAppender.list.size()); + + x.info("hello again"); + assertEquals(1, listAppender.list.size()); + + + } + + public void testAppenderWithLowerLevelOverridesLevelOnParentLogger() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + + Logger x = lc.getLogger("x"); + x.addAppender(listAppender, Level.DEBUG); + + x.debug("hello"); + + // Should have appender's level of debug, so appended + assertEquals(1, listAppender.list.size()); + } + + public void testAppenderWithHigherLevelOverridesLevelOnParentLogger() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.DEBUG); + + Logger x = lc.getLogger("x"); + x.addAppender(listAppender, Level.INFO); + + x.debug("hello"); + + // Should have appender's level of info, so not appended + assertEquals(0, listAppender.list.size()); + } + + public void testAppenderWithLevelOverridesAppenderWithLevelOnParentLogger() { + LoggerContext lc = new LoggerContext(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + root.addAppender(listAppender, Level.INFO); + + Logger x = lc.getLogger("x"); + x.addAppender(listAppender, Level.DEBUG); + + x.debug("hello"); + + // Should have latest appender's level of debug, so appended + assertEquals(1, listAppender.list.size()); + } + + public void testChangingParentLevelWhenChildHasAppenderWithLevel() { + LoggerContext lc = new LoggerContext(); + ListAppender listDebugAppender = new ListAppender(); + listDebugAppender.start(); + ListAppender listAppender = new ListAppender(); + listAppender.start(); + + Logger x = lc.getLogger("x"); + x.addAppender(listDebugAppender, Level.DEBUG); + + Logger root = lc.getLogger(LoggerContext.ROOT_NAME); + root.setLevel(Level.INFO); + root.addAppender(listAppender); + + x.debug("hello"); + + // Should have x's appender's level of debug, so appended + assertEquals(1, listDebugAppender.list.size()); + // Should have root's level of info, so not appended + assertEquals(0, listAppender.list.size()); + } +} Index: logback-classic/src/test/java/ch/qos/logback/classic/joran/JoranConfiguratorTest.java =================================================================== --- logback-classic/src/test/java/ch/qos/logback/classic/joran/JoranConfiguratorTest.java (revision 1945) +++ logback-classic/src/test/java/ch/qos/logback/classic/joran/JoranConfiguratorTest.java (working copy) @@ -65,6 +65,16 @@ logger.debug(msg); assertEquals(0, listAppender.list.size()); } + + @Test + public void testAppenderLevel() throws JoranException { + configure(TeztConstants.TEST_DIR_PREFIX + "input/joran/simpleAppenderLevel.xml"); + ListAppender listAppender = (ListAppender) root.getAppender("LIST"); + assertEquals(0, listAppender.list.size()); + String msg = "hello world"; + logger.debug(msg); + assertEquals(1, listAppender.list.size()); + } @Test public void testStatusListener() throws JoranException { Index: logback-classic/src/test/java/ch/qos/logback/classic/PackageTest.java =================================================================== --- logback-classic/src/test/java/ch/qos/logback/classic/PackageTest.java (revision 1945) +++ logback-classic/src/test/java/ch/qos/logback/classic/PackageTest.java (working copy) @@ -24,6 +24,7 @@ suite.addTestSuite(MessageFormattingTest.class); suite.addTestSuite(MDCTest.class); suite.addTestSuite(TurboFilteringInLoggerTest.class); + suite.addTestSuite(AppenderWithLevelTest.class); return suite; } } \ No newline at end of file Index: logback-classic/src/test/java/ch/qos/logback/classic/net/SyslogAppenderTest.java =================================================================== --- logback-classic/src/test/java/ch/qos/logback/classic/net/SyslogAppenderTest.java (revision 1945) +++ logback-classic/src/test/java/ch/qos/logback/classic/net/SyslogAppenderTest.java (working copy) @@ -75,7 +75,7 @@ + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">"; assertTrue(msg.startsWith(expected)); - String first = "<\\d{2}>\\w{3} \\d{2} \\d{2}(:\\d{2}){2} [\\w.]* "; + String first = "<\\d{2}>\\w{3} \\d{2} \\d{2}(:\\d{2}){2} [\\w.-]* "; assertTrue(msg.matches(first + "\\[" + threadName + "\\] " + loggerName + " " + logMsg)); @@ -127,7 +127,7 @@ + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">"; assertTrue(msg.startsWith(expected)); - String expectedPrefix = "<\\d{2}>\\w{3} \\d{2} \\d{2}(:\\d{2}){2} [\\w.]* "; + String expectedPrefix = "<\\d{2}>\\w{3} \\d{2} \\d{2}(:\\d{2}){2} [\\w.-]* "; String threadName = Thread.currentThread().getName(); String expectedResult = expectedPrefix + "\\[" + threadName + "\\] " + loggerName + " " + logMsg; Index: logback-classic/src/test/input/joran/simpleAppenderLevel.xml =================================================================== --- logback-classic/src/test/input/joran/simpleAppenderLevel.xml (revision 0) +++ logback-classic/src/test/input/joran/simpleAppenderLevel.xml (revision 0) @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +