You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sm...@apache.org on 2024/10/02 18:56:35 UTC

(cassandra) branch trunk updated: Add JSON and YAML output option to nodetool gcstats

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

smiklosovic 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 664ab193d6 Add JSON and YAML output option to nodetool gcstats
664ab193d6 is described below

commit 664ab193d6b4db387e5ed751c0dff1635015bf28
Author: Suhel0328 <su...@ksolves.com>
AuthorDate: Fri Aug 30 11:01:45 2024 +0530

    Add JSON and YAML output option to nodetool gcstats
    
    patch by Mohammad Suhel; reviewed by Maxim Muzafarov, Maxwell Guo and Stefan Miklosovic for CASSANDRA-19771
---
 CHANGES.txt                                        |   1 +
 .../apache/cassandra/tools/nodetool/GcStats.java   |  22 +++-
 .../{GcStats.java => stats/GcStatsHolder.java}     |  45 +++++--
 .../tools/nodetool/stats/GcStatsPrinter.java       |  71 ++++++++++
 .../cassandra/tools/nodetool/GcStatsTest.java      | 146 +++++++++++++++++++++
 5 files changed, 269 insertions(+), 16 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 89a6c8abc4..1797b9fdf0 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 5.1
+ * Add JSON and YAML output option to nodetool gcstats (CASSANDRA-19771)
  * Introduce metadata serialization version V4 (CASSANDRA-19970)
  * Allow CMS reconfiguration to work around DOWN nodes (CASSANDRA-19943)
  * Make TableParams.Serializer set allowAutoSnapshots and incrementalBackups (CASSANDRA-19954)
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java b/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
index 6e36f25040..f65095bcaf 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
@@ -19,19 +19,29 @@ package org.apache.cassandra.tools.nodetool;
 
 import org.apache.cassandra.tools.NodeProbe;
 import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.tools.nodetool.stats.GcStatsHolder;
+import org.apache.cassandra.tools.nodetool.stats.GcStatsPrinter;
+import org.apache.cassandra.tools.nodetool.stats.StatsPrinter;
 
 import io.airlift.airline.Command;
+import io.airlift.airline.Option;
 
 @Command(name = "gcstats", description = "Print GC Statistics")
 public class GcStats extends NodeToolCmd
 {
+    @Option(title = "format",
+            name = {"-F", "--format"},
+            description = "Output format (json, yaml)")
+    private String outputFormat = "";
+
     @Override
     public void execute(NodeProbe probe)
     {
-        double[] stats = probe.getAndResetGCStats();
-        double mean = stats[2] / stats[5];
-        double stdev = Math.sqrt((stats[3] / stats[5]) - (mean * mean));
-        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]);
+        if (!outputFormat.isEmpty() && !"json".equals(outputFormat) && !"yaml".equals(outputFormat))
+            throw new IllegalArgumentException("arguments for -F are json, yaml only.");
+
+        GcStatsHolder data = new GcStatsHolder(probe);
+        StatsPrinter<GcStatsHolder> printer = GcStatsPrinter.from(outputFormat);
+        printer.print(data, probe.output().out);
     }
-}
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java
similarity index 51%
copy from src/java/org/apache/cassandra/tools/nodetool/GcStats.java
copy to src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java
index 6e36f25040..7533de0e33 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/GcStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java
@@ -15,23 +15,48 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.cassandra.tools.nodetool;
 
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+package org.apache.cassandra.tools.nodetool.stats;
+
+import java.util.HashMap;
+import java.util.Map;
 
-import io.airlift.airline.Command;
+import org.apache.cassandra.tools.NodeProbe;
 
-@Command(name = "gcstats", description = "Print GC Statistics")
-public class GcStats extends NodeToolCmd
+/**
+ * Holds and converts GC statistics to a map structure.
+ */
+public class GcStatsHolder implements StatsHolder
 {
+    public final NodeProbe probe;
+
+    public GcStatsHolder(NodeProbe probe)
+    {
+        this.probe = probe;
+    }
+
+    /**
+     * Converts the GC statistics gathered from the probe into a map.
+     *
+     * @return A map containing GC statistics with keys such as interval_ms, max_gc_elapsed_ms, etc.
+     */
     @Override
-    public void execute(NodeProbe probe)
+    public Map<String, Object> convert2Map()
     {
+        HashMap<String, Object> result = new HashMap<>();
+
         double[] stats = probe.getAndResetGCStats();
         double mean = stats[2] / stats[5];
         double stdev = Math.sqrt((stats[3] / stats[5]) - (mean * mean));
-        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]);
+
+        result.put("interval_ms", stats[0]);
+        result.put("max_gc_elapsed_ms", stats[1]);
+        result.put("total_gc_elapsed_ms", stats[2]);
+        result.put("stdev_gc_elapsed_ms", stdev);
+        result.put("gc_reclaimed_mb", stats[4]);
+        result.put("collections", stats[5]);
+        result.put("direct_memory_bytes", (long) stats[6]);
+
+        return result;
     }
-}
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsPrinter.java b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsPrinter.java
new file mode 100644
index 0000000000..87b639184d
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsPrinter.java
@@ -0,0 +1,71 @@
+/*
+ * 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.stats;
+
+import java.io.PrintStream;
+import java.util.Map;
+
+/**
+ * Printer for GC statistics.
+ */
+public class GcStatsPrinter
+{
+    /**
+     * Factory method to get a printer based on the format.
+     *
+     * @param format The desired output format (e.g., json, yaml).
+     * @return A StatsPrinter appropriate for the format.
+     */
+    public static StatsPrinter<GcStatsHolder> from(String format)
+    {
+        switch (format)
+        {
+            case "json":
+                return new StatsPrinter.JsonPrinter<>();
+            case "yaml":
+                return new StatsPrinter.YamlPrinter<>();
+            default:
+                return new DefaultPrinter();
+        }
+    }
+
+    /**
+     * Default printer for GC statistics.
+     */
+    public static class DefaultPrinter implements StatsPrinter<GcStatsHolder>
+    {
+        /**
+         * Prints GC statistics in a human-readable table format.
+         *
+         * @param data The GC statistics data holder.
+         * @param out The output stream to print to.
+         */
+        @Override
+        public void print(GcStatsHolder data, PrintStream out)
+        {
+            Map<String, Object> stats = data.convert2Map();
+
+            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");
+            out.printf("%20.0f%20.0f%20.0f%20.0f%20.0f%20.0f%25d%n", stats.get("interval_ms"), stats.get("max_gc_elapsed_ms"),
+                    stats.get("total_gc_elapsed_ms"), stats.get("stdev_gc_elapsed_ms"), stats.get("gc_reclaimed_mb"),
+                    stats.get("collections"), (long) stats.get("direct_memory_bytes"));
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GcStatsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GcStatsTest.java
new file mode 100644
index 0000000000..107b37a873
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/GcStatsTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.util.Arrays;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.service.GCInspector;
+import org.apache.cassandra.tools.ToolRunner;
+import org.apache.cassandra.utils.JsonUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+public class GcStatsTest extends CQLTester
+{
+    @BeforeClass
+    public static void setUp() throws Exception
+    {
+        requireNetwork();
+        startJMXServer();
+        GCInspector.register();
+    }
+
+    @Test
+    @SuppressWarnings("SingleCharacterStringConcatenation")
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "gcstats");
+        tool.assertOnCleanExit();
+
+        String help =   "NAME\n" +
+                        "        nodetool gcstats - Print GC Statistics\n" +
+                        "\n" +
+                        "SYNOPSIS\n" +
+                        "        nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+                        "                [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+                        "                [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+                        "                [(-u <username> | --username <username>)] gcstats\n" +
+                        "                [(-F <format> | --format <format>)]\n" +
+                        "\n" +
+                        "OPTIONS\n" +
+                        "        -F <format>, --format <format>\n" +
+                        "            Output format (json, yaml)\n" +
+                        "\n" +
+                        "        -h <host>, --host <host>\n" +
+                        "            Node hostname or ip address\n" +
+                        "\n" +
+                        "        -p <port>, --port <port>\n" +
+                        "            Remote jmx agent port number\n" +
+                        "\n" +
+                        "        -pp, --print-port\n" +
+                        "            Operate in 4.0 mode with hosts disambiguated by port number\n" +
+                        "\n" +
+                        "        -pw <password>, --password <password>\n" +
+                        "            Remote jmx agent password\n" +
+                        "\n" +
+                        "        -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+                        "            Path to the JMX password file\n" +
+                        "\n" +
+                        "        -u <username>, --username <username>\n" +
+                        "            Remote jmx agent username\n" +
+                        "\n";
+
+        assertThat(tool.getStdout().trim()).isEqualTo(help.trim());
+    }
+
+    @Test
+    public void testDefaultGcStatsOutput()
+    {
+        ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("gcstats");
+        tool.assertOnCleanExit();
+        String output = tool.getStdout();
+        assertThat(output).contains("Interval (ms)");
+        assertThat(output).contains("Max GC Elapsed (ms)");
+        assertThat(output).contains("Total GC Elapsed (ms)");
+        assertThat(output).contains("GC Reclaimed (MB)");
+        assertThat(output).contains("Collections");
+        assertThat(output).contains("Direct Memory Bytes");
+    }
+
+    @Test
+    public void testJsonGcStatsOutput()
+    {
+        Arrays.asList("-F", "--format").forEach(arg -> {
+            ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("gcstats", arg, "json");
+            tool.assertOnCleanExit();
+            String json = tool.getStdout();
+            assertThatCode(() -> JsonUtils.JSON_OBJECT_MAPPER.readTree(json)).doesNotThrowAnyException();
+            assertThat(json).containsPattern("\"interval_ms\"");
+            assertThat(json).containsPattern("\"stdev_gc_elapsed_ms\"");
+            assertThat(json).containsPattern("\"collections\"");
+            assertThat(json).containsPattern("\"max_gc_elapsed_ms\"");
+            assertThat(json).containsPattern("\"gc_reclaimed_mb\"");
+            assertThat(json).containsPattern("\"total_gc_elapsed_ms\"");
+            assertThat(json).containsPattern("\"direct_memory_bytes\"");
+        });
+    }
+
+    @Test
+    public void testYamlGcStatsOutput()
+    {
+        Arrays.asList("-F", "--format").forEach(arg -> {
+            ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("gcstats", arg, "yaml");
+            tool.assertOnCleanExit();
+            String yamlOutput = tool.getStdout();
+            Yaml yaml = new Yaml();
+            assertThatCode(() -> yaml.load(yamlOutput)).doesNotThrowAnyException();
+            assertThat(yamlOutput).containsPattern("interval_ms:");
+            assertThat(yamlOutput).containsPattern("stdev_gc_elapsed_ms:");
+            assertThat(yamlOutput).containsPattern("collections:");
+            assertThat(yamlOutput).containsPattern("max_gc_elapsed_ms:");
+            assertThat(yamlOutput).containsPattern("gc_reclaimed_mb:");
+            assertThat(yamlOutput).containsPattern("total_gc_elapsed_ms:");
+            assertThat(yamlOutput).containsPattern("direct_memory_bytes:");
+        });
+    }
+
+    @Test
+    public void testInvalidFormatOption() throws Exception
+    {
+        ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("gcstats", "-F", "invalid_format");
+        assertThat(tool.getExitCode()).isEqualTo(1);
+        assertThat(tool.getStdout()).contains("arguments for -F are json, yaml only.");
+    }
+}
\ No newline at end of file


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