From 664ab193d6b4db387e5ed751c0dff1635015bf28 Mon Sep 17 00:00:00 2001 From: Suhel0328 Date: Fri, 30 Aug 2024 11:01:45 +0530 Subject: [PATCH] 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 + .../cassandra/tools/nodetool/GcStats.java | 22 ++- .../tools/nodetool/stats/GcStatsHolder.java | 62 ++++++++ .../tools/nodetool/stats/GcStatsPrinter.java | 71 +++++++++ .../cassandra/tools/nodetool/GcStatsTest.java | 146 ++++++++++++++++++ 5 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java create mode 100644 src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsPrinter.java create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/GcStatsTest.java 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 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/stats/GcStatsHolder.java b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java new file mode 100644 index 0000000000..7533de0e33 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/stats/GcStatsHolder.java @@ -0,0 +1,62 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.tools.NodeProbe; + +/** + * 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 Map convert2Map() + { + HashMap result = new HashMap<>(); + + double[] stats = probe.getAndResetGCStats(); + double mean = stats[2] / stats[5]; + double stdev = Math.sqrt((stats[3] / stats[5]) - (mean * mean)); + + 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 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 + { + /** + * 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 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 )] [(-p | --port )]\n" + + " [(-pp | --print-port)] [(-pw | --password )]\n" + + " [(-pwf | --password-file )]\n" + + " [(-u | --username )] gcstats\n" + + " [(-F | --format )]\n" + + "\n" + + "OPTIONS\n" + + " -F , --format \n" + + " Output format (json, yaml)\n" + + "\n" + + " -h , --host \n" + + " Node hostname or ip address\n" + + "\n" + + " -p , --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 \n" + + " Remote jmx agent password\n" + + "\n" + + " -pwf , --password-file \n" + + " Path to the JMX password file\n" + + "\n" + + " -u , --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