mirror of https://github.com/apache/cassandra
Implement nodetool history
patch by Stefan Miklosovic; reviewed by Maxim Muzafarov for CASSANDRA-20851
This commit is contained in:
parent
63fb8741b8
commit
9c93d52ac9
|
|
@ -1,4 +1,5 @@
|
||||||
5.1
|
5.1
|
||||||
|
* Implement nodetool history (CASSANDRA-20851)
|
||||||
* Expose StorageService.dropPreparedStatements via JMX (CASSANDRA-20870)
|
* Expose StorageService.dropPreparedStatements via JMX (CASSANDRA-20870)
|
||||||
* Expose Metric for Prepared Statement Cache Size (in bytes) (CASSANDRA-20864)
|
* Expose Metric for Prepared Statement Cache Size (in bytes) (CASSANDRA-20864)
|
||||||
* Add support for BEGIN TRANSACTION to allow mutations that touch multiple partitions (CASSANDRA-20857)
|
* Add support for BEGIN TRANSACTION to allow mutations that touch multiple partitions (CASSANDRA-20857)
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ public class NodeTool
|
||||||
String cmdLine = Joiner.on(" ").skipNulls().join(args);
|
String cmdLine = Joiner.on(" ").skipNulls().join(args);
|
||||||
cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " <hidden>");
|
cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " <hidden>");
|
||||||
|
|
||||||
try (FileWriter writer = new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE).newWriter(APPEND))
|
try (FileWriter writer = getHistoryFile().newWriter(APPEND))
|
||||||
{
|
{
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
|
||||||
writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator());
|
writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator());
|
||||||
|
|
@ -144,6 +144,10 @@ public class NodeTool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static File getHistoryFile()
|
||||||
|
{
|
||||||
|
return new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE);
|
||||||
|
}
|
||||||
|
|
||||||
public static List<String> getCommandsWithoutRoot(String separator)
|
public static List<String> getCommandsWithoutRoot(String separator)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.List;
|
||||||
|
|
||||||
|
import org.apache.cassandra.io.util.File;
|
||||||
|
import org.apache.cassandra.io.util.FileUtils;
|
||||||
|
import org.apache.cassandra.tools.NodeProbe;
|
||||||
|
import org.apache.cassandra.tools.NodeTool;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
@Command(name = "history", description = "Print previously executed nodetool commands")
|
||||||
|
public class History extends AbstractCommand
|
||||||
|
{
|
||||||
|
@Option(paramLabel = "commands",
|
||||||
|
names = { "-n", "--num", "--number-of-commands" },
|
||||||
|
description = "Number of commands to print, defaults to 1000.")
|
||||||
|
public int commands = 1000;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(NodeProbe probe)
|
||||||
|
{
|
||||||
|
if (commands < 1)
|
||||||
|
throw new IllegalArgumentException("Number of commands to display has to be at least 1.");
|
||||||
|
|
||||||
|
File historyFile = getHistoryFile();
|
||||||
|
validateHistoryFile(historyFile);
|
||||||
|
|
||||||
|
for (String line : commandsToPrint(historyFile))
|
||||||
|
output.out.println(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldConnect() throws CommandLine.ExecutionException
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File getHistoryFile()
|
||||||
|
{
|
||||||
|
return NodeTool.getHistoryFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> commandsToPrint(File file)
|
||||||
|
{
|
||||||
|
List<String> historyCommands = FileUtils.readLines(file);
|
||||||
|
int size = historyCommands.size();
|
||||||
|
List<String> commandLines;
|
||||||
|
|
||||||
|
if (commands > size)
|
||||||
|
commandLines = historyCommands;
|
||||||
|
else
|
||||||
|
commandLines = historyCommands.subList(size - commands, size);
|
||||||
|
|
||||||
|
return commandLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodetool is appending command to history file before it is executed so us checking on its
|
||||||
|
* existence and validating it is not technically necessary however nodetool is also swallowing
|
||||||
|
* all errors when it was not succesful in appending to the history file so better to check here in that case.
|
||||||
|
*
|
||||||
|
* @param historyFile file to check that it is actually a file which exists and it is readable
|
||||||
|
*/
|
||||||
|
void validateHistoryFile(File historyFile)
|
||||||
|
{
|
||||||
|
if (!historyFile.exists())
|
||||||
|
throw new IllegalStateException(String.format("History file %s does not exist.%n", historyFile.absolutePath()));
|
||||||
|
|
||||||
|
if (!historyFile.isFile())
|
||||||
|
throw new IllegalStateException(String.format("History file %s is not a file.%n", historyFile.absolutePath()));
|
||||||
|
|
||||||
|
if (!historyFile.isReadable())
|
||||||
|
throw new IllegalStateException(String.format("History file %s is not readable.%n", historyFile.absolutePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -128,6 +128,7 @@ import static org.apache.cassandra.tools.nodetool.Help.printTopCommandUsage;
|
||||||
GossipInfo.class,
|
GossipInfo.class,
|
||||||
GuardrailsConfigCommand.GetGuardrailsConfig.class,
|
GuardrailsConfigCommand.GetGuardrailsConfig.class,
|
||||||
GuardrailsConfigCommand.SetGuardrailsConfig.class,
|
GuardrailsConfigCommand.SetGuardrailsConfig.class,
|
||||||
|
History.class,
|
||||||
Import.class,
|
Import.class,
|
||||||
Info.class,
|
Info.class,
|
||||||
InvalidateCIDRPermissionsCache.class,
|
InvalidateCIDRPermissionsCache.class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
NAME
|
||||||
|
nodetool history - Print previously executed nodetool commands
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
|
||||||
|
[(-pp | --print-port)] [(-pw <password> | --password <password>)]
|
||||||
|
[(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
|
||||||
|
[(-u <username> | --username <username>)] history
|
||||||
|
[(-n <commands> | --number-of-commands <commands>)]
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
-h <host>, --host <host>
|
||||||
|
Node hostname or ip address
|
||||||
|
|
||||||
|
-n <commands>, --num <commands>, --number-of-commands <commands>
|
||||||
|
Number of commands to print, defaults to 1000.
|
||||||
|
|
||||||
|
-p <port>, --port <port>
|
||||||
|
Remote jmx agent port number
|
||||||
|
|
||||||
|
-pp, --print-port
|
||||||
|
Operate in 4.0 mode with hosts disambiguated by port number
|
||||||
|
|
||||||
|
-pw <password>, --password <password>
|
||||||
|
Remote jmx agent password
|
||||||
|
|
||||||
|
-pwf <passwordFilePath>, --password-file <passwordFilePath>
|
||||||
|
Path to the JMX password file
|
||||||
|
|
||||||
|
-u <username>, --username <username>
|
||||||
|
Remote jmx agent username
|
||||||
|
|
@ -77,6 +77,7 @@ The most commonly used nodetool commands are:
|
||||||
gettraceprobability Print the current trace probability value
|
gettraceprobability Print the current trace probability value
|
||||||
gossipinfo Shows the gossip information for the cluster
|
gossipinfo Shows the gossip information for the cluster
|
||||||
help Display help information
|
help Display help information
|
||||||
|
history Print previously executed nodetool commands
|
||||||
import Import new SSTables to the system
|
import Import new SSTables to the system
|
||||||
info Print node information (uptime, load, ...)
|
info Print node information (uptime, load, ...)
|
||||||
invalidatecidrpermissionscache Invalidate the cidr permissions cache
|
invalidatecidrpermissionscache Invalidate the cidr permissions cache
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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.List;
|
||||||
|
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.io.util.File;
|
||||||
|
import org.apache.cassandra.io.util.FileUtils;
|
||||||
|
import org.apache.cassandra.tools.Output;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
public class HistoryTest
|
||||||
|
{
|
||||||
|
public static File tempHistoryFile = FileUtils.createTempFile("test_nodetool", "history");
|
||||||
|
public static File emptyHistoryFile = FileUtils.createTempFile("test_nodetool_empty", "history");
|
||||||
|
public static List<String> listOfCommands = List.of("oldest command",
|
||||||
|
"older command",
|
||||||
|
"newer command",
|
||||||
|
"newest command");
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void populateHistoryFile()
|
||||||
|
{
|
||||||
|
FileUtils.write(tempHistoryFile, listOfCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHistory()
|
||||||
|
{
|
||||||
|
History history = new History()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
File getHistoryFile()
|
||||||
|
{
|
||||||
|
return HistoryTest.tempHistoryFile;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
history.logger(Output.CONSOLE);
|
||||||
|
File historyFile = history.getHistoryFile();
|
||||||
|
|
||||||
|
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.toArray(new String[0]));
|
||||||
|
|
||||||
|
history.commands = 2;
|
||||||
|
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.subList(2, listOfCommands.size()).toArray(new String[0]));
|
||||||
|
|
||||||
|
history.commands = 4;
|
||||||
|
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyHistory()
|
||||||
|
{
|
||||||
|
History history = new History()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
File getHistoryFile()
|
||||||
|
{
|
||||||
|
return HistoryTest.emptyHistoryFile;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
history.logger(Output.CONSOLE);
|
||||||
|
File historyFile = history.getHistoryFile();
|
||||||
|
|
||||||
|
assertThat(history.commandsToPrint(historyFile)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHistoryOutputOnNonExistingHistoryFile()
|
||||||
|
{
|
||||||
|
History history = new History()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
File getHistoryFile()
|
||||||
|
{
|
||||||
|
return new File("/does/not/exist");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
history.logger(Output.CONSOLE);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> history.execute(null))
|
||||||
|
.isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessage("History file /does/not/exist does not exist.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidNumberOfCommandsToPrint()
|
||||||
|
{
|
||||||
|
History history = new History();
|
||||||
|
history.commands = 0;
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> history.execute(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Number of commands to display has to be at least 1.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue