You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sn...@apache.org on 2015/06/24 08:11:48 UTC

cassandra git commit: Improve log output from unit tests

Repository: cassandra
Updated Branches:
  refs/heads/trunk b7e72e1ee -> c8d3cc149


Improve log output from unit tests

patch by Ariel Weisberg; reviewed by Robert Stupp for CASSANDRA-9528


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/c8d3cc14
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/c8d3cc14
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/c8d3cc14

Branch: refs/heads/trunk
Commit: c8d3cc1493a0ca47fa34e88d9a113440611dce3b
Parents: b7e72e1
Author: Ariel Weisberg <ar...@weisberg.ws>
Authored: Wed Jun 24 08:09:52 2015 +0200
Committer: Robert Stupp <sn...@snazy.de>
Committed: Wed Jun 24 08:09:52 2015 +0200

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 build.xml                                       |  25 +-
 test/conf/logback-test.xml                      |  45 +-
 .../CassandraBriefJUnitResultFormatter.java     |  13 +-
 .../CassandraXMLJUnitResultFormatter.java       |  11 +
 .../org/apache/cassandra/ConsoleAppender.java   |  81 ++++
 .../apache/cassandra/LogbackStatusListener.java | 454 +++++++++++++++++++
 .../org/apache/cassandra/TeeingAppender.java    |  79 ++++
 8 files changed, 680 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 33869cb..d2d1d5f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.0:
+ * Improve log output from unit tests (CASSANDRA-9528)
  * Add algorithmic token allocation (CASSANDRA-7032)
  * Add nodetool command to replay batchlog (CASSANDRA-9547)
  * Make file buffer cache independent of paths being read (CASSANDRA-8897)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/build.xml
----------------------------------------------------------------------
diff --git a/build.xml b/build.xml
index 1fbc2fa..3d83ee6 100644
--- a/build.xml
+++ b/build.xml
@@ -132,6 +132,20 @@
       <format property="YEAR" pattern="yyyy"/>
     </tstamp>
 
+    <!-- Check if all tests are being run or just one. If it's all tests don't spam the console with test output.
+         If it's an individual test print the output from the test under the assumption someone is debugging the test
+         and wants to know what is going on without having to context switch to the log file that is generated.
+         Debug level output still needs to be retrieved from the log file.  -->
+    <script language="javascript">
+        if (project.getProperty("cassandra.keepBriefBrief") == null)
+        {
+            if (project.getProperty("test.name").equals("*Test"))
+                project.setProperty("cassandra.keepBriefBrief", "true");
+            else
+                project.setProperty("cassandra.keepBriefBrief", "false");
+        }
+    </script>
+
     <!--
          Add all the dependencies.
     -->
@@ -149,7 +163,7 @@
           <exclude name="**/*-sources.jar"/>
         </fileset>
     </path>
-	
+
 	<path id="cobertura.classpath">
 		<pathelement location="${cobertura.classes.dir}"/>
 	</path>
@@ -709,7 +723,7 @@
             description="Run in test mode.  Not for production use!">
       <java classname="org.apache.cassandra.service.CassandraDaemon" fork="true">
         <classpath>
-          <path refid="cassandra.classpath"/>  
+          <path refid="cassandra.classpath"/>
           <pathelement location="${test.conf}"/>
         </classpath>
         <jvmarg value="-Dstorage-config=${test.conf}"/>
@@ -1131,8 +1145,8 @@
     <attribute name="filelist" default="" />
     <attribute name="poffset" default="0"/>
     <attribute name="testtag" default=""/>
-    
     <attribute name="usejacoco" default="no"/>
+
     <sequential>
       <condition property="additionalagent"
                  value="-javaagent:${build.dir.lib}/jars/jacocoagent.jar=destfile=${jacoco.execfile}"
@@ -1157,7 +1171,8 @@
         <jvmarg value="-Dcassandra.test.use_prepared=${cassandra.test.use_prepared}"/>
 	    <jvmarg value="-Dcassandra.test.offsetseed=@{poffset}"/>
         <jvmarg value="-Dcassandra.test.sstableformatdevelopment=true"/>
-        <jvmarg value="-Dcassandra.testtag=@{testtag}"/>
+        <jvmarg value="-Dcassandra.testtag=@{testtag}"/> 
+        <jvmarg value="-Dcassandra.keepBriefBrief=${cassandra.keepBriefBrief}" />
 	<optjvmargs/>
         <classpath>
           <path refid="cassandra.classpath" />
@@ -1989,7 +2004,7 @@
             <arg value="-properties" />
             <arg value="${ecj.properties}" />
             <arg value="-cp" />
-            <arg value="${toString:cassandra.classpath}" />
+            <arg value="${toString:cassandra.build.classpath}" />
             <arg value="${build.src.java}" />
         </java>
   </target>

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/conf/logback-test.xml
----------------------------------------------------------------------
diff --git a/test/conf/logback-test.xml b/test/conf/logback-test.xml
index b503f04..8cb2d6f 100644
--- a/test/conf/logback-test.xml
+++ b/test/conf/logback-test.xml
@@ -18,12 +18,17 @@
 -->
 
 <configuration debug="false">
+  <!-- Shutdown hook ensures that async appender flushes -->
   <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
 
+  <!-- Status listener is used to wrap stdout/stderr and tee to log file -->
+  <statusListener class="org.apache.cassandra.LogbackStatusListener" />
+
   <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-    <file>./build/test/logs/system.log</file>
+
+    <file>./build/test/output/${cassandra.testtag}/TEST-${suitename}.log</file>
     <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
-      <fileNamePattern>./build/test/logs/system.log.%i.zip</fileNamePattern>
+      <fileNamePattern>./build/test/logs/${cassandra.testtag}/TEST-${suitename}.log.%i.gz</fileNamePattern>
       <minIndex>1</minIndex>
       <maxIndex>20</maxIndex>
     </rollingPolicy>
@@ -31,41 +36,39 @@
     <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
       <maxFileSize>20MB</maxFileSize>
     </triggeringPolicy>
+
     <encoder>
       <pattern>%-5level [%thread] %date{ISO8601} %msg%n</pattern>
       <immediateFlush>false</immediateFlush>
     </encoder>
   </appender>
   
-  <appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
-      <discardingThreshold>0</discardingThreshold>
-      <maxFlushTime>0</maxFlushTime>
-      <queueSize>1024</queueSize>
-      <appender-ref ref="FILE"/>
-  </appender>
-
-    <appender name="STDERR" target="System.err" class="ch.qos.logback.core.ConsoleAppender">
+  <appender name="STDOUT" target="System.out" class="org.apache.cassandra.ConsoleAppender">
     <encoder>
       <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
     </encoder>
     <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-      <level>WARN</level>
+      <level>INFO</level>
     </filter>
   </appender>
-  
-  <appender name="STDOUT" target="System.out" class="ch.qos.logback.core.ConsoleAppender">
-    <encoder>
-      <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
-    </encoder>
-    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-      <level>WARN</level>
-    </filter>
+
+  <appender name="TEE" class="org.apache.cassandra.TeeingAppender">
+      <appender-ref ref="FILE"/>
+      <appender-ref ref="STDOUT"/>
   </appender>
 
   <logger name="org.apache.hadoop" level="WARN"/>
 
+  <!-- Do not change the name of this appender. LogbackStatusListener uses the thread name
+       tied to the appender name to know when to write to real stdout/stderr vs forwarding to logback -->
+  <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+      <discardingThreshold>0</discardingThreshold>
+      <maxFlushTime>0</maxFlushTime>
+      <queueSize>1024</queueSize>
+      <appender-ref ref="TEE"/>
+  </appender>
+
   <root level="DEBUG">
-    <appender-ref ref="ASYNCFILE" />
-    <appender-ref ref="STDOUT" />
+    <appender-ref ref="ASYNC" />
   </root>
 </configuration>

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/unit/org/apache/cassandra/CassandraBriefJUnitResultFormatter.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/CassandraBriefJUnitResultFormatter.java b/test/unit/org/apache/cassandra/CassandraBriefJUnitResultFormatter.java
index 084858f..a6c5997 100644
--- a/test/unit/org/apache/cassandra/CassandraBriefJUnitResultFormatter.java
+++ b/test/unit/org/apache/cassandra/CassandraBriefJUnitResultFormatter.java
@@ -49,6 +49,8 @@ public class CassandraBriefJUnitResultFormatter implements JUnitResultFormatter,
 
     private static final String tag = System.getProperty("cassandra.testtag", "");
 
+    private static final Boolean keepBriefBrief = Boolean.getBoolean("cassandra.keepBriefBrief");
+
     /**
      * Where to write the log to.
      */
@@ -145,7 +147,12 @@ public class CassandraBriefJUnitResultFormatter implements JUnitResultFormatter,
      * @param suite the test suite
      */
     public void endTestSuite(JUnitTest suite) {
-        StringBuffer sb = new StringBuffer("Tests run: ");
+        StringBuffer sb = new StringBuffer("Testsuite: ");
+        String n = suite.getName();
+        if (n != null && !tag.isEmpty())
+            n = n + "-" + tag;
+        sb.append(n);
+        sb.append(" Tests run: ");
         sb.append(suite.runCount());
         sb.append(", Failures: ");
         sb.append(suite.failureCount());
@@ -160,7 +167,7 @@ public class CassandraBriefJUnitResultFormatter implements JUnitResultFormatter,
         sb.append(StringUtils.LINE_SEP);
 
         // append the err and output streams to the log
-        if (systemOutput != null && systemOutput.length() > 0) {
+        if (!keepBriefBrief && systemOutput != null && systemOutput.length() > 0) {
             sb.append("------------- Standard Output ---------------")
                     .append(StringUtils.LINE_SEP)
                     .append(systemOutput)
@@ -168,7 +175,7 @@ public class CassandraBriefJUnitResultFormatter implements JUnitResultFormatter,
                     .append(StringUtils.LINE_SEP);
         }
 
-        if (systemError != null && systemError.length() > 0) {
+        if (!keepBriefBrief && systemError != null && systemError.length() > 0) {
             sb.append("------------- Standard Error -----------------")
                     .append(StringUtils.LINE_SEP)
                     .append(systemError)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java
index 066315d..b342b45 100644
--- a/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java
+++ b/test/unit/org/apache/cassandra/CassandraXMLJUnitResultFormatter.java
@@ -74,6 +74,17 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
 
     private static final String tag = System.getProperty("cassandra.testtag", "");
 
+    /*
+     * Set the property for the test suite name so that log configuration can pick it up
+     * and log to a file specific to this test suite
+     */
+    static
+    {
+        String command = System.getProperty("sun.java.command");
+        String args[] = command.split(" ");
+        System.setProperty("suitename", args[1]);
+    }
+
     /**
      * The XML document.
      */

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/unit/org/apache/cassandra/ConsoleAppender.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/ConsoleAppender.java b/test/unit/org/apache/cassandra/ConsoleAppender.java
new file mode 100644
index 0000000..aa8af1e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/ConsoleAppender.java
@@ -0,0 +1,81 @@
+/*
+ * 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.cassandra;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import ch.qos.logback.core.OutputStreamAppender;
+import ch.qos.logback.core.status.Status;
+import ch.qos.logback.core.status.WarnStatus;
+
+public class ConsoleAppender<E> extends OutputStreamAppender<E>
+{
+    private String target = "System.out";
+
+    public void setTarget(String target)
+    {
+        if(!(target.equals("System.out") || target.equals("System.err")))
+        {
+            Status status = new WarnStatus("[" + target + "] should be one of System.out or System.err", this);
+            status.add(new WarnStatus("Using default target System.out", this));
+            addStatus(status);
+            return;
+        }
+        this.target = target;
+    }
+
+    public String getTarget()
+    {
+        return target;
+    }
+
+    @Override
+    public void start()
+    {
+        @SuppressWarnings("resource")
+        final PrintStream targetStream = target.equals("System.out") ? LogbackStatusListener.originalOut : LogbackStatusListener.originalErr;
+        setOutputStream(new OutputStream() {
+            @Override
+            public void write(int b)
+            {
+                targetStream.write(b);
+            }
+
+            @Override
+            public void write(byte[] b) throws IOException
+            {
+                targetStream.write(b);
+            }
+
+            @Override
+            public void write(byte[] b, int off, int len)
+            {
+                targetStream.write(b, off, len);
+            }
+
+            @Override
+            public void flush()
+            {
+                targetStream.flush();
+            }
+        });
+        super.start();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/unit/org/apache/cassandra/LogbackStatusListener.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/LogbackStatusListener.java b/test/unit/org/apache/cassandra/LogbackStatusListener.java
new file mode 100644
index 0000000..756f7eb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/LogbackStatusListener.java
@@ -0,0 +1,454 @@
+/*
+ * 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.cassandra;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Locale;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.core.status.Status;
+import ch.qos.logback.core.status.StatusListener;
+
+/*
+ * Listen for logback readiness and then redirect stdout/stderr to logback
+ */
+public class LogbackStatusListener implements StatusListener
+{
+
+    public static final PrintStream originalOut = System.out;
+
+    public static final PrintStream originalErr = System.err;
+
+    private boolean hadError = false;
+
+    private PrintStream replacementOut;
+
+    private PrintStream replacementErr;
+
+    @Override
+    public void addStatusEvent(Status s)
+    {
+        if (s.getLevel() != 0 || s.getEffectiveLevel() != 0)
+            hadError = true;
+
+        if (!hadError && s.getMessage().equals("Registering current configuration as safe fallback point"))
+        {
+            try
+            {
+                installReplacementStreams();
+            }
+            catch (Exception e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        if (s.getMessage().equals("Logback context being closed via shutdown hook"))
+        {
+            if (replacementOut != null) replacementOut.flush();
+            if (replacementErr != null) replacementErr.flush();
+            System.setErr(originalErr);
+            System.setOut(originalOut);
+        }
+    }
+
+    private void installReplacementStreams() throws Exception
+    {
+        Logger stdoutLogger = LoggerFactory.getLogger("stdout");
+        Logger stderrLogger = LoggerFactory.getLogger("stderr");
+
+        replacementOut = wrapLogger(stdoutLogger, originalOut, "sun.stdout.encoding", false);
+        System.setOut(replacementOut);
+        replacementErr = wrapLogger(stderrLogger, originalErr, "sun.stderr.encoding", true);
+        System.setErr(replacementErr);
+    }
+
+    private static PrintStream wrapLogger(final Logger logger, final PrintStream original, String encodingProperty, boolean error) throws Exception
+    {
+        final String encoding = System.getProperty(encodingProperty);
+        OutputStream os = new OutputStream()
+        {
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+            @Override
+            public void write(int b) throws IOException
+            {
+                baos.write(b);
+            }
+
+            @Override
+            public void write(byte[] b, int offset, int length)
+            {
+                baos.write(b,  offset, length);
+            }
+
+            @Override
+            public void write(byte[] b)
+            {
+                write(b, 0, b.length);
+            }
+
+            @Override
+            public void flush() throws IOException
+            {
+                try
+                {
+                    //Filter out stupid PrintStream empty flushes
+                    if (baos.size() == 0) return;
+
+                    //Filter out newlines, log framework provides its own
+                    if (baos.size() == 1)
+                    {
+                        byte[] bytes = baos.toByteArray();
+                        if (bytes[0] == 0xA)
+                            return;
+                    }
+
+                    //Filter out Windows newline
+                    if (baos.size() == 2)
+                    {
+                        byte[] bytes = baos.toByteArray();
+                        if (bytes[0] == 0xD && bytes[1] == 0xA)
+                            return;
+                    }
+
+                    String statement;
+                    if (encoding != null)
+                        statement = new String(baos.toByteArray(), encoding);
+                    else
+                        statement = new String(baos.toByteArray());
+
+                    if (error)
+                        logger.error(statement);
+                    else
+                        logger.info(statement);
+                }
+                finally
+                {
+                    baos.reset();
+                }
+            }
+        };
+
+        if (encoding != null)
+            return new PrintStream(os, true, encoding);
+        return new PrintStream(os, true)
+        {
+
+            private long asyncAppenderThreadId = Long.MIN_VALUE;
+
+            /*
+             * Long and the short of it is that we don't want to serve logback a fake System.out/err.
+             * ConsoleAppender is replaced so it always goes to the real System.out/err, but logback itself
+             * will at times try to log to System.out/err when it has issues.
+             *
+             * Now here is the problem. There is a deadlock if a thread logs to System.out, blocks on the async
+             * appender queue, and the async appender thread tries to log to System.out directly as part of some
+             * internal logback issue.
+             *
+             * So to prevent this we have to exhaustively check before locking in the PrintStream and forward
+             * to real System.out/err if it is the async appender
+             */
+            private boolean isAsyncAppender()
+            {
+                //Set the thread id based on the name
+                if (asyncAppenderThreadId == Long.MIN_VALUE)
+                        asyncAppenderThreadId = Thread.currentThread().getName().equals("AsyncAppender-Worker-ASYNC") ? Thread.currentThread().getId() : asyncAppenderThreadId;
+                if (Thread.currentThread().getId() == asyncAppenderThreadId)
+                    original.println("Was in async appender");
+                return Thread.currentThread().getId() == asyncAppenderThreadId;
+            }
+
+            @Override
+            public void flush()
+            {
+                if (isAsyncAppender())
+                    original.flush();
+                else
+                    super.flush();
+            }
+
+            @Override
+            public void close()
+            {
+                if (isAsyncAppender())
+                    original.close();
+                else
+                    super.flush();
+            }
+
+            @Override
+            public void write(int b)
+            {
+                if (isAsyncAppender())
+                    original.write(b);
+                else
+                    super.write(b);
+            }
+
+            @Override
+            public void write(byte[] buf, int off, int len)
+            {
+                if (isAsyncAppender())
+                    original.write(buf, off, len);
+                else
+                    super.write(buf, off, len);
+            }
+
+            @Override
+            public void print(boolean b)
+            {
+                if (isAsyncAppender())
+                    original.print(b);
+                else
+                    super.print(b);
+            }
+
+            @Override
+            public void print(char c)
+            {
+                if (isAsyncAppender())
+                    original.print(c);
+                else
+                    super.print(c);
+            }
+
+            @Override
+            public void print(int i)
+            {
+                if (isAsyncAppender())
+                    original.print(i);
+                else
+                    super.print(i);
+            }
+
+            @Override
+            public void print(long l)
+            {
+                if (isAsyncAppender())
+                    original.print(l);
+                else
+                    super.print(l);
+            }
+
+            @Override
+            public void print(float f)
+            {
+                if (isAsyncAppender())
+                    original.print(f);
+                else
+                    super.print(f);
+            }
+
+            @Override
+            public void print(double d)
+            {
+                if (isAsyncAppender())
+                    original.print(d);
+                else
+                    super.print(d);
+            }
+
+            @Override
+            public void print(char[] s)
+            {
+                if(isAsyncAppender())
+                    original.println(s);
+                else
+                    super.print(s);
+            }
+
+            @Override
+            public void print(String s)
+            {
+                if (isAsyncAppender())
+                    original.print(s);
+                else
+                    super.print(s);
+            }
+
+            @Override
+            public void print(Object obj)
+            {
+                if (isAsyncAppender())
+                    original.print(obj);
+                else
+                    super.print(obj);
+            }
+
+            @Override
+            public void println()
+            {
+                if (isAsyncAppender())
+                    original.println();
+                else
+                    super.println();
+            }
+
+            @Override
+            public void println(boolean v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(char v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(int v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(long v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(float v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(double v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(char[] v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(String v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public void println(Object v)
+            {
+                if (isAsyncAppender())
+                    original.println(v);
+                else
+                    super.println(v);
+            }
+
+            @Override
+            public PrintStream printf(String format, Object... args)
+            {
+                if (isAsyncAppender())
+                    return original.printf(format, args);
+                else
+                    return super.printf(format, args);
+            }
+
+            @Override
+            public PrintStream printf(Locale l, String format, Object... args)
+            {
+                if (isAsyncAppender())
+                    return original.printf(l, format, args);
+                else
+                    return super.printf(l, format, args);
+            }
+
+            @Override
+            public PrintStream format(String format, Object... args)
+            {
+                if (isAsyncAppender())
+                    return original.format(format, args);
+                else
+                    return super.format(format, args);
+            }
+
+            @Override
+            public PrintStream format(Locale l, String format, Object... args)
+            {
+                if (isAsyncAppender())
+                    return original.format(l, format, args);
+                else
+                    return super.format(l, format, args);
+            }
+
+            @Override
+            public PrintStream append(CharSequence csq)
+            {
+                if (isAsyncAppender())
+                    return original.append(csq);
+                else
+                    return super.append(csq);
+            }
+
+            @Override
+            public PrintStream append(CharSequence csq, int start, int end)
+            {
+                if (isAsyncAppender())
+                    return original.append(csq, start, end);
+                else
+                    return super.append(csq, start, end);
+            }
+
+            @Override
+            public PrintStream append(char c)
+            {
+                if (isAsyncAppender())
+                    return original.append(c);
+                else
+                    return super.append(c);
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/c8d3cc14/test/unit/org/apache/cassandra/TeeingAppender.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/TeeingAppender.java b/test/unit/org/apache/cassandra/TeeingAppender.java
new file mode 100644
index 0000000..4e3735e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/TeeingAppender.java
@@ -0,0 +1,79 @@
+/*
+ * 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.cassandra;
+
+import java.util.Iterator;
+
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.UnsynchronizedAppenderBase;
+import ch.qos.logback.core.spi.AppenderAttachable;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+
+public class TeeingAppender<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E>
+{
+    AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<>();
+
+    @Override
+    protected void append(E e)
+    {
+        aai.appendLoopOnAppenders(e);
+    }
+
+    @Override
+    public void addAppender(Appender<E> appender)
+    {
+        aai.addAppender(appender);
+    }
+
+    @Override
+    public void detachAndStopAllAppenders()
+    {
+        aai.detachAndStopAllAppenders();
+    }
+
+    @Override
+    public boolean detachAppender(Appender<E> appender)
+    {
+        return aai.detachAppender(appender);
+    }
+
+    @Override
+    public boolean detachAppender(String name)
+    {
+        return aai.detachAppender(name);
+    }
+
+    @Override
+    public Appender<E> getAppender(String name)
+    {
+        return aai.getAppender(name);
+    }
+
+    @Override
+    public boolean isAttached(Appender<E> appender)
+    {
+        return aai.isAttached(appender);
+    }
+
+    @Override
+    public Iterator<Appender<E>> iteratorForAppenders()
+    {
+        return aai.iteratorForAppenders();
+    }
+
+}