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(); + } + +}