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
This commit is contained in:
Suhel0328 2024-08-30 11:01:45 +05:30 committed by Stefan Miklosovic
parent 9cfe1f478a
commit 664ab193d6
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
5 changed files with 296 additions and 6 deletions

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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<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));
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;
}
}

View File

@ -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"));
}
}
}

View File

@ -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.");
}
}