You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by dc...@apache.org on 2020/10/06 20:48:41 UTC

[cassandra] branch trunk updated: in-jvm dtest now exposes stdout and stderr for nodetool

This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 83e1e9e  in-jvm dtest now exposes stdout and stderr for nodetool
83e1e9e is described below

commit 83e1e9e45193322f18f57aa7cc4ad31d9d5a152d
Author: Yifan Cai <yc...@gmail.com>
AuthorDate: Tue Oct 6 08:54:17 2020 -0700

    in-jvm dtest now exposes stdout and stderr for nodetool
    
    patch by Yifan Cai; reviewed by Alex Petrov, David Capwell for CASSANDRA-16057
---
 src/java/org/apache/cassandra/tools/NodeProbe.java | 16 +++-
 src/java/org/apache/cassandra/tools/NodeTool.java  | 51 +++++++-----
 .../tools/{nodetool/Version.java => Output.java}   | 22 +++---
 .../cassandra/tools/nodetool/BootstrapResume.java  |  4 +-
 .../apache/cassandra/tools/nodetool/Cleanup.java   |  2 +-
 .../cassandra/tools/nodetool/ClearSnapshot.java    |  4 +-
 .../cassandra/tools/nodetool/ClientStats.java      | 24 +++---
 .../tools/nodetool/CompactionHistory.java          |  2 +-
 .../cassandra/tools/nodetool/CompactionStats.java  | 18 +++--
 .../cassandra/tools/nodetool/DescribeCluster.java  | 50 ++++++------
 .../cassandra/tools/nodetool/DescribeRing.java     |  8 +-
 .../tools/nodetool/FailureDetectorInfo.java        |  4 +-
 .../cassandra/tools/nodetool/GarbageCollect.java   |  2 +-
 .../apache/cassandra/tools/nodetool/GcStats.java   |  6 +-
 .../tools/nodetool/GetBatchlogReplayTrottle.java   |  4 +-
 .../tools/nodetool/GetCompactionThreshold.java     |  8 +-
 .../tools/nodetool/GetCompactionThroughput.java    |  4 +-
 .../cassandra/tools/nodetool/GetConcurrency.java   | 10 +--
 .../tools/nodetool/GetConcurrentCompactors.java    |  4 +-
 .../tools/nodetool/GetConcurrentViewBuilders.java  |  4 +-
 .../cassandra/tools/nodetool/GetEndpoints.java     |  4 +-
 .../cassandra/tools/nodetool/GetFullQueryLog.java  |  2 +-
 .../tools/nodetool/GetInterDCStreamThroughput.java |  2 +-
 .../cassandra/tools/nodetool/GetLoggingLevels.java |  6 +-
 .../cassandra/tools/nodetool/GetMaxHintWindow.java |  4 +-
 .../cassandra/tools/nodetool/GetSSTables.java      |  4 +-
 .../apache/cassandra/tools/nodetool/GetSeeds.java  |  6 +-
 .../tools/nodetool/GetStreamThroughput.java        |  4 +-
 .../cassandra/tools/nodetool/GetTimeout.java       |  2 +-
 .../tools/nodetool/GetTraceProbability.java        |  2 +-
 .../cassandra/tools/nodetool/GossipInfo.java       |  2 +-
 .../apache/cassandra/tools/nodetool/Import.java    | 11 ++-
 .../org/apache/cassandra/tools/nodetool/Info.java  | 42 +++++-----
 .../cassandra/tools/nodetool/ListSnapshots.java    | 10 ++-
 .../apache/cassandra/tools/nodetool/NetStats.java  | 92 +++++++++++-----------
 .../cassandra/tools/nodetool/ProfileLoad.java      | 17 ++--
 .../cassandra/tools/nodetool/ProxyHistograms.java  | 13 +--
 .../cassandra/tools/nodetool/RangeKeySample.java   |  6 +-
 .../apache/cassandra/tools/nodetool/Refresh.java   |  4 +-
 .../cassandra/tools/nodetool/ReloadSeeds.java      |  8 +-
 .../cassandra/tools/nodetool/RemoveNode.java       |  4 +-
 .../apache/cassandra/tools/nodetool/Repair.java    |  2 +-
 .../cassandra/tools/nodetool/RepairAdmin.java      | 30 ++++---
 .../org/apache/cassandra/tools/nodetool/Ring.java  | 52 ++++++------
 .../org/apache/cassandra/tools/nodetool/Scrub.java |  2 +-
 .../cassandra/tools/nodetool/SetConcurrency.java   |  2 +-
 .../org/apache/cassandra/tools/nodetool/Sjk.java   | 83 +++++++++++--------
 .../apache/cassandra/tools/nodetool/Snapshot.java  | 12 +--
 .../apache/cassandra/tools/nodetool/Status.java    | 22 +++---
 .../tools/nodetool/StatusAutoCompaction.java       |  8 +-
 .../cassandra/tools/nodetool/StatusBackup.java     |  4 +-
 .../cassandra/tools/nodetool/StatusBinary.java     |  4 +-
 .../cassandra/tools/nodetool/StatusGossip.java     |  4 +-
 .../cassandra/tools/nodetool/StatusHandoff.java    |  6 +-
 .../cassandra/tools/nodetool/TableHistograms.java  | 18 +++--
 .../cassandra/tools/nodetool/TableStats.java       |  2 +-
 .../apache/cassandra/tools/nodetool/TpStats.java   |  4 +-
 .../cassandra/tools/nodetool/UpgradeSSTable.java   |  4 +-
 .../apache/cassandra/tools/nodetool/Verify.java    |  8 +-
 .../apache/cassandra/tools/nodetool/Version.java   |  4 +-
 .../cassandra/tools/nodetool/ViewBuildStatus.java  | 10 ++-
 .../tools/nodetool/formatter/TableBuilder.java     |  2 +-
 .../tools/nodetool/stats/TpStatsPrinter.java       |  6 +-
 .../cassandra/distributed/impl/Instance.java       | 60 ++++++++++++--
 .../shared/NodeToolResultWithOutput.java           |  2 +
 .../cassandra/distributed/test/NodeToolTest.java   | 15 +++-
 .../cassandra/distributed/util/NodetoolUtils.java  |  7 +-
 .../apache/cassandra/tools/nodetool/SjkTest.java   | 17 ++--
 .../apache/cassandra/stress/CompactionStress.java  |  2 +-
 69 files changed, 512 insertions(+), 372 deletions(-)

diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java
index 3020fec..16d7f73 100644
--- a/src/java/org/apache/cassandra/tools/NodeProbe.java
+++ b/src/java/org/apache/cassandra/tools/NodeProbe.java
@@ -59,7 +59,6 @@ import org.apache.cassandra.batchlog.BatchlogManager;
 import org.apache.cassandra.batchlog.BatchlogManagerMBean;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStoreMBean;
-import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.compaction.CompactionManager;
 import org.apache.cassandra.db.compaction.CompactionManagerMBean;
 import org.apache.cassandra.fql.FullQueryLoggerOptions;
@@ -135,6 +134,7 @@ public class NodeProbe implements AutoCloseable
     protected HintsServiceMBean hsProxy;
     protected BatchlogManagerMBean bmProxy;
     protected ActiveRepairServiceMBean arsProxy;
+    protected Output output;
     private boolean failed;
 
     /**
@@ -153,6 +153,7 @@ public class NodeProbe implements AutoCloseable
         this.port = port;
         this.username = username;
         this.password = password;
+        this.output = Output.CONSOLE;
         connect();
     }
 
@@ -167,6 +168,7 @@ public class NodeProbe implements AutoCloseable
     {
         this.host = host;
         this.port = port;
+        this.output = Output.CONSOLE;
         connect();
     }
 
@@ -180,6 +182,7 @@ public class NodeProbe implements AutoCloseable
     {
         this.host = host;
         this.port = defaultPort;
+        this.output = Output.CONSOLE;
         connect();
     }
 
@@ -188,6 +191,7 @@ public class NodeProbe implements AutoCloseable
         // this constructor is only used for extensions to rewrite their own connect method
         this.host = "";
         this.port = 0;
+        this.output = Output.CONSOLE;
     }
 
     /**
@@ -270,6 +274,16 @@ public class NodeProbe implements AutoCloseable
         }
     }
 
+    public void setOutput(Output output)
+    {
+        this.output = output;
+    }
+
+    public Output output()
+    {
+        return output;
+    }
+
     public int forceKeyspaceCleanup(int jobs, String keyspaceName, String... tables) throws IOException, ExecutionException, InterruptedException
     {
         return ssProxy.forceKeyspaceCleanup(jobs, keyspaceName, tables);
diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java
index f558267..c3cebd8 100644
--- a/src/java/org/apache/cassandra/tools/NodeTool.java
+++ b/src/java/org/apache/cassandra/tools/NodeTool.java
@@ -44,7 +44,6 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Scanner;
 import java.util.SortedMap;
-import java.util.function.Consumer;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Throwables;
@@ -78,20 +77,22 @@ public class NodeTool
     private static final String HISTORYFILE = "nodetool.history";
 
     private final INodeProbeFactory nodeProbeFactory;
+    private final Output output;
 
     public static void main(String... args)
     {
-        System.exit(new NodeTool(new NodeProbeFactory()).execute(args));
+        System.exit(new NodeTool(new NodeProbeFactory(), Output.CONSOLE).execute(args));
     }
 
-    public NodeTool(INodeProbeFactory nodeProbeFactory)
+    public NodeTool(INodeProbeFactory nodeProbeFactory, Output output)
     {
         this.nodeProbeFactory = nodeProbeFactory;
+        this.output = output;
     }
 
     public int execute(String... args)
     {
-        List<Class<? extends Consumer<INodeProbeFactory>>> commands = newArrayList(
+        List<Class<? extends NodeToolCmdRunnable>> commands = newArrayList(
                 CassHelp.class,
                 Info.class,
                 Ring.class,
@@ -212,7 +213,7 @@ public class NodeTool
                 DisableOldProtocolVersions.class
         );
 
-        Cli.CliBuilder<Consumer<INodeProbeFactory>> builder = Cli.builder("nodetool");
+        Cli.CliBuilder<NodeToolCmdRunnable> builder = Cli.builder("nodetool");
 
         builder.withDescription("Manage your Cassandra cluster")
                  .withDefaultCommand(CassHelp.class)
@@ -233,14 +234,14 @@ public class NodeTool
                .withCommand(RepairAdmin.SummarizePendingCmd.class)
                .withCommand(RepairAdmin.SummarizeRepairedCmd.class);
 
-        Cli<Consumer<INodeProbeFactory>> parser = builder.build();
+        Cli<NodeToolCmdRunnable> parser = builder.build();
 
         int status = 0;
         try
         {
-            Consumer<INodeProbeFactory> parse = parser.parse(args);
+            NodeToolCmdRunnable parse = parser.parse(args);
             printHistory(args);
-            parse.accept(nodeProbeFactory);
+            parse.run(nodeProbeFactory, output);
         } catch (IllegalArgumentException |
                 IllegalStateException |
                 ParseArgumentsMissingException |
@@ -284,26 +285,31 @@ public class NodeTool
 
     protected void badUse(Exception e)
     {
-        System.out.println("nodetool: " + e.getMessage());
-        System.out.println("See 'nodetool help' or 'nodetool help <command>'.");
+        output.out.println("nodetool: " + e.getMessage());
+        output.out.println("See 'nodetool help' or 'nodetool help <command>'.");
     }
 
     protected void err(Throwable e)
     {
-        System.err.println("error: " + e.getMessage());
-        System.err.println("-- StackTrace --");
-        System.err.println(getStackTraceAsString(e));
+        output.err.println("error: " + e.getMessage());
+        output.err.println("-- StackTrace --");
+        output.err.println(getStackTraceAsString(e));
     }
 
-    public static class CassHelp extends Help implements Consumer<INodeProbeFactory>
+    public static class CassHelp extends Help implements NodeToolCmdRunnable
     {
-        public void accept(INodeProbeFactory nodeProbeFactory)
+        public void run(INodeProbeFactory nodeProbeFactory, Output output)
         {
             run();
         }
     }
 
-    public static abstract class NodeToolCmd implements Consumer<INodeProbeFactory>
+    interface NodeToolCmdRunnable
+    {
+        void run(INodeProbeFactory nodeProbeFactory, Output output);
+    }
+
+    public static abstract class NodeToolCmd implements NodeToolCmdRunnable
     {
 
         @Option(type = OptionType.GLOBAL, name = {"-h", "--host"}, description = "Node hostname or ip address")
@@ -325,14 +331,17 @@ public class NodeTool
         protected boolean printPort = false;
 
         private INodeProbeFactory nodeProbeFactory;
+        protected Output output;
 
-        public void accept(INodeProbeFactory nodeProbeFactory)
+        @Override
+        public void run(INodeProbeFactory nodeProbeFactory, Output output)
         {
             this.nodeProbeFactory = nodeProbeFactory;
-            run();
+            this.output = output;
+            runInternal();
         }
 
-        public void run()
+        public void runInternal()
         {
             if (isNotEmpty(username)) {
                 if (isNotEmpty(passwordFilePath))
@@ -405,10 +414,12 @@ public class NodeTool
                     nodeClient = nodeProbeFactory.create(host, parseInt(port));
                 else
                     nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password);
+
+                nodeClient.setOutput(output);
             } catch (IOException | SecurityException e)
             {
                 Throwable rootCause = Throwables.getRootCause(e);
-                System.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage()));
+                output.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage()));
                 System.exit(1);
             }
 
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Version.java b/src/java/org/apache/cassandra/tools/Output.java
similarity index 66%
copy from src/java/org/apache/cassandra/tools/nodetool/Version.java
copy to src/java/org/apache/cassandra/tools/Output.java
index 395a247..1d2fcb3 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Version.java
+++ b/src/java/org/apache/cassandra/tools/Output.java
@@ -15,19 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.cassandra.tools.nodetool;
 
-import io.airlift.airline.Command;
+package org.apache.cassandra.tools;
 
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import java.io.PrintStream;
 
-@Command(name = "version", description = "Print cassandra version")
-public class Version extends NodeToolCmd
+public class Output
 {
-    @Override
-    public void execute(NodeProbe probe)
+    public final static Output CONSOLE = new Output(System.out, System.err);
+
+    public final PrintStream out;
+    public final PrintStream err;
+
+    public Output(PrintStream out, PrintStream err)
     {
-        System.out.println("ReleaseVersion: " + probe.getReleaseVersion());
+        this.out = out;
+        this.err = err;
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/BootstrapResume.java b/src/java/org/apache/cassandra/tools/nodetool/BootstrapResume.java
index 7be9173..b005818 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/BootstrapResume.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/BootstrapResume.java
@@ -33,11 +33,11 @@ public class BootstrapResume extends NodeToolCmd
     {
         try
         {
-            probe.resumeBootstrap(System.out);
+            probe.resumeBootstrap(probe.output().out);
         }
         catch (IOException e)
         {
             throw new IOError(e);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java b/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
index 200d255..8c40209 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Cleanup.java
@@ -52,7 +52,7 @@ public class Cleanup extends NodeToolCmd
 
             try
             {
-                probe.forceKeyspaceCleanup(System.out, jobs, keyspace, tableNames);
+                probe.forceKeyspaceCleanup(probe.output().out, jobs, keyspace, tableNames);
             }
             catch (Exception e)
             {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ClearSnapshot.java b/src/java/org/apache/cassandra/tools/nodetool/ClearSnapshot.java
index 12322d0..1c05ae5 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ClearSnapshot.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ClearSnapshot.java
@@ -66,7 +66,7 @@ public class ClearSnapshot extends NodeToolCmd
         else
             sb.append(" with snapshot name [").append(snapshotName).append("]");
 
-        System.out.println(sb.toString());
+        probe.output().out.println(sb.toString());
 
         try
         {
@@ -76,4 +76,4 @@ public class ClearSnapshot extends NodeToolCmd
             throw new RuntimeException("Error during clearing snapshots", e);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
index 3bf46b4..b9bf45e 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
@@ -46,9 +47,10 @@ public class ClientStats extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         if (clearConnectionHistory)
         {
-            System.out.println("Clearing connection history");
+            out.println("Clearing connection history");
             probe.clearConnectionHistory();
             return;
         }
@@ -57,8 +59,8 @@ public class ClientStats extends NodeToolCmd
         {
             SimpleDateFormat sdf = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss");
 
-            System.out.println("Clients by protocol version");
-            System.out.println();
+            out.println("Clients by protocol version");
+            out.println();
 
             List<Map<String, String>> clients = (List<Map<String, String>>) probe.getClientMetric("clientsByProtocolVersion");
 
@@ -74,8 +76,8 @@ public class ClientStats extends NodeToolCmd
                               sdf.format(new Date(Long.valueOf(client.get(ClientStat.LAST_SEEN_TIME)))));
                 }
 
-                table.printTo(System.out);
-                System.out.println();
+                table.printTo(out);
+                out.println();
             }
 
             return;
@@ -101,21 +103,21 @@ public class ClientStats extends NodeToolCmd
                               conn.get(ConnectedClient.DRIVER_NAME),
                               conn.get(ConnectedClient.DRIVER_VERSION));
                 }
-                table.printTo(System.out);
-                System.out.println();
+                table.printTo(out);
+                out.println();
             }
         }
 
         Map<String, Integer> connectionsByUser = (Map<String, Integer>) probe.getClientMetric("connectedNativeClientsByUser");
         int total = connectionsByUser.values().stream().reduce(0, Integer::sum);
-        System.out.println("Total connected clients: " + total);
-        System.out.println();
+        out.println("Total connected clients: " + total);
+        out.println();
         TableBuilder table = new TableBuilder();
         table.add("User", "Connections");
         for (Entry<String, Integer> entry : connectionsByUser.entrySet())
         {
             table.add(entry.getKey(), entry.getValue().toString());
         }
-        table.printTo(System.out);
+        table.printTo(out);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java b/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
index 8896db0..d1a5061 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/CompactionHistory.java
@@ -43,6 +43,6 @@ public class CompactionHistory extends NodeToolCmd
         }
         StatsHolder data = new CompactionHistoryHolder(probe);
         StatsPrinter printer = CompactionHistoryPrinter.from(outputFormat);
-        printer.print(data, System.out);
+        printer.print(data, probe.output().out);
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
index 497fe24..e815d74 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/CompactionStats.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.text.DecimalFormat;
 import java.util.List;
 import java.util.Map;
@@ -47,6 +48,7 @@ public class CompactionStats extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         CompactionManagerMBean cm = probe.getCompactionManagerProxy();
         Map<String, Map<String, Integer>> pendingTaskNumberByTable =
             (Map<String, Map<String, Integer>>) probe.getCompactionMetric("PendingTasksByTableName");
@@ -56,7 +58,7 @@ public class CompactionStats extends NodeToolCmd
             for (Entry<String, Integer> tableEntry : ksEntry.getValue().entrySet())
                 numTotalPendingTask += tableEntry.getValue();
         }
-        System.out.println("pending tasks: " + numTotalPendingTask);
+        out.println("pending tasks: " + numTotalPendingTask);
         for (Entry<String, Map<String, Integer>> ksEntry : pendingTaskNumberByTable.entrySet())
         {
             String ksName = ksEntry.getKey();
@@ -65,14 +67,14 @@ public class CompactionStats extends NodeToolCmd
                 String tableName = tableEntry.getKey();
                 int pendingTaskCount = tableEntry.getValue();
 
-                System.out.println("- " + ksName + '.' + tableName + ": " + pendingTaskCount);
+                out.println("- " + ksName + '.' + tableName + ": " + pendingTaskCount);
             }
         }
-        System.out.println();
-        reportCompactionTable(cm.getCompactions(), probe.getCompactionThroughput(), humanReadable);
+        out.println();
+        reportCompactionTable(cm.getCompactions(), probe.getCompactionThroughput(), humanReadable, out);
     }
 
-    public static void reportCompactionTable(List<Map<String,String>> compactions, int compactionThroughput, boolean humanReadable)
+    public static void reportCompactionTable(List<Map<String,String>> compactions, int compactionThroughput, boolean humanReadable, PrintStream out)
     {
         if (!compactions.isEmpty())
         {
@@ -97,7 +99,7 @@ public class CompactionStats extends NodeToolCmd
                 if (taskType.equals(OperationType.COMPACTION.toString()))
                     remainingBytes += total - completed;
             }
-            table.printTo(System.out);
+            table.printTo(out);
 
             String remainingTime = "n/a";
             if (compactionThroughput != 0)
@@ -105,8 +107,8 @@ public class CompactionStats extends NodeToolCmd
                 long remainingTimeInSecs = remainingBytes / (1024L * 1024L * compactionThroughput);
                 remainingTime = format("%dh%02dm%02ds", remainingTimeInSecs / 3600, (remainingTimeInSecs % 3600) / 60, (remainingTimeInSecs % 60));
             }
-            System.out.printf("%25s%10s%n", "Active compaction remaining time : ", remainingTime);
+            out.printf("%25s%10s%n", "Active compaction remaining time : ", remainingTime);
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java b/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
index 6a9f023..33a0a4d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/DescribeCluster.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -43,9 +44,10 @@ public class DescribeCluster extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         // display cluster name, snitch and partitioner
-        System.out.println("Cluster Information:");
-        System.out.println("\tName: " + probe.getClusterName());
+        out.println("Cluster Information:");
+        out.println("\tName: " + probe.getClusterName());
         String snitch = probe.getEndpointSnitchInfoProxy().getSnitchName();
         boolean dynamicSnitchEnabled = false;
         if (snitch.equals(DynamicEndpointSnitch.class.getName()))
@@ -53,16 +55,16 @@ public class DescribeCluster extends NodeToolCmd
             snitch = probe.getDynamicEndpointSnitchInfoProxy().getSubsnitchClassName();
             dynamicSnitchEnabled = true;
         }
-        System.out.println("\tSnitch: " + snitch);
-        System.out.println("\tDynamicEndPointSnitch: " + (dynamicSnitchEnabled ? "enabled" : "disabled"));
-        System.out.println("\tPartitioner: " + probe.getPartitioner());
+        out.println("\tSnitch: " + snitch);
+        out.println("\tDynamicEndPointSnitch: " + (dynamicSnitchEnabled ? "enabled" : "disabled"));
+        out.println("\tPartitioner: " + probe.getPartitioner());
 
         // display schema version for each node
-        System.out.println("\tSchema versions:");
+        out.println("\tSchema versions:");
         Map<String, List<String>> schemaVersions = printPort ? probe.getSpProxy().getSchemaVersionsWithPort() : probe.getSpProxy().getSchemaVersions();
         for (String version : schemaVersions.keySet())
         {
-            System.out.println(format("\t\t%s: %s%n", version, schemaVersions.get(version)));
+            out.println(format("\t\t%s: %s%n", version, schemaVersions.get(version)));
         }
 
         // Collect status information of all nodes
@@ -76,12 +78,12 @@ public class DescribeCluster extends NodeToolCmd
         // Get the list of all keyspaces
         List<String> keyspaces = probe.getKeyspaces();
 
-        System.out.println("Stats for all nodes:");
-        System.out.println("\tLive: " + liveNodes.size());
-        System.out.println("\tJoining: " + joiningNodes.size());
-        System.out.println("\tMoving: " + movingNodes.size());
-        System.out.println("\tLeaving: " + leavingNodes.size());
-        System.out.println("\tUnreachable: " + unreachableNodes.size());
+        out.println("Stats for all nodes:");
+        out.println("\tLive: " + liveNodes.size());
+        out.println("\tJoining: " + joiningNodes.size());
+        out.println("\tMoving: " + movingNodes.size());
+        out.println("\tLeaving: " + leavingNodes.size());
+        out.println("\tUnreachable: " + unreachableNodes.size());
 
         Map<String, String> tokensToEndpoints = probe.getTokenToEndpointMap(withPort);
         Map<String, Float> ownerships = null;
@@ -92,20 +94,20 @@ public class DescribeCluster extends NodeToolCmd
         catch (IllegalStateException ex)
         {
             ownerships = probe.getOwnershipWithPort();
-            System.out.println("Error: " + ex.getMessage());
+            out.println("Error: " + ex.getMessage());
         }
         catch (IllegalArgumentException ex)
         {
-            System.out.println("%nError: " + ex.getMessage());
+            out.println("%nError: " + ex.getMessage());
             System.exit(1);
         }
 
         SortedMap<String, SetHostStatWithPort> dcs = NodeTool.getOwnershipByDcWithPort(probe, resolveIp, tokensToEndpoints, ownerships);
 
-        System.out.println("\nData Centers: ");
+        out.println("\nData Centers: ");
         for (Map.Entry<String, SetHostStatWithPort> dc : dcs.entrySet())
         {
-            System.out.print("\t" + dc.getKey());
+            out.print("\t" + dc.getKey());
 
             ArrayListMultimap<InetAddressAndPort, HostStatWithPort> hostToTokens = ArrayListMultimap.create();
             for (HostStatWithPort stat : dc.getValue())
@@ -120,27 +122,27 @@ public class DescribeCluster extends NodeToolCmd
                 if (unreachableNodes.contains(endpoint.toString()))
                     downNodes++;
             }
-            System.out.print(" #Nodes: " + totalNodes);
-            System.out.println(" #Down: " + downNodes);
+            out.print(" #Nodes: " + totalNodes);
+            out.println(" #Down: " + downNodes);
         }
 
         // display database version for each node
-        System.out.println("\nDatabase versions:");
+        out.println("\nDatabase versions:");
         Map<String, List<String>> databaseVersions = probe.getGossProxy().getReleaseVersionsWithPort();
         for (String version : databaseVersions.keySet())
         {
-            System.out.println(format("\t%s: %s%n", version, databaseVersions.get(version)));
+            out.println(format("\t%s: %s%n", version, databaseVersions.get(version)));
         }
 
-        System.out.println("Keyspaces:");
+        out.println("Keyspaces:");
         for (String keyspaceName : keyspaces)
         {
             String replicationInfo = probe.getKeyspaceReplicationInfo(keyspaceName);
             if (replicationInfo == null)
             {
-                System.out.println("something went wrong for keyspace: " + keyspaceName);
+                out.println("something went wrong for keyspace: " + keyspaceName);
             }
-            System.out.println("\t" + keyspaceName + " -> Replication class: " + replicationInfo);
+            out.println("\t" + keyspaceName + " -> Replication class: " + replicationInfo);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/DescribeRing.java b/src/java/org/apache/cassandra/tools/nodetool/DescribeRing.java
index ef8c97e..d90e6ba 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/DescribeRing.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/DescribeRing.java
@@ -22,6 +22,7 @@ import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 
 import java.io.IOException;
+import java.io.PrintStream;
 
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
@@ -35,13 +36,14 @@ public class DescribeRing extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Schema Version:" + probe.getSchemaVersion());
-        System.out.println("TokenRange: ");
+        PrintStream out = probe.output().out;
+        out.println("Schema Version:" + probe.getSchemaVersion());
+        out.println("TokenRange: ");
         try
         {
             for (String tokenRangeString : probe.describeRing(keyspace, printPort))
             {
-                System.out.println("\t" + tokenRangeString);
+                out.println("\t" + tokenRangeString);
             }
         } catch (IOException e)
         {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/FailureDetectorInfo.java b/src/java/org/apache/cassandra/tools/nodetool/FailureDetectorInfo.java
index c1b2192..6431e05 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/FailureDetectorInfo.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/FailureDetectorInfo.java
@@ -34,12 +34,12 @@ public class FailureDetectorInfo extends NodeToolCmd
     public void execute(NodeProbe probe)
     {
         TabularData data = probe.getFailureDetectorPhilValues(printPort);
-        System.out.printf("%10s,%16s%n", "Endpoint", "Phi");
+        probe.output().out.printf("%10s,%16s%n", "Endpoint", "Phi");
         for (Object o : data.keySet())
         {
             @SuppressWarnings({ "rawtypes", "unchecked" })
             CompositeData datum = data.get(((List) o).toArray(new Object[((List) o).size()]));
-            System.out.printf("%10s,%16.8f%n",datum.get("Endpoint"), datum.get("PHI"));
+            probe.output().out.printf("%10s,%16.8f%n", datum.get("Endpoint"), datum.get("PHI"));
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java b/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java
index 5b8cd6e..5d24d84 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java
@@ -56,7 +56,7 @@ public class GarbageCollect extends NodeToolCmd
         {
             try
             {
-                probe.garbageCollect(System.out, tombstoneOption, jobs, keyspace, tableNames);
+                probe.garbageCollect(probe.output().out, tombstoneOption, jobs, keyspace, tableNames);
             } catch (Exception e)
             {
                 throw new RuntimeException("Error occurred during garbage collection", e);
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java b/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
index 07ae6d9..6e36f25 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
@@ -31,7 +31,7 @@ public class GcStats extends NodeToolCmd
         double[] stats = probe.getAndResetGCStats();
         double mean = stats[2] / stats[5];
         double stdev = Math.sqrt((stats[3] / stats[5]) - (mean * mean));
-        System.out.printf("%20s%20s%20s%20s%20s%20s%25s%n", "Interval (ms)", "Max GC Elapsed (ms)", "Total GC Elapsed (ms)", "Stdev GC Elapsed (ms)", "GC Reclaimed (MB)", "Collections", "Direct Memory Bytes");
-        System.out.printf("%20.0f%20.0f%20.0f%20.0f%20.0f%20.0f%25d%n", stats[0], stats[1], stats[2], stdev, stats[4], stats[5], (long)stats[6]);
+        probe.output().out.printf("%20s%20s%20s%20s%20s%20s%25s%n", "Interval (ms)", "Max GC Elapsed (ms)", "Total GC Elapsed (ms)", "Stdev GC Elapsed (ms)", "GC Reclaimed (MB)", "Collections", "Direct Memory Bytes");
+        probe.output().out.printf("%20.0f%20.0f%20.0f%20.0f%20.0f%20.0f%25d%n", stats[0], stats[1], stats[2], stdev, stats[4], stats[5], (long)stats[6]);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetBatchlogReplayTrottle.java b/src/java/org/apache/cassandra/tools/nodetool/GetBatchlogReplayTrottle.java
index 661c495..6497aa1 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetBatchlogReplayTrottle.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetBatchlogReplayTrottle.java
@@ -28,6 +28,6 @@ public class GetBatchlogReplayTrottle extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Batchlog replay throttle: " + probe.getBatchlogReplayThrottle() + " KB/s");
+        probe.output().out.println("Batchlog replay throttle: " + probe.getBatchlogReplayThrottle() + " KB/s");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThreshold.java b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThreshold.java
index 589b1b3..c0ccbbf 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThreshold.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThreshold.java
@@ -42,8 +42,8 @@ public class GetCompactionThreshold extends NodeToolCmd
         String cf = args.get(1);
 
         ColumnFamilyStoreMBean cfsProxy = probe.getCfsProxy(ks, cf);
-        System.out.println("Current compaction thresholds for " + ks + "/" + cf + ": \n" +
-                " min = " + cfsProxy.getMinimumCompactionThreshold() + ", " +
-                " max = " + cfsProxy.getMaximumCompactionThreshold());
+        probe.output().out.println("Current compaction thresholds for " + ks + "/" + cf + ": \n" +
+                            " min = " + cfsProxy.getMinimumCompactionThreshold() + ", " +
+                            " max = " + cfsProxy.getMaximumCompactionThreshold());
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java
index a7df4d1..839c78d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetCompactionThroughput.java
@@ -28,6 +28,6 @@ public class GetCompactionThroughput extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Current compaction throughput: " + probe.getCompactionThroughput() + " MB/s");
+        probe.output().out.println("Current compaction throughput: " + probe.getCompactionThroughput() + " MB/s");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrency.java b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrency.java
index ede2908..ddf7b6a 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrency.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrency.java
@@ -37,14 +37,14 @@ public class GetConcurrency extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.printf("%-25s%16s%16s%n", "Stage", "CorePoolSize", "MaximumPoolSize");
+        probe.output().out.printf("%-25s%16s%16s%n", "Stage", "CorePoolSize", "MaximumPoolSize");
         probe.getMaximumPoolSizes(args).entrySet().stream()
              .sorted(Map.Entry.comparingByKey())
              .forEach(entry ->
-                System.out.printf("%-25s%16d%16d%n",
-                                  entry.getKey(),
-                                  entry.getValue().get(0),
-                                  entry.getValue().get(1)));
+                probe.output().out.printf("%-25s%16d%16d%n",
+                                   entry.getKey(),
+                                   entry.getValue().get(0),
+                                   entry.getValue().get(1)));
 
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java
index 6aa4d8b..3176c3e 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentCompactors.java
@@ -27,7 +27,7 @@ public class GetConcurrentCompactors extends NodeToolCmd
 {
     protected void execute(NodeProbe probe)
     {
-        System.out.println("Current concurrent compactors in the system is: \n" +
-                           probe.getConcurrentCompactors());
+        probe.output().out.println("Current concurrent compactors in the system is: \n" +
+                            probe.getConcurrentCompactors());
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentViewBuilders.java b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentViewBuilders.java
index c189fb0..0fe1830 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentViewBuilders.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetConcurrentViewBuilders.java
@@ -27,7 +27,7 @@ public class GetConcurrentViewBuilders extends NodeToolCmd
 {
     protected void execute(NodeProbe probe)
     {
-        System.out.println("Current number of concurrent view builders in the system is: \n" +
-                           probe.getConcurrentViewBuilders());
+        probe.output().out.println("Current number of concurrent view builders in the system is: \n" +
+                            probe.getConcurrentViewBuilders());
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java b/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java
index 78065c4..a834e28 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java
@@ -46,7 +46,7 @@ public class GetEndpoints extends NodeToolCmd
         {
             for (String endpoint : probe.getEndpointsWithPort(ks, table, key))
             {
-                System.out.println(endpoint);
+                probe.output().out.println(endpoint);
             }
         }
         else
@@ -54,7 +54,7 @@ public class GetEndpoints extends NodeToolCmd
             List<InetAddress> endpoints = probe.getEndpoints(ks, table, key);
             for (InetAddress endpoint : endpoints)
             {
-                System.out.println(endpoint.getHostAddress());
+                probe.output().out.println(endpoint.getHostAddress());
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetFullQueryLog.java b/src/java/org/apache/cassandra/tools/nodetool/GetFullQueryLog.java
index 47d8b8a..be3aa56 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetFullQueryLog.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetFullQueryLog.java
@@ -43,6 +43,6 @@ public class GetFullQueryLog extends NodeToolCmd
         tableBuilder.add("max_queue_weight", Integer.toString(options.max_queue_weight));
         tableBuilder.add("max_archive_retries", Long.toString(options.max_archive_retries));
 
-        tableBuilder.printTo(System.out);
+        tableBuilder.printTo(probe.output().out);
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetInterDCStreamThroughput.java b/src/java/org/apache/cassandra/tools/nodetool/GetInterDCStreamThroughput.java
index 039814e..554876d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetInterDCStreamThroughput.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetInterDCStreamThroughput.java
@@ -28,6 +28,6 @@ public class GetInterDCStreamThroughput extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Current inter-datacenter stream throughput: " + probe.getInterDCStreamThroughput() + " Mb/s");
+        probe.output().out.println("Current inter-datacenter stream throughput: " + probe.getInterDCStreamThroughput() + " Mb/s");
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetLoggingLevels.java b/src/java/org/apache/cassandra/tools/nodetool/GetLoggingLevels.java
index 90d6817..3cadceb 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetLoggingLevels.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetLoggingLevels.java
@@ -31,8 +31,8 @@ public class GetLoggingLevels extends NodeToolCmd
     public void execute(NodeProbe probe)
     {
         // what if some one set a very long logger name? 50 space may not be enough...
-        System.out.printf("%n%-50s%10s%n", "Logger Name", "Log Level");
+        probe.output().out.printf("%n%-50s%10s%n", "Logger Name", "Log Level");
         for (Map.Entry<String, String> entry : probe.getLoggingLevels().entrySet())
-            System.out.printf("%-50s%10s%n", entry.getKey(), entry.getValue());
+            probe.output().out.printf("%-50s%10s%n", entry.getKey(), entry.getValue());
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetMaxHintWindow.java b/src/java/org/apache/cassandra/tools/nodetool/GetMaxHintWindow.java
index 7bc1a30..54e73ab 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetMaxHintWindow.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetMaxHintWindow.java
@@ -28,6 +28,6 @@ public class GetMaxHintWindow extends NodeTool.NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Current max hint window: " + probe.getMaxHintWindow() + " ms");
+        probe.output().out.println("Current max hint window: " + probe.getMaxHintWindow() + " ms");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java b/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
index e43c2bf..f1e2117 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetSSTables.java
@@ -50,7 +50,7 @@ public class GetSSTables extends NodeToolCmd
         List<String> sstables = probe.getSSTables(ks, cf, key, hexFormat);
         for (String sstable : sstables)
         {
-            System.out.println(sstable);
+            probe.output().out.println(sstable);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetSeeds.java b/src/java/org/apache/cassandra/tools/nodetool/GetSeeds.java
index 207363c..faf5e8d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetSeeds.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetSeeds.java
@@ -33,12 +33,12 @@ public class GetSeeds extends NodeToolCmd
         List<String> seedList = probe.getSeeds();
         if (seedList.isEmpty())
         {
-            System.out.println("Seed node list does not contain any remote node IPs");
+            probe.output().out.println("Seed node list does not contain any remote node IPs");
         }
         else
         {
-            System.out.println("Current list of seed node IPs, excluding the current node's IP: " + String.join(" ", seedList));
+            probe.output().out.println("Current list of seed node IPs, excluding the current node's IP: " + String.join(" ", seedList));
         }
 
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetStreamThroughput.java b/src/java/org/apache/cassandra/tools/nodetool/GetStreamThroughput.java
index b76d14b..9014d3c 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetStreamThroughput.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetStreamThroughput.java
@@ -28,6 +28,6 @@ public class GetStreamThroughput extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Current stream throughput: " + probe.getStreamThroughput() + " Mb/s");
+        probe.output().out.println("Current stream throughput: " + probe.getStreamThroughput() + " Mb/s");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java b/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java
index 9f99ac6..60637d7 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetTimeout.java
@@ -42,7 +42,7 @@ public class GetTimeout extends NodeToolCmd
         checkArgument(args.size() == 1, "gettimeout requires a timeout type, one of (" + TIMEOUT_TYPES + ")");
         try
         {
-            System.out.println("Current timeout for type " + args.get(0) + ": " + probe.getTimeout(args.get(0)) + " ms");
+            probe.output().out.println("Current timeout for type " + args.get(0) + ": " + probe.getTimeout(args.get(0)) + " ms");
         } catch (Exception e)
         {
             throw new IllegalArgumentException(e.getMessage());
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetTraceProbability.java b/src/java/org/apache/cassandra/tools/nodetool/GetTraceProbability.java
index 374ab2c..007b813 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GetTraceProbability.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetTraceProbability.java
@@ -28,6 +28,6 @@ public class GetTraceProbability extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("Current trace probability: " + probe.getTraceProbability());
+        probe.output().out.println("Current trace probability: " + probe.getTraceProbability());
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GossipInfo.java b/src/java/org/apache/cassandra/tools/nodetool/GossipInfo.java
index 24c0634..4f5f1b3 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GossipInfo.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GossipInfo.java
@@ -28,6 +28,6 @@ public class GossipInfo extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println(probe.getGossipInfo(printPort));
+        probe.output().out.println(probe.getGossipInfo(printPort));
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Import.java b/src/java/org/apache/cassandra/tools/nodetool/Import.java
index 7315c3e..08fe35d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Import.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Import.java
@@ -23,6 +23,8 @@ import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 
 import io.airlift.airline.Option;
+
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -80,7 +82,7 @@ public class Import extends NodeToolCmd
 
         if (quick)
         {
-            System.out.println("Doing a quick import - skipping sstable verification and row cache invalidation");
+            probe.output().out.println("Doing a quick import - skipping sstable verification and row cache invalidation");
             noVerifyTokens = true;
             noInvalidateCaches = true;
             noVerify = true;
@@ -90,10 +92,11 @@ public class Import extends NodeToolCmd
         List<String> failedDirs = probe.importNewSSTables(args.get(0), args.get(1), new HashSet<>(srcPaths), !keepLevel, !keepRepaired, !noVerify, !noVerifyTokens, !noInvalidateCaches, extendedVerify);
         if (!failedDirs.isEmpty())
         {
-            System.err.println("Some directories failed to import, check server logs for details:");
+            PrintStream err = probe.output().err;
+            err.println("Some directories failed to import, check server logs for details:");
             for (String directory : failedDirs)
-                System.err.println(directory);
+                err.println(directory);
             System.exit(1);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java
index 6ff5037..1ee6bac 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Info.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.tools.nodetool;
 import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
+import java.io.PrintStream;
 import java.lang.management.MemoryUsage;
 import java.util.Iterator;
 import java.util.List;
@@ -45,27 +46,28 @@ public class Info extends NodeToolCmd
     {
         boolean gossipInitialized = probe.isGossipRunning();
 
-        System.out.printf("%-23s: %s%n", "ID", probe.getLocalHostId());
-        System.out.printf("%-23s: %s%n", "Gossip active", gossipInitialized);
-        System.out.printf("%-23s: %s%n", "Native Transport active", probe.isNativeTransportRunning());
-        System.out.printf("%-23s: %s%n", "Load", probe.getLoadString());
+        PrintStream out = probe.output().out;
+        out.printf("%-23s: %s%n", "ID", probe.getLocalHostId());
+        out.printf("%-23s: %s%n", "Gossip active", gossipInitialized);
+        out.printf("%-23s: %s%n", "Native Transport active", probe.isNativeTransportRunning());
+        out.printf("%-23s: %s%n", "Load", probe.getLoadString());
         if (gossipInitialized)
-            System.out.printf("%-23s: %s%n", "Generation No", probe.getCurrentGenerationNumber());
+            out.printf("%-23s: %s%n", "Generation No", probe.getCurrentGenerationNumber());
         else
-            System.out.printf("%-23s: %s%n", "Generation No", 0);
+            out.printf("%-23s: %s%n", "Generation No", 0);
 
         // Uptime
         long secondsUp = probe.getUptime() / 1000;
-        System.out.printf("%-23s: %d%n", "Uptime (seconds)", secondsUp);
+        out.printf("%-23s: %d%n", "Uptime (seconds)", secondsUp);
 
         // Memory usage
         MemoryUsage heapUsage = probe.getHeapMemoryUsage();
         double memUsed = (double) heapUsage.getUsed() / (1024 * 1024);
         double memMax = (double) heapUsage.getMax() / (1024 * 1024);
-        System.out.printf("%-23s: %.2f / %.2f%n", "Heap Memory (MB)", memUsed, memMax);
+        out.printf("%-23s: %.2f / %.2f%n", "Heap Memory (MB)", memUsed, memMax);
         try
         {
-            System.out.printf("%-23s: %.2f%n", "Off Heap Memory (MB)", getOffHeapMemoryUsed(probe));
+            out.printf("%-23s: %.2f%n", "Off Heap Memory (MB)", getOffHeapMemoryUsed(probe));
         }
         catch (RuntimeException e)
         {
@@ -75,16 +77,16 @@ public class Info extends NodeToolCmd
         }
 
         // Data Center/Rack
-        System.out.printf("%-23s: %s%n", "Data Center", probe.getDataCenter());
-        System.out.printf("%-23s: %s%n", "Rack", probe.getRack());
+        out.printf("%-23s: %s%n", "Data Center", probe.getDataCenter());
+        out.printf("%-23s: %s%n", "Rack", probe.getRack());
 
         // Exceptions
-        System.out.printf("%-23s: %s%n", "Exceptions", probe.getStorageMetric("Exceptions"));
+        out.printf("%-23s: %s%n", "Exceptions", probe.getStorageMetric("Exceptions"));
 
         CacheServiceMBean cacheService = probe.getCacheServiceMBean();
 
         // Key Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds
-        System.out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
+        out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
                 "Key Cache",
                 probe.getCacheMetric("KeyCache", "Entries"),
                 FileUtils.stringifyFileSize((long) probe.getCacheMetric("KeyCache", "Size")),
@@ -95,7 +97,7 @@ public class Info extends NodeToolCmd
                 cacheService.getKeyCacheSavePeriodInSeconds());
 
         // Row Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds
-        System.out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
+        out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
                 "Row Cache",
                 probe.getCacheMetric("RowCache", "Entries"),
                 FileUtils.stringifyFileSize((long) probe.getCacheMetric("RowCache", "Size")),
@@ -106,7 +108,7 @@ public class Info extends NodeToolCmd
                 cacheService.getRowCacheSavePeriodInSeconds());
 
         // Counter Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds
-        System.out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
+        out.printf("%-23s: entries %d, size %s, capacity %s, %d hits, %d requests, %.3f recent hit rate, %d save period in seconds%n",
                 "Counter Cache",
                 probe.getCacheMetric("CounterCache", "Entries"),
                 FileUtils.stringifyFileSize((long) probe.getCacheMetric("CounterCache", "Size")),
@@ -119,7 +121,7 @@ public class Info extends NodeToolCmd
         // Chunk Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds
         try
         {
-            System.out.printf("%-23s: entries %d, size %s, capacity %s, %d misses, %d requests, %.3f recent hit rate, %.3f %s miss latency%n",
+            out.printf("%-23s: entries %d, size %s, capacity %s, %d misses, %d requests, %.3f recent hit rate, %.3f %s miss latency%n",
                     "Chunk Cache",
                     probe.getCacheMetric("ChunkCache", "Entries"),
                     FileUtils.stringifyFileSize((long) probe.getCacheMetric("ChunkCache", "Size")),
@@ -139,7 +141,7 @@ public class Info extends NodeToolCmd
         }
 
         // Global table stats
-        System.out.printf("%-23s: %s%%%n", "Percent Repaired", probe.getColumnFamilyMetric(null, null, "PercentRepaired"));
+        out.printf("%-23s: %s%%%n", "Percent Repaired", probe.getColumnFamilyMetric(null, null, "PercentRepaired"));
 
         // check if node is already joined, before getting tokens, since it throws exception if not.
         if (probe.isJoined())
@@ -148,14 +150,14 @@ public class Info extends NodeToolCmd
             List<String> tokens = probe.getTokens();
             if (tokens.size() == 1 || this.tokens)
                 for (String token : tokens)
-                    System.out.printf("%-23s: %s%n", "Token", token);
+                    out.printf("%-23s: %s%n", "Token", token);
             else
-                System.out.printf("%-23s: (invoke with -T/--tokens to see all %d tokens)%n", "Token",
+                out.printf("%-23s: (invoke with -T/--tokens to see all %d tokens)%n", "Token",
                                   tokens.size());
         }
         else
         {
-            System.out.printf("%-23s: (node is not joined to the cluster)%n", "Token");
+            out.printf("%-23s: (node is not joined to the cluster)%n", "Token");
         }
     }
 
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java b/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
index 9bc62d1..79494cd 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ListSnapshots.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -35,14 +36,15 @@ public class ListSnapshots extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         try
         {
-            System.out.println("Snapshot Details: ");
+            out.println("Snapshot Details: ");
 
             final Map<String,TabularData> snapshotDetails = probe.getSnapshotDetails();
             if (snapshotDetails.isEmpty())
             {
-                System.out.println("There are no snapshots");
+                out.println("There are no snapshots");
                 return;
             }
 
@@ -61,9 +63,9 @@ public class ListSnapshots extends NodeToolCmd
                     table.add(value.toArray(new String[value.size()]));
                 }
             }
-            table.printTo(System.out);
+            table.printTo(out);
 
-            System.out.println("\nTotal TrueDiskSpaceUsed: " + FileUtils.stringifyFileSize(trueSnapshotsSize) + "\n");
+            out.println("\nTotal TrueDiskSpaceUsed: " + FileUtils.stringifyFileSize(trueSnapshotsSize) + "\n");
         }
         catch (Exception e)
         {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/NetStats.java b/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
index e86505b..45af43d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/NetStats.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.tools.nodetool;
 import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
+import java.io.PrintStream;
 import java.util.Set;
 
 import org.apache.cassandra.io.util.FileUtils;
@@ -41,22 +42,23 @@ public class NetStats extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.printf("Mode: %s%n", probe.getOperationMode());
+        PrintStream out = probe.output().out;
+        out.printf("Mode: %s%n", probe.getOperationMode());
         Set<StreamState> statuses = probe.getStreamStatus();
         if (statuses.isEmpty())
-            System.out.println("Not sending any streams.");
+            out.println("Not sending any streams.");
         for (StreamState status : statuses)
         {
-            System.out.printf("%s %s%n", status.streamOperation.getDescription(), status.planId.toString());
+            out.printf("%s %s%n", status.streamOperation.getDescription(), status.planId.toString());
             for (SessionInfo info : status.sessions)
             {
-                System.out.printf("    %s", info.peer.toString(printPort));
+                out.printf("    %s", info.peer.toString(printPort));
                 // print private IP when it is used
                 if (!info.peer.equals(info.connecting))
                 {
-                    System.out.printf(" (using %s)", info.connecting.toString(printPort));
+                    out.printf(" (using %s)", info.connecting.toString(printPort));
                 }
-                System.out.printf("%n");
+                out.printf("%n");
                 if (!info.receivingSummaries.isEmpty())
                 {
                     long totalFilesToReceive = info.getTotalFilesToReceive();
@@ -67,24 +69,24 @@ public class NetStats extends NodeToolCmd
                     double percentageSizesReceived = ((double) totalSizeReceived / totalBytesToReceive) * 100;
 
                     if (humanReadable)
-                        System.out.printf("        Receiving %d files, %s total. Already received %d files (%.2f%%), %s total (%.2f%%)%n",
-                                          totalFilesToReceive,
-                                          FileUtils.stringifyFileSize(totalBytesToReceive),
-                                          totalFilesReceived,
-                                          percentageFilesReceived,
-                                          FileUtils.stringifyFileSize(totalSizeReceived),
-                                          percentageSizesReceived);
+                        out.printf("        Receiving %d files, %s total. Already received %d files (%.2f%%), %s total (%.2f%%)%n",
+                                   totalFilesToReceive,
+                                   FileUtils.stringifyFileSize(totalBytesToReceive),
+                                   totalFilesReceived,
+                                   percentageFilesReceived,
+                                   FileUtils.stringifyFileSize(totalSizeReceived),
+                                   percentageSizesReceived);
                     else
-                        System.out.printf("        Receiving %d files, %d bytes total. Already received %d files (%.2f%%), %d bytes total (%.2f%%)%n",
-                                          totalFilesToReceive,
-                                          totalBytesToReceive,
-                                          totalFilesReceived,
-                                          percentageFilesReceived,
-                                          totalSizeReceived,
-                                          percentageSizesReceived);
+                        out.printf("        Receiving %d files, %d bytes total. Already received %d files (%.2f%%), %d bytes total (%.2f%%)%n",
+                                   totalFilesToReceive,
+                                   totalBytesToReceive,
+                                   totalFilesReceived,
+                                   percentageFilesReceived,
+                                   totalSizeReceived,
+                                   percentageSizesReceived);
                     for (ProgressInfo progress : info.getReceivingFiles())
                     {
-                        System.out.printf("            %s%n", progress.toString(printPort));
+                        out.printf("            %s%n", progress.toString(printPort));
                     }
                 }
                 if (!info.sendingSummaries.isEmpty())
@@ -97,24 +99,24 @@ public class NetStats extends NodeToolCmd
                     double percentageSizeSent = ((double) totalSizeSent / totalSizeToSend) * 100;
 
                     if (humanReadable)
-                        System.out.printf("        Sending %d files, %s total. Already sent %d files (%.2f%%), %s total (%.2f%%)%n",
-                                          totalFilesToSend,
-                                          FileUtils.stringifyFileSize(totalSizeToSend),
-                                          totalFilesSent,
-                                          percentageFilesSent,
-                                          FileUtils.stringifyFileSize(totalSizeSent),
-                                          percentageSizeSent);
+                        out.printf("        Sending %d files, %s total. Already sent %d files (%.2f%%), %s total (%.2f%%)%n",
+                                   totalFilesToSend,
+                                   FileUtils.stringifyFileSize(totalSizeToSend),
+                                   totalFilesSent,
+                                   percentageFilesSent,
+                                   FileUtils.stringifyFileSize(totalSizeSent),
+                                   percentageSizeSent);
                     else
-                        System.out.printf("        Sending %d files, %d bytes total. Already sent %d files (%.2f%%), %d bytes total (%.2f%%) %n",
-                                          totalFilesToSend,
-                                          totalSizeToSend,
-                                          totalFilesSent,
-                                          percentageFilesSent,
-                                          totalSizeSent,
-                                          percentageSizeSent);
+                        out.printf("        Sending %d files, %d bytes total. Already sent %d files (%.2f%%), %d bytes total (%.2f%%) %n",
+                                   totalFilesToSend,
+                                   totalSizeToSend,
+                                   totalFilesSent,
+                                   percentageFilesSent,
+                                   totalSizeSent,
+                                   percentageSizeSent);
                     for (ProgressInfo progress : info.getSendingFiles())
                     {
-                        System.out.printf("            %s%n", progress.toString(printPort));
+                        out.printf("            %s%n", progress.toString(printPort));
                     }
                 }
             }
@@ -122,14 +124,14 @@ public class NetStats extends NodeToolCmd
 
         if (!probe.isStarting())
         {
-            System.out.printf("Read Repair Statistics:%nAttempted: %d%nMismatch (Blocking): %d%nMismatch (Background): %d%n", probe.getReadRepairAttempted(), probe.getReadRepairRepairedBlocking(), probe.getReadRepairRepairedBackground());
+            out.printf("Read Repair Statistics:%nAttempted: %d%nMismatch (Blocking): %d%nMismatch (Background): %d%n", probe.getReadRepairAttempted(), probe.getReadRepairRepairedBlocking(), probe.getReadRepairRepairedBackground());
 
             MessagingServiceMBean ms = probe.getMessagingServiceProxy();
-            System.out.printf("%-25s", "Pool Name");
-            System.out.printf("%10s", "Active");
-            System.out.printf("%10s", "Pending");
-            System.out.printf("%15s", "Completed");
-            System.out.printf("%10s%n", "Dropped");
+            out.printf("%-25s", "Pool Name");
+            out.printf("%10s", "Active");
+            out.printf("%10s", "Pending");
+            out.printf("%15s", "Completed");
+            out.printf("%10s%n", "Dropped");
 
             int pending;
             long completed;
@@ -144,7 +146,7 @@ public class NetStats extends NodeToolCmd
             dropped = 0;
             for (long n : ms.getLargeMessageDroppedTasksWithPort().values())
                 dropped += n;
-            System.out.printf("%-25s%10s%10s%15s%10s%n", "Large messages", "n/a", pending, completed, dropped);
+            out.printf("%-25s%10s%10s%15s%10s%n", "Large messages", "n/a", pending, completed, dropped);
 
             pending = 0;
             for (int n : ms.getSmallMessagePendingTasksWithPort().values())
@@ -155,7 +157,7 @@ public class NetStats extends NodeToolCmd
             dropped = 0;
             for (long n : ms.getSmallMessageDroppedTasksWithPort().values())
                 dropped += n;
-            System.out.printf("%-25s%10s%10s%15s%10s%n", "Small messages", "n/a", pending, completed, dropped);
+            out.printf("%-25s%10s%10s%15s%10s%n", "Small messages", "n/a", pending, completed, dropped);
 
             pending = 0;
             for (int n : ms.getGossipMessagePendingTasksWithPort().values())
@@ -166,7 +168,7 @@ public class NetStats extends NodeToolCmd
             dropped = 0;
             for (long n : ms.getGossipMessageDroppedTasksWithPort().values())
                 dropped += n;
-            System.out.printf("%-25s%10s%10s%15s%10s%n", "Gossip messages", "n/a", pending, completed, dropped);
+            out.printf("%-25s%10s%10s%15s%10s%n", "Gossip messages", "n/a", pending, completed, dropped);
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ProfileLoad.java b/src/java/org/apache/cassandra/tools/nodetool/ProfileLoad.java
index 7037969..487f14a 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ProfileLoad.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ProfileLoad.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.tools.nodetool;
 import static com.google.common.base.Preconditions.checkArgument;
 import static org.apache.commons.lang3.StringUtils.join;
 
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -108,19 +109,19 @@ public class ProfileLoad extends NodeToolCmd
             .addColumn("Partition", "value")
             .addColumn("Count", "count")
             .addColumn("+/-", "error")
-            .print();
+            .print(probe.output().out);
         }
 
         rb.forType(SamplerType.WRITE_SIZE, "Max mutation size by partition")
             .addColumn("Table", "table")
             .addColumn("Partition", "value")
             .addColumn("Bytes", "count")
-            .print();
+            .print(probe.output().out);
 
         rb.forType(SamplerType.LOCAL_READ_TIME, "Longest read query times")
             .addColumn("Query", "value")
             .addColumn("Microseconds", "count")
-            .print();
+            .print(probe.output().out);
     }
 
     private class ResultBuilder
@@ -163,14 +164,14 @@ public class ProfileLoad extends NodeToolCmd
             return key;
         }
 
-        public void print()
+        public void print(PrintStream outStream)
         {
             if (targets.contains(type.toString()))
             {
                 if (!first.get())
-                    System.out.println();
+                    outStream.println();
                 first.set(false);
-                System.out.println(description + ':');
+                outStream.println(description + ':');
                 TableBuilder out = new TableBuilder();
                 out.add(dataKeys.stream().map(p -> p.left).collect(Collectors.toList()).toArray(new String[] {}));
                 List<CompositeData> topk = results.get(type.toString());
@@ -180,11 +181,11 @@ public class ProfileLoad extends NodeToolCmd
                 }
                 if (topk.size() == 0)
                 {
-                    System.out.println("   Nothing recorded during sampling period...");
+                    outStream.println("   Nothing recorded during sampling period...");
                 }
                 else
                 {
-                    out.printTo(System.out);
+                    out.printTo(outStream);
                 }
             }
         }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java b/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
index 1bf31d5..c736a67 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ProxyHistograms.java
@@ -17,6 +17,8 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
+
 import static java.lang.String.format;
 import io.airlift.airline.Command;
 
@@ -29,6 +31,7 @@ public class ProxyHistograms extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         String[] percentiles = {"50%", "75%", "95%", "98%", "99%", "Min", "Max"};
         Double[] readLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("Read"));
         Double[] writeLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("Write"));
@@ -37,14 +40,14 @@ public class ProxyHistograms extends NodeToolCmd
         Double[] casWriteLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("CASWrite"));
         Double[] viewWriteLatency = probe.metricPercentilesAsArray(probe.getProxyMetric("ViewWrite"));
 
-        System.out.println("proxy histograms");
-        System.out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
+        out.println("proxy histograms");
+        out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
                 "Percentile", "Read Latency", "Write Latency", "Range Latency", "CAS Read Latency", "CAS Write Latency", "View Write Latency"));
-        System.out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
+        out.println(format("%-10s%19s%19s%19s%19s%19s%19s",
                 "", "(micros)", "(micros)", "(micros)", "(micros)", "(micros)", "(micros)"));
         for (int i = 0; i < percentiles.length; i++)
         {
-            System.out.println(format("%-10s%19.2f%19.2f%19.2f%19.2f%19.2f%19.2f",
+            out.println(format("%-10s%19.2f%19.2f%19.2f%19.2f%19.2f%19.2f",
                     percentiles[i],
                     readLatency[i],
                     writeLatency[i],
@@ -53,6 +56,6 @@ public class ProxyHistograms extends NodeToolCmd
                     casWriteLatency[i],
                     viewWriteLatency[i]));
         }
-        System.out.println();
+        out.println();
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/RangeKeySample.java b/src/java/org/apache/cassandra/tools/nodetool/RangeKeySample.java
index 1ca2aa9..fb44d2c 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/RangeKeySample.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/RangeKeySample.java
@@ -30,11 +30,11 @@ public class RangeKeySample extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("RangeKeySample: ");
+        probe.output().out.println("RangeKeySample: ");
         List<String> tokenStrings = probe.sampleKeyRange();
         for (String tokenString : tokenStrings)
         {
-            System.out.println("\t" + tokenString);
+            probe.output().out.println("\t" + tokenString);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Refresh.java b/src/java/org/apache/cassandra/tools/nodetool/Refresh.java
index 3c8d4c9..b32e7b6 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Refresh.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Refresh.java
@@ -36,8 +36,8 @@ public class Refresh extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("nodetool refresh is deprecated, use nodetool import instead");
+        probe.output().out.println("nodetool refresh is deprecated, use nodetool import instead");
         checkArgument(args.size() == 2, "refresh requires ks and cf args");
         probe.loadNewSSTables(args.get(0), args.get(1));
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ReloadSeeds.java b/src/java/org/apache/cassandra/tools/nodetool/ReloadSeeds.java
index b9682cf..e054abd 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ReloadSeeds.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ReloadSeeds.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.util.List;
 
 import io.airlift.airline.Command;
@@ -30,18 +31,19 @@ public class ReloadSeeds extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         List<String> seedList = probe.reloadSeeds();
         if (seedList == null)
         {
-            System.out.println("Failed to reload the seed node list.");
+            out.println("Failed to reload the seed node list.");
         }
         else if (seedList.isEmpty())
         {
-            System.out.println("Seed node list does not contain any remote node IPs");
+            out.println("Seed node list does not contain any remote node IPs");
         }
         else
         {
-            System.out.println("Updated seed node IP list, excluding the current node's IP: " + String.join(" ", seedList));
+            out.println("Updated seed node IP list, excluding the current node's IP: " + String.join(" ", seedList));
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java b/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java
index 0acb313..e26cce3 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/RemoveNode.java
@@ -36,10 +36,10 @@ public class RemoveNode extends NodeToolCmd
         switch (removeOperation)
         {
             case "status":
-                System.out.println("RemovalStatus: " + probe.getRemovalStatus(printPort));
+                probe.output().out.println("RemovalStatus: " + probe.getRemovalStatus(printPort));
                 break;
             case "force":
-                System.out.println("RemovalStatus: " + probe.getRemovalStatus(printPort));
+                probe.output().out.println("RemovalStatus: " + probe.getRemovalStatus(printPort));
                 probe.forceRemoveCompletion();
                 break;
             default:
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Repair.java b/src/java/org/apache/cassandra/tools/nodetool/Repair.java
index 0f9bfb3..c4f60af 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Repair.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Repair.java
@@ -168,7 +168,7 @@ public class Repair extends NodeToolCmd
             options.put(RepairOption.HOSTS_KEY, StringUtils.join(specificHosts, ","));
             try
             {
-                probe.repairAsync(System.out, keyspace, options);
+                probe.repairAsync(probe.output().out, keyspace, options);
             } catch (Exception e)
             {
                 throw new RuntimeException("Error occurred during repair", e);
diff --git a/src/java/org/apache/cassandra/tools/nodetool/RepairAdmin.java b/src/java/org/apache/cassandra/tools/nodetool/RepairAdmin.java
index b66c32a..2226bf4 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/RepairAdmin.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/RepairAdmin.java
@@ -18,17 +18,15 @@
 
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-
 import javax.management.openmbean.CompositeData;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-
 import org.apache.commons.lang3.StringUtils;
 
 import io.airlift.airline.Arguments;
@@ -36,10 +34,8 @@ import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 import org.apache.cassandra.repair.consistent.LocalSessionInfo;
 import org.apache.cassandra.repair.consistent.admin.CleanupSummary;
-import org.apache.cassandra.repair.consistent.admin.PendingStat;
 import org.apache.cassandra.repair.consistent.admin.PendingStats;
 import org.apache.cassandra.repair.consistent.admin.RepairStats;
-import org.apache.cassandra.service.ActiveRepairServiceMBean;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool;
 import org.apache.cassandra.utils.FBUtilities;
@@ -63,11 +59,11 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
 
         protected void execute(NodeProbe probe)
         {
+            PrintStream out = probe.output().out;
             List<Map<String, String>> sessions = probe.getRepairServiceProxy().getSessions(all, getRangeString(startToken, endToken));
             if (sessions.isEmpty())
             {
-                System.out.println("no sessions");
-
+                out.println("no sessions");
             }
             else
             {
@@ -91,7 +87,7 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
                     rows.add(values);
                 }
 
-                printTable(rows);
+                printTable(rows, out);
             }
         }
     }
@@ -150,7 +146,7 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
                 rows.add(row);
             }
 
-            printTable(rows);
+            printTable(rows, probe.output().out);
         }
     }
 
@@ -172,11 +168,12 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
 
         protected void execute(NodeProbe probe)
         {
+            PrintStream out = probe.output().out;
             List<CompositeData> compositeData = probe.getRepairServiceProxy().getRepairStats(schemaArgs, getRangeString(startToken, endToken));
 
             if (compositeData.isEmpty())
             {
-                System.out.println("no stats");
+                out.println("no stats");
                 return;
             }
 
@@ -211,7 +208,7 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
                 rows.add(row);
             }
 
-            printTable(rows);
+            printTable(rows, out);
         }
     }
 
@@ -235,7 +232,8 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
 
         protected void execute(NodeProbe probe)
         {
-            System.out.println("Cleaning up data from completed sessions...");
+            PrintStream out = probe.output().out;
+            out.println("Cleaning up data from completed sessions...");
             List<CompositeData> compositeData = probe.getRepairServiceProxy().cleanupPending(schemaArgs, getRangeString(startToken, endToken), force);
 
             List<CleanupSummary> summaries = new ArrayList<>(compositeData.size());
@@ -266,9 +264,9 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
             }
 
             if (hasFailures)
-                System.out.println("Some tables couldn't be cleaned up completely");
+                out.println("Some tables couldn't be cleaned up completely");
 
-            printTable(rows);
+            printTable(rows, out);
         }
     }
 
@@ -289,7 +287,7 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
         }
     }
 
-    private static void printTable(List<List<String>> rows)
+    private static void printTable(List<List<String>> rows, PrintStream out)
     {
         if (rows.isEmpty())
             return;
@@ -319,7 +317,7 @@ public abstract class RepairAdmin extends NodeTool.NodeToolCmd
             {
                 formatted.add(String.format(fmts.get(i), row.get(i)));
             }
-            System.out.println(Joiner.on(" | ").join(formatted));
+            out.println(Joiner.on(" | ").join(formatted));
         }
     }
 
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Ring.java b/src/java/org/apache/cassandra/tools/nodetool/Ring.java
index 20cb890..8b23717 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Ring.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Ring.java
@@ -22,6 +22,7 @@ import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
+import java.io.PrintStream;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.text.DecimalFormat;
@@ -51,6 +52,7 @@ public class Ring extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         try
         {
             Map<String, String> tokensToEndpoints = probe.getTokenToEndpointMap(printPort);
@@ -93,22 +95,22 @@ public class Ring extends NodeToolCmd
                 }
                 catch (IllegalArgumentException ex)
                 {
-                    System.out.printf("%nError: %s%n", ex.getMessage());
+                    out.printf("%nError: %s%n", ex.getMessage());
                     return;
                 }
 
 
-                System.out.println();
+                out.println();
                 for (Entry<String, SetHostStatWithPort> entry : NodeTool.getOwnershipByDcWithPort(probe, resolveIp, tokensToEndpoints, ownerships).entrySet())
                     printDc(probe, format, entry.getKey(), endpointsToTokens, entry.getValue(), showEffectiveOwnership);
 
                 if (haveVnodes)
                 {
-                    System.out.println("  Warning: \"nodetool ring\" is used to output all the tokens of a node.");
-                    System.out.println("  To view status related info of a node use \"nodetool status\" instead.\n");
+                    out.println("  Warning: \"nodetool ring\" is used to output all the tokens of a node.");
+                    out.println("  To view status related info of a node use \"nodetool status\" instead.\n");
                 }
 
-                System.out.printf("%n  " + errors.toString());
+                out.printf("%n  " + errors.toString());
             }
             else
             {
@@ -126,22 +128,22 @@ public class Ring extends NodeToolCmd
                 }
                 catch (IllegalArgumentException ex)
                 {
-                    System.out.printf("%nError: %s%n", ex.getMessage());
+                    out.printf("%nError: %s%n", ex.getMessage());
                     return;
                 }
 
 
-                System.out.println();
+                out.println();
                 for (Entry<String, SetHostStat> entry : NodeTool.getOwnershipByDc(probe, resolveIp, tokensToEndpoints, ownerships).entrySet())
                     printDc(probe, format, entry.getKey(), endpointsToTokens, entry.getValue(), showEffectiveOwnership);
 
                 if (haveVnodes)
                 {
-                    System.out.println("  Warning: \"nodetool ring\" is used to output all the tokens of a node.");
-                    System.out.println("  To view status related info of a node use \"nodetool status\" instead.\n");
+                    out.println("  Warning: \"nodetool ring\" is used to output all the tokens of a node.");
+                    out.println("  To view status related info of a node use \"nodetool status\" instead.\n");
                 }
 
-                System.out.printf("%n  " + errors.toString());
+                out.printf("%n  " + errors.toString());
             }
         } catch (Exception e)
         {
@@ -155,6 +157,7 @@ public class Ring extends NodeToolCmd
                          LinkedHashMultimap<String, String> endpointsToTokens,
                          SetHostStat hoststats,boolean showEffectiveOwnership)
     {
+        PrintStream out = probe.output().out;
         Collection<String> liveNodes = probe.getLiveNodes(false);
         Collection<String> deadNodes = probe.getUnreachableNodes(false);
         Collection<String> joiningNodes = probe.getJoiningNodes(false);
@@ -162,8 +165,8 @@ public class Ring extends NodeToolCmd
         Collection<String> movingNodes = probe.getMovingNodes(false);
         Map<String, String> loadMap = probe.getLoadMap(false);
 
-        System.out.println("Datacenter: " + dc);
-        System.out.println("==========");
+        out.println("Datacenter: " + dc);
+        out.println("==========");
 
         // get the total amount of replicas for this dc and the last token in this dc's ring
         List<String> tokens = new ArrayList<>();
@@ -175,12 +178,12 @@ public class Ring extends NodeToolCmd
             lastToken = tokens.get(tokens.size() - 1);
         }
 
-        System.out.printf(format, "Address", "Rack", "Status", "State", "Load", "Owns", "Token");
+        out.printf(format, "Address", "Rack", "Status", "State", "Load", "Owns", "Token");
 
         if (hoststats.size() > 1)
-            System.out.printf(format, "", "", "", "", "", "", lastToken);
+            out.printf(format, "", "", "", "", "", "", lastToken);
         else
-            System.out.println();
+            out.println();
 
         for (HostStat stat : hoststats)
         {
@@ -214,9 +217,9 @@ public class Ring extends NodeToolCmd
                     ? loadMap.get(endpoint)
                     : "?";
             String owns = stat.owns != null && showEffectiveOwnership? new DecimalFormat("##0.00%").format(stat.owns) : "?";
-            System.out.printf(format, stat.ipOrDns(), rack, status, state, load, owns, stat.token);
+            out.printf(format, stat.ipOrDns(), rack, status, state, load, owns, stat.token);
         }
-        System.out.println();
+        out.println();
     }
 
     private void printDc(NodeProbe probe, String format,
@@ -224,6 +227,7 @@ public class Ring extends NodeToolCmd
                          LinkedHashMultimap<String, String> endpointsToTokens,
                          SetHostStatWithPort hoststats,boolean showEffectiveOwnership)
     {
+        PrintStream out = probe.output().out;
         Collection<String> liveNodes = probe.getLiveNodes(true);
         Collection<String> deadNodes = probe.getUnreachableNodes(true);
         Collection<String> joiningNodes = probe.getJoiningNodes(true);
@@ -231,8 +235,8 @@ public class Ring extends NodeToolCmd
         Collection<String> movingNodes = probe.getMovingNodes(true);
         Map<String, String> loadMap = probe.getLoadMap(true);
 
-        System.out.println("Datacenter: " + dc);
-        System.out.println("==========");
+        out.println("Datacenter: " + dc);
+        out.println("==========");
 
         // get the total amount of replicas for this dc and the last token in this dc's ring
         List<String> tokens = new ArrayList<>();
@@ -244,12 +248,12 @@ public class Ring extends NodeToolCmd
             lastToken = tokens.get(tokens.size() - 1);
         }
 
-        System.out.printf(format, "Address", "Rack", "Status", "State", "Load", "Owns", "Token");
+        out.printf(format, "Address", "Rack", "Status", "State", "Load", "Owns", "Token");
 
         if (hoststats.size() > 1)
-            System.out.printf(format, "", "", "", "", "", "", lastToken);
+            out.printf(format, "", "", "", "", "", "", lastToken);
         else
-            System.out.println();
+            out.println();
 
         for (HostStatWithPort stat : hoststats)
         {
@@ -283,8 +287,8 @@ public class Ring extends NodeToolCmd
                           ? loadMap.get(endpoint)
                           : "?";
             String owns = stat.owns != null && showEffectiveOwnership? new DecimalFormat("##0.00%").format(stat.owns) : "?";
-            System.out.printf(format, stat.ipOrDns(), rack, status, state, load, owns, stat.token);
+            out.printf(format, stat.ipOrDns(), rack, status, state, load, owns, stat.token);
         }
-        System.out.println();
+        out.println();
     }
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Scrub.java b/src/java/org/apache/cassandra/tools/nodetool/Scrub.java
index 7c5ff00..049d1d0 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Scrub.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Scrub.java
@@ -69,7 +69,7 @@ public class Scrub extends NodeToolCmd
         {
             try
             {
-                probe.scrub(System.out, disableSnapshot, skipCorrupted, !noValidation, reinsertOverflowedTTL, jobs, keyspace, tableNames);
+                probe.scrub(probe.output().out, disableSnapshot, skipCorrupted, !noValidation, reinsertOverflowedTTL, jobs, keyspace, tableNames);
             }
             catch (IllegalArgumentException e)
             {
diff --git a/src/java/org/apache/cassandra/tools/nodetool/SetConcurrency.java b/src/java/org/apache/cassandra/tools/nodetool/SetConcurrency.java
index 3ce94e1..6705c92 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/SetConcurrency.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/SetConcurrency.java
@@ -54,7 +54,7 @@ public class SetConcurrency extends NodeToolCmd
         catch (IllegalArgumentException e)
         {
             String message = e.getMessage() != null ? e.getMessage() : "invalid pool size";
-            System.out.println("Unable to set concurrency: " + message);
+            probe.output().out.println("Unable to set concurrency: " + message);
             System.exit(1);
         }
     }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java
index f394d38..3ad2c94 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java
@@ -1,8 +1,25 @@
-
+/*
+ * 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.tools.nodetool;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.lang.reflect.Field;
 import java.net.URL;
 import java.net.URLDecoder;
@@ -40,6 +57,7 @@ import com.beust.jcommander.ParameterDescription;
 import com.beust.jcommander.Parameterized;
 import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
+import org.apache.cassandra.tools.Output;
 import org.gridkit.jvmtool.JmxConnectionInfo;
 import org.gridkit.jvmtool.cli.CommandLauncher;
 
@@ -54,32 +72,33 @@ public class Sjk extends NodeToolCmd
 
     private final Wrapper wrapper = new Wrapper();
 
-    public void run()
+    @Override
+    public void runInternal()
     {
-        wrapper.prepare(args != null ? args.toArray(new String[0]) : new String[]{"help"});
+        wrapper.prepare(args != null ? args.toArray(new String[0]) : new String[]{"help"}, output.out, output.err);
 
         if (!wrapper.requiresMbeanServerConn())
         {
             // SJK command does not require an MBeanServerConnection, so just invoke it
-            wrapper.run(null);
+            wrapper.run(null, output);
         }
         else
         {
             // invoke common nodetool handling to establish MBeanServerConnection
-            super.run();
+            super.runInternal();
         }
     }
 
     public void sequenceRun(NodeProbe probe)
     {
-        wrapper.prepare(args != null ? args.toArray(new String[0]) : new String[]{"help"});
-        if (!wrapper.run(probe))
+        wrapper.prepare(args != null ? args.toArray(new String[0]) : new String[]{"help"}, probe.output().out, probe.output().err);
+        if (!wrapper.run(probe, probe.output()))
             probe.failed();
     }
 
     protected void execute(NodeProbe probe)
     {
-        if (!wrapper.run(probe))
+        if (!wrapper.run(probe, probe.output()))
             probe.failed();
     }
 
@@ -107,7 +126,7 @@ public class Sjk extends NodeToolCmd
             throw new UnsupportedOperationException();
         }
 
-        public void prepare(String[] args)
+        public void prepare(String[] args, PrintStream out, PrintStream err)
         {
             try
             {
@@ -143,7 +162,7 @@ public class Sjk extends NodeToolCmd
                 {
                     for (String cmd : commands.keySet())
                     {
-                        System.out.println(String.format("%8s - %s", cmd, parser.getCommandDescription(cmd)));
+                        out.println(String.format("%8s - %s", cmd, parser.getCommandDescription(cmd)));
                     }
                 }
                 else
@@ -161,32 +180,37 @@ public class Sjk extends NodeToolCmd
             {
                 for (String m : error.messages)
                 {
-                    logError(m);
+                    err.println(m);
                 }
                 if (isVerbose() && error.getCause() != null)
                 {
-                    logTrace(error.getCause());
+                    error.getCause().printStackTrace(err);
                 }
                 if (error.printUsage && parser != null)
                 {
-                    if (parser.getParsedCommand() != null)
-                    {
-                        parser.usage(parser.getParsedCommand());
-                    }
-                    else
-                    {
-                        parser.usage();
-                    }
+                    printUsage(parser, out, parser.getParsedCommand());
                 }
             }
             catch (Throwable e)
             {
-                e.printStackTrace();
+                e.printStackTrace(err);
             }
         }
 
-        public boolean run(final NodeProbe probe)
+        void printUsage(JCommander parser, PrintStream out, String optionalCommand)
         {
+            StringBuilder sb = new StringBuilder();
+            if (optionalCommand != null)
+                parser.usage(sb, optionalCommand);
+            else
+                parser.usage(sb);
+            out.println(sb.toString());
+        }
+
+        public boolean run(final NodeProbe probe, final Output output)
+        {
+            PrintStream out = output.out;
+            PrintStream err = output.err;
             try
             {
                 setJmxConnInfo(probe);
@@ -200,28 +224,21 @@ public class Sjk extends NodeToolCmd
             {
                 for (String m : error.messages)
                 {
-                    logError(m);
+                    err.println(m);
                 }
                 if (isVerbose() && error.getCause() != null)
                 {
-                    logTrace(error.getCause());
+                    error.getCause().printStackTrace(err);
                 }
                 if (error.printUsage && parser != null)
                 {
-                    if (parser.getParsedCommand() != null)
-                    {
-                        parser.usage(parser.getParsedCommand());
-                    }
-                    else
-                    {
-                        parser.usage();
-                    }
+                    printUsage(parser, out, parser.getParsedCommand());
                 }
                 return true;
             }
             catch (Throwable e)
             {
-                e.printStackTrace();
+                e.printStackTrace(err);
             }
 
             // abnormal termination
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java b/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
index 495ee9d..6619c53 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Snapshot.java
@@ -24,6 +24,7 @@ import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -53,6 +54,7 @@ public class Snapshot extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         try
         {
             StringBuilder sb = new StringBuilder();
@@ -76,9 +78,9 @@ public class Snapshot extends NodeToolCmd
                 if (!snapshotName.isEmpty())
                     sb.append(" with snapshot name [").append(snapshotName).append("]");
                 sb.append(" and options ").append(options.toString());
-                System.out.println(sb.toString());
+                out.println(sb.toString());
                 probe.takeMultipleTableSnapshot(snapshotName, options, ktList.split(","));
-                System.out.println("Snapshot directory: " + snapshotName);
+                out.println("Snapshot directory: " + snapshotName);
             }
             else
             {
@@ -90,10 +92,10 @@ public class Snapshot extends NodeToolCmd
                 if (!snapshotName.isEmpty())
                     sb.append(" with snapshot name [").append(snapshotName).append("]");
                 sb.append(" and options ").append(options.toString());
-                System.out.println(sb.toString());
+                out.println(sb.toString());
 
                 probe.takeSnapshot(snapshotName, table, options, toArray(keyspaces, String.class));
-                System.out.println("Snapshot directory: " + snapshotName);
+                out.println("Snapshot directory: " + snapshotName);
             }
         }
         catch (IOException e)
@@ -101,4 +103,4 @@ public class Snapshot extends NodeToolCmd
             throw new RuntimeException("Error during taking a snapshot", e);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Status.java b/src/java/org/apache/cassandra/tools/nodetool/Status.java
index fe11335..4184939 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Status.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Status.java
@@ -21,7 +21,7 @@ import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
-import java.net.InetAddress;
+import java.io.PrintStream;
 import java.net.UnknownHostException;
 import java.text.DecimalFormat;
 import java.util.Collection;
@@ -31,7 +31,6 @@ import java.util.Map;
 import java.util.SortedMap;
 
 import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
-import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
@@ -57,6 +56,7 @@ public class Status extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         joiningNodes = probe.getJoiningNodes(printPort);
         leavingNodes = probe.getLeavingNodes(printPort);
         movingNodes = probe.getMovingNodes(printPort);
@@ -84,7 +84,7 @@ public class Status extends NodeToolCmd
         }
         catch (IllegalArgumentException ex)
         {
-            System.out.printf("%nError: %s%n", ex.getMessage());
+            out.printf("%nError: %s%n", ex.getMessage());
             System.exit(1);
         }
 
@@ -118,22 +118,22 @@ public class Status extends NodeToolCmd
         for (Map.Entry<String, SetHostStatWithPort> dc : dcs.entrySet())
         {
             if (!first) {
-                System.out.println();
+                out.println();
             }
             first = false;
             String dcHeader = String.format("Datacenter: %s%n", dc.getKey());
-            System.out.print(dcHeader);
-            for (int i = 0; i < (dcHeader.length() - 1); i++) System.out.print('=');
-            System.out.println();
+            out.print(dcHeader);
+            for (int i = 0; i < (dcHeader.length() - 1); i++) out.print('=');
+            out.println();
 
             // Legend
-            System.out.println("Status=Up/Down");
-            System.out.println("|/ State=Normal/Leaving/Joining/Moving");
+            out.println("Status=Up/Down");
+            out.println("|/ State=Normal/Leaving/Joining/Moving");
             TableBuilder dcTable = results.next();
-            dcTable.printTo(System.out);
+            dcTable.printTo(out);
         }
 
-        System.out.printf("%n" + errors);
+        out.printf("%n" + errors);
     }
 
     private void addNodesHeader(boolean hasEffectiveOwns, TableBuilder tableBuilder)
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StatusAutoCompaction.java b/src/java/org/apache/cassandra/tools/nodetool/StatusAutoCompaction.java
index 4322bb8..a82f8e8 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StatusAutoCompaction.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StatusAutoCompaction.java
@@ -17,12 +17,8 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -67,9 +63,9 @@ public class StatusAutoCompaction extends NodeToolCmd
                 }
             }
             if (showAll)
-                table.printTo(System.out);
+                table.printTo(probe.output().out);
             else
-                System.out.println(allEnabled ? "running" :
+                probe.output().out.println(allEnabled ? "running" :
                                    allDisabled ? "not running" : "partially running");
         }
         catch (IOException e)
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StatusBackup.java b/src/java/org/apache/cassandra/tools/nodetool/StatusBackup.java
index 84fa8a5..0d5b527 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StatusBackup.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StatusBackup.java
@@ -28,9 +28,9 @@ public class StatusBackup extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println(
+        probe.output().out.println(
                 probe.isIncrementalBackupsEnabled()
                 ? "running"
                 : "not running");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StatusBinary.java b/src/java/org/apache/cassandra/tools/nodetool/StatusBinary.java
index 45fe6c3..12bbdf7 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StatusBinary.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StatusBinary.java
@@ -28,9 +28,9 @@ public class StatusBinary extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println(
+        probe.output().out.println(
                 probe.isNativeTransportRunning()
                 ? "running"
                 : "not running");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StatusGossip.java b/src/java/org/apache/cassandra/tools/nodetool/StatusGossip.java
index b6a1164..1c4e66a 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StatusGossip.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StatusGossip.java
@@ -28,9 +28,9 @@ public class StatusGossip extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println(
+        probe.output().out.println(
                 probe.isGossipRunning()
                 ? "running"
                 : "not running");
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/StatusHandoff.java b/src/java/org/apache/cassandra/tools/nodetool/StatusHandoff.java
index bee161d..0978861 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/StatusHandoff.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/StatusHandoff.java
@@ -28,12 +28,12 @@ public class StatusHandoff extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println(String.format("Hinted handoff is %s",
+        probe.output().out.println(String.format("Hinted handoff is %s",
                 probe.isHandoffEnabled()
                 ? "running"
                 : "not running"));
 
         for (String dc : probe.getHintedHandoffDisabledDCs())
-            System.out.println(String.format("Data center %s is disabled", dc));
+            probe.output().out.println(String.format("Data center %s is disabled", dc));
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java b/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
index 1c6fec4..fec47a6 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TableHistograms.java
@@ -22,6 +22,7 @@ import static java.lang.String.format;
 import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -47,6 +48,7 @@ public class TableHistograms extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         Multimap<String, String> tablesList = HashMultimap.create();
 
         // a <keyspace, set<table>> mapping for verification or as reference if none provided
@@ -99,7 +101,7 @@ public class TableHistograms extends NodeToolCmd
 
                 if (ArrayUtils.isEmpty(estimatedPartitionSize) || ArrayUtils.isEmpty(estimatedColumnCount))
                 {
-                    System.out.println("No SSTables exists, unable to calculate 'Partition Size' and 'Cell Count' percentiles");
+                    out.println("No SSTables exists, unable to calculate 'Partition Size' and 'Cell Count' percentiles");
 
                     for (int i = 0; i < 7; i++)
                     {
@@ -114,7 +116,7 @@ public class TableHistograms extends NodeToolCmd
 
                     if (partitionSizeHist.isOverflowed())
                     {
-                        System.out.println(String.format("Row sizes are larger than %s, unable to calculate percentiles", partitionSizeHist.getLargestBucketOffset()));
+                        out.println(String.format("Row sizes are larger than %s, unable to calculate percentiles", partitionSizeHist.getLargestBucketOffset()));
                         for (int i = 0; i < offsetPercentiles.length; i++)
                             estimatedRowSizePercentiles[i] = Double.NaN;
                     }
@@ -126,7 +128,7 @@ public class TableHistograms extends NodeToolCmd
 
                     if (columnCountHist.isOverflowed())
                     {
-                        System.out.println(String.format("Column counts are larger than %s, unable to calculate percentiles", columnCountHist.getLargestBucketOffset()));
+                        out.println(String.format("Column counts are larger than %s, unable to calculate percentiles", columnCountHist.getLargestBucketOffset()));
                         for (int i = 0; i < estimatedColumnCountPercentiles.length; i++)
                             estimatedColumnCountPercentiles[i] = Double.NaN;
                     }
@@ -149,15 +151,15 @@ public class TableHistograms extends NodeToolCmd
                 Double[] writeLatency = probe.metricPercentilesAsArray((CassandraMetricsRegistry.JmxTimerMBean) probe.getColumnFamilyMetric(keyspace, table, "WriteLatency"));
                 Double[] sstablesPerRead = probe.metricPercentilesAsArray((CassandraMetricsRegistry.JmxHistogramMBean) probe.getColumnFamilyMetric(keyspace, table, "SSTablesPerReadHistogram"));
 
-                System.out.println(format("%s/%s histograms", keyspace, table));
-                System.out.println(format("%-10s%18s%18s%18s%18s%18s",
+                out.println(format("%s/%s histograms", keyspace, table));
+                out.println(format("%-10s%18s%18s%18s%18s%18s",
                         "Percentile", "Read Latency", "Write Latency", "SSTables", "Partition Size", "Cell Count"));
-                System.out.println(format("%-10s%18s%18s%18s%18s%18s",
+                out.println(format("%-10s%18s%18s%18s%18s%18s",
                         "", "(micros)", "(micros)", "", "(bytes)", ""));
 
                 for (int i = 0; i < percentiles.length; i++)
                 {
-                    System.out.println(format("%-10s%18.2f%18.2f%18.2f%18.0f%18.0f",
+                    out.println(format("%-10s%18.2f%18.2f%18.2f%18.0f%18.0f",
                             percentiles[i],
                             readLatency[i],
                             writeLatency[i],
@@ -165,7 +167,7 @@ public class TableHistograms extends NodeToolCmd
                             estimatedRowSizePercentiles[i],
                             estimatedColumnCountPercentiles[i]));
                 }
-                System.out.println();
+                out.println();
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TableStats.java b/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
index 5f5651b..47ca132 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TableStats.java
@@ -95,7 +95,7 @@ public class TableStats extends NodeToolCmd
         StatsHolder holder = new TableStatsHolder(probe, humanReadable, ignore, tableNames, sortKey, top);
         // print out the keyspace and table statistics
         StatsPrinter printer = TableStatsPrinter.from(outputFormat, !sortKey.isEmpty());
-        printer.print(holder, System.out);
+        printer.print(holder, probe.output().out);
     }
 
 }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/TpStats.java b/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
index d7e8f06..5b20b13 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/TpStats.java
@@ -45,6 +45,6 @@ public class TpStats extends NodeToolCmd
 
         StatsHolder data = new TpStatsHolder(probe);
         StatsPrinter printer = TpStatsPrinter.from(outputFormat);
-        printer.print(data, System.out);
+        printer.print(data, probe.output().out);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/UpgradeSSTable.java b/src/java/org/apache/cassandra/tools/nodetool/UpgradeSSTable.java
index c957c8c..ba1b6f5 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/UpgradeSSTable.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/UpgradeSSTable.java
@@ -51,7 +51,7 @@ public class UpgradeSSTable extends NodeToolCmd
         {
             try
             {
-                probe.upgradeSSTables(System.out, keyspace, !includeAll, jobs, tableNames);
+                probe.upgradeSSTables(probe.output().out, keyspace, !includeAll, jobs, tableNames);
             }
             catch (Exception e)
             {
@@ -59,4 +59,4 @@ public class UpgradeSSTable extends NodeToolCmd
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Verify.java b/src/java/org/apache/cassandra/tools/nodetool/Verify.java
index 28b91fd..d2ae151 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Verify.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Verify.java
@@ -21,6 +21,7 @@ import io.airlift.airline.Arguments;
 import io.airlift.airline.Command;
 import io.airlift.airline.Option;
 
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -66,12 +67,13 @@ public class Verify extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         List<String> keyspaces = parseOptionalKeyspace(args, probe);
         String[] tableNames = parseOptionalTables(args);
 
         if (checkOwnsTokens && !extendedVerify)
         {
-            System.out.println("Token verification requires --extended-verify");
+            out.println("Token verification requires --extended-verify");
             System.exit(1);
         }
 
@@ -79,11 +81,11 @@ public class Verify extends NodeToolCmd
         {
             try
             {
-                probe.verify(System.out, extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspace, tableNames);
+                probe.verify(out, extendedVerify, checkVersion, diskFailurePolicy, mutateRepairStatus, checkOwnsTokens, quick, keyspace, tableNames);
             } catch (Exception e)
             {
                 throw new RuntimeException("Error occurred during verifying", e);
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Version.java b/src/java/org/apache/cassandra/tools/nodetool/Version.java
index 395a247..f95907a 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Version.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/Version.java
@@ -28,6 +28,6 @@ public class Version extends NodeToolCmd
     @Override
     public void execute(NodeProbe probe)
     {
-        System.out.println("ReleaseVersion: " + probe.getReleaseVersion());
+        probe.output().out.println("ReleaseVersion: " + probe.getReleaseVersion());
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java b/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java
index b432b68..89d35a4 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ViewBuildStatus.java
@@ -18,6 +18,7 @@
 
 package org.apache.cassandra.tools.nodetool;
 
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -40,6 +41,7 @@ public class ViewBuildStatus extends NodeTool.NodeToolCmd
 
     protected void execute(NodeProbe probe)
     {
+        PrintStream out = probe.output().out;
         String keyspace = null, view = null;
         if (args.size() == 2)
         {
@@ -72,12 +74,12 @@ public class ViewBuildStatus extends NodeTool.NodeToolCmd
         }
 
         if (failed) {
-            System.out.println(String.format("%s.%s has not finished building; node status is below.", keyspace, view));
-            System.out.println();
-            builder.printTo(System.out);
+            out.println(String.format("%s.%s has not finished building; node status is below.", keyspace, view));
+            out.println();
+            builder.printTo(out);
             System.exit(1);
         } else {
-            System.out.println(String.format("%s.%s has finished building", keyspace, view));
+            out.println(String.format("%s.%s has finished building", keyspace, view));
             System.exit(0);
         }
     }
diff --git a/src/java/org/apache/cassandra/tools/nodetool/formatter/TableBuilder.java b/src/java/org/apache/cassandra/tools/nodetool/formatter/TableBuilder.java
index 2557a7d..166ed3d 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/formatter/TableBuilder.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/formatter/TableBuilder.java
@@ -38,7 +38,7 @@ import javax.annotation.Nonnull;
  * {
  *     table.add(row);
  * }
- * table.print(System.out);
+ * table.print(probe.outStream());
  * }
  * </pre>
  */
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java
index 617342a..252adf0 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/TpStatsPrinter.java
@@ -66,9 +66,9 @@ public class TpStatsPrinter
                                  data.probe.getThreadPoolMetric(tpool.getKey(), tpool.getValue(), "TotalBlockedTasks").toString());
             }
 
-            poolBuilder.printTo(System.out);
+            poolBuilder.printTo(out);
 
-            System.out.println("\nLatencies waiting in queue (micros) per dropped message types");
+            out.println("\nLatencies waiting in queue (micros) per dropped message types");
 
             final TableBuilder droppedBuilder = new TableBuilder();
             droppedBuilder.add("Message type", "Dropped    ", "50%     ", "95%     ", "99%     ", "Max");
@@ -109,7 +109,7 @@ public class TpStatsPrinter
                 droppedBuilder.add(columns.toArray(new String[0]));
             }
 
-            droppedBuilder.printTo(System.out);
+            droppedBuilder.printTo(out);
         }
     }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
index b20d874..83669e2 100644
--- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
+++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java
@@ -18,8 +18,11 @@
 
 package org.apache.cassandra.distributed.impl;
 
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -102,6 +105,7 @@ import org.apache.cassandra.service.StorageServiceMBean;
 import org.apache.cassandra.streaming.StreamReceiveTask;
 import org.apache.cassandra.streaming.StreamTransferTask;
 import org.apache.cassandra.streaming.async.StreamingInboundHandler;
+import org.apache.cassandra.tools.Output;
 import org.apache.cassandra.tools.NodeTool;
 import org.apache.cassandra.tracing.TraceState;
 import org.apache.cassandra.tracing.Tracing;
@@ -621,20 +625,66 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
     public NodeToolResult nodetoolResult(boolean withNotifications, String... commandAndArgs)
     {
         return sync(() -> {
-            DTestNodeTool nodetool = new DTestNodeTool(withNotifications);
-            int rc =  nodetool.execute(commandAndArgs);
-            return new NodeToolResult(commandAndArgs, rc, new ArrayList<>(nodetool.notifications.notifications), nodetool.latestError);
+            try (CapturingOutput output = new CapturingOutput())
+            {
+                DTestNodeTool nodetool = new DTestNodeTool(withNotifications, output.delegate);
+                int rc = nodetool.execute(commandAndArgs);
+                return new NodeToolResult(commandAndArgs, rc,
+                                          new ArrayList<>(nodetool.notifications.notifications),
+                                          nodetool.latestError,
+                                          output.getOutString(),
+                                          output.getErrString());
+            }
         }).call();
     }
 
+    private static class CapturingOutput implements Closeable
+    {
+        @SuppressWarnings("resource")
+        private final ByteArrayOutputStream outBase = new ByteArrayOutputStream();
+        @SuppressWarnings("resource")
+        private final ByteArrayOutputStream errBase = new ByteArrayOutputStream();
+
+        public final PrintStream out;
+        public final PrintStream err;
+        private final Output delegate;
+
+        public CapturingOutput()
+        {
+            PrintStream out = new PrintStream(outBase, true);
+            PrintStream err = new PrintStream(errBase, true);
+            this.delegate = new Output(out, err);
+            this.out = out;
+            this.err = err;
+        }
+
+        public String getOutString()
+        {
+            out.flush();
+            return outBase.toString();
+        }
+
+        public String getErrString()
+        {
+            err.flush();
+            return errBase.toString();
+        }
+
+        public void close()
+        {
+            out.close();
+            err.close();
+        }
+    }
+
     public static class DTestNodeTool extends NodeTool {
         private final StorageServiceMBean storageProxy;
         private final CollectingNotificationListener notifications = new CollectingNotificationListener();
 
         private Throwable latestError;
 
-        public DTestNodeTool(boolean withNotifications) {
-            super(new InternalNodeProbeFactory(withNotifications));
+        public DTestNodeTool(boolean withNotifications, Output output) {
+            super(new InternalNodeProbeFactory(withNotifications), output);
             storageProxy = new InternalNodeProbe(withNotifications).getStorageService();
             storageProxy.addNotificationListener(notifications, null, null);
         }
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/NodeToolResultWithOutput.java b/test/distributed/org/apache/cassandra/distributed/shared/NodeToolResultWithOutput.java
index cb94887..9cefd4d 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/NodeToolResultWithOutput.java
+++ b/test/distributed/org/apache/cassandra/distributed/shared/NodeToolResultWithOutput.java
@@ -22,6 +22,8 @@ import java.io.ByteArrayOutputStream;
 
 import org.apache.cassandra.distributed.api.NodeToolResult;
 
+// Perfer the NodeToolResult that includes output since version 0.0.5
+// CASSANDRA-16057
 public class NodeToolResultWithOutput
 {
     private final NodeToolResult result;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
index 1a1bdc7..adafe2b 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java
@@ -21,13 +21,14 @@ package org.apache.cassandra.distributed.test;
 import org.junit.Test;
 
 import org.apache.cassandra.distributed.api.ICluster;
+import org.apache.cassandra.distributed.api.NodeToolResult;
 
 import static org.junit.Assert.assertEquals;
 
 public class NodeToolTest extends TestBaseImpl
 {
     @Test
-    public void test() throws Throwable
+    public void testCommands() throws Throwable
     {
         try (ICluster cluster = init(builder().withNodes(1).start()))
         {
@@ -36,4 +37,16 @@ public class NodeToolTest extends TestBaseImpl
             assertEquals(1, cluster.get(1).nodetool("not_a_legal_command"));
         }
     }
+
+    @Test
+    public void testCaptureConsoleOutput() throws Throwable
+    {
+        try (ICluster cluster = init(builder().withNodes(1).start()))
+        {
+            NodeToolResult ringResult = cluster.get(1).nodetoolResult("ring");
+            ringResult.asserts().stdoutContains("Datacenter: datacenter0");
+            ringResult.asserts().stdoutContains("127.0.0.1  rack0       Up     Normal");
+            assertEquals("Non-empty error output", "", ringResult.getStderr());
+        }
+    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/util/NodetoolUtils.java b/test/distributed/org/apache/cassandra/distributed/util/NodetoolUtils.java
index 1bb6adf..834e661 100644
--- a/test/distributed/org/apache/cassandra/distributed/util/NodetoolUtils.java
+++ b/test/distributed/org/apache/cassandra/distributed/util/NodetoolUtils.java
@@ -25,7 +25,10 @@ import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.api.NodeToolResult;
 import org.apache.cassandra.distributed.impl.Instance;
 import org.apache.cassandra.distributed.shared.NodeToolResultWithOutput;
+import org.apache.cassandra.tools.Output;
 
+// Prefer to use the NodeToolResult that includes output since version 0.0.5
+// CASSANDRA-16057
 public final class NodetoolUtils
 {
     private NodetoolUtils()
@@ -53,7 +56,7 @@ public final class NodetoolUtils
             {
                 System.setOut(newOut);
                 System.setErr(newErr);
-                Instance.DTestNodeTool nodetool = new Instance.DTestNodeTool(withNotifications);
+                Instance.DTestNodeTool nodetool = new Instance.DTestNodeTool(withNotifications, new Output(newOut, newErr));
                 int rc = nodetool.execute(args);
                 NodeToolResult result = new NodeToolResult(args, rc, nodetool.getNotifications(), nodetool.getLatestError());
                 return new NodeToolResultWithOutput(result, toolOut, toolErr);
@@ -65,4 +68,4 @@ public final class NodetoolUtils
             }
         });
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Version.java b/test/unit/org/apache/cassandra/tools/nodetool/SjkTest.java
similarity index 69%
copy from src/java/org/apache/cassandra/tools/nodetool/Version.java
copy to test/unit/org/apache/cassandra/tools/nodetool/SjkTest.java
index 395a247..c183edf 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Version.java
+++ b/test/unit/org/apache/cassandra/tools/nodetool/SjkTest.java
@@ -17,17 +17,16 @@
  */
 package org.apache.cassandra.tools.nodetool;
 
-import io.airlift.airline.Command;
+import org.junit.Test;
 
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.tools.ToolRunner;
 
-@Command(name = "version", description = "Print cassandra version")
-public class Version extends NodeToolCmd
+public class SjkTest
 {
-    @Override
-    public void execute(NodeProbe probe)
+    @Test
+    public void sjkHelpReturnsRc0()
     {
-        System.out.println("ReleaseVersion: " + probe.getReleaseVersion());
+        ToolRunner.ToolResult result = ToolRunner.invokeNodetool("sjk", "--help");
+        result.assertOnExitCode();
     }
-}
\ No newline at end of file
+}
diff --git a/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java
index 5daf654..88aa6a4 100644
--- a/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java
+++ b/tools/stress/src/org/apache/cassandra/stress/CompactionStress.java
@@ -262,7 +262,7 @@ public abstract class CompactionStress implements Runnable
     {
         System.out.println("========");
         System.out.println(String.format("Pending compactions: %d\n", CompactionManager.instance.getPendingTasks()));
-        CompactionStats.reportCompactionTable(CompactionManager.instance.getCompactions(), 0, true);
+        CompactionStats.reportCompactionTable(CompactionManager.instance.getCompactions(), 0, true, System.out);
     }
 
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org