Skip to content

Commit 9c93d52

Browse files
committed
Implement nodetool history
patch by Stefan Miklosovic; reviewed by Maxim Muzafarov for CASSANDRA-20851
1 parent 63fb874 commit 9c93d52

File tree

7 files changed

+254
-1
lines changed

7 files changed

+254
-1
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
5.1
2+
* Implement nodetool history (CASSANDRA-20851)
23
* Expose StorageService.dropPreparedStatements via JMX (CASSANDRA-20870)
34
* Expose Metric for Prepared Statement Cache Size (in bytes) (CASSANDRA-20864)
45
* Add support for BEGIN TRANSACTION to allow mutations that touch multiple partitions (CASSANDRA-20857)

src/java/org/apache/cassandra/tools/NodeTool.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private static void printHistory(String... args)
133133
String cmdLine = Joiner.on(" ").skipNulls().join(args);
134134
cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " <hidden>");
135135

136-
try (FileWriter writer = new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE).newWriter(APPEND))
136+
try (FileWriter writer = getHistoryFile().newWriter(APPEND))
137137
{
138138
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
139139
writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator());
@@ -144,6 +144,10 @@ private static void printHistory(String... args)
144144
}
145145
}
146146

147+
public static File getHistoryFile()
148+
{
149+
return new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE);
150+
}
147151

148152
public static List<String> getCommandsWithoutRoot(String separator)
149153
{
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.tools.nodetool;
20+
21+
import java.util.List;
22+
23+
import org.apache.cassandra.io.util.File;
24+
import org.apache.cassandra.io.util.FileUtils;
25+
import org.apache.cassandra.tools.NodeProbe;
26+
import org.apache.cassandra.tools.NodeTool;
27+
import picocli.CommandLine;
28+
import picocli.CommandLine.Command;
29+
import picocli.CommandLine.Option;
30+
31+
@Command(name = "history", description = "Print previously executed nodetool commands")
32+
public class History extends AbstractCommand
33+
{
34+
@Option(paramLabel = "commands",
35+
names = { "-n", "--num", "--number-of-commands" },
36+
description = "Number of commands to print, defaults to 1000.")
37+
public int commands = 1000;
38+
39+
@Override
40+
protected void execute(NodeProbe probe)
41+
{
42+
if (commands < 1)
43+
throw new IllegalArgumentException("Number of commands to display has to be at least 1.");
44+
45+
File historyFile = getHistoryFile();
46+
validateHistoryFile(historyFile);
47+
48+
for (String line : commandsToPrint(historyFile))
49+
output.out.println(line);
50+
}
51+
52+
@Override
53+
protected boolean shouldConnect() throws CommandLine.ExecutionException
54+
{
55+
return false;
56+
}
57+
58+
File getHistoryFile()
59+
{
60+
return NodeTool.getHistoryFile();
61+
}
62+
63+
List<String> commandsToPrint(File file)
64+
{
65+
List<String> historyCommands = FileUtils.readLines(file);
66+
int size = historyCommands.size();
67+
List<String> commandLines;
68+
69+
if (commands > size)
70+
commandLines = historyCommands;
71+
else
72+
commandLines = historyCommands.subList(size - commands, size);
73+
74+
return commandLines;
75+
}
76+
77+
/**
78+
* Nodetool is appending command to history file before it is executed so us checking on its
79+
* existence and validating it is not technically necessary however nodetool is also swallowing
80+
* all errors when it was not succesful in appending to the history file so better to check here in that case.
81+
*
82+
* @param historyFile file to check that it is actually a file which exists and it is readable
83+
*/
84+
void validateHistoryFile(File historyFile)
85+
{
86+
if (!historyFile.exists())
87+
throw new IllegalStateException(String.format("History file %s does not exist.%n", historyFile.absolutePath()));
88+
89+
if (!historyFile.isFile())
90+
throw new IllegalStateException(String.format("History file %s is not a file.%n", historyFile.absolutePath()));
91+
92+
if (!historyFile.isReadable())
93+
throw new IllegalStateException(String.format("History file %s is not readable.%n", historyFile.absolutePath()));
94+
}
95+
}

src/java/org/apache/cassandra/tools/nodetool/NodetoolCommand.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
GossipInfo.class,
129129
GuardrailsConfigCommand.GetGuardrailsConfig.class,
130130
GuardrailsConfigCommand.SetGuardrailsConfig.class,
131+
History.class,
131132
Import.class,
132133
Info.class,
133134
InvalidateCIDRPermissionsCache.class,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
NAME
2+
nodetool history - Print previously executed nodetool commands
3+
4+
SYNOPSIS
5+
nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
6+
[(-pp | --print-port)] [(-pw <password> | --password <password>)]
7+
[(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
8+
[(-u <username> | --username <username>)] history
9+
[(-n <commands> | --number-of-commands <commands>)]
10+
11+
OPTIONS
12+
-h <host>, --host <host>
13+
Node hostname or ip address
14+
15+
-n <commands>, --num <commands>, --number-of-commands <commands>
16+
Number of commands to print, defaults to 1000.
17+
18+
-p <port>, --port <port>
19+
Remote jmx agent port number
20+
21+
-pp, --print-port
22+
Operate in 4.0 mode with hosts disambiguated by port number
23+
24+
-pw <password>, --password <password>
25+
Remote jmx agent password
26+
27+
-pwf <passwordFilePath>, --password-file <passwordFilePath>
28+
Path to the JMX password file
29+
30+
-u <username>, --username <username>
31+
Remote jmx agent username

test/resources/nodetool/help/nodetool

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ The most commonly used nodetool commands are:
7777
gettraceprobability Print the current trace probability value
7878
gossipinfo Shows the gossip information for the cluster
7979
help Display help information
80+
history Print previously executed nodetool commands
8081
import Import new SSTables to the system
8182
info Print node information (uptime, load, ...)
8283
invalidatecidrpermissionscache Invalidate the cidr permissions cache
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.tools.nodetool;
20+
21+
import java.util.List;
22+
23+
import org.junit.BeforeClass;
24+
import org.junit.Test;
25+
26+
import org.apache.cassandra.io.util.File;
27+
import org.apache.cassandra.io.util.FileUtils;
28+
import org.apache.cassandra.tools.Output;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
32+
33+
public class HistoryTest
34+
{
35+
public static File tempHistoryFile = FileUtils.createTempFile("test_nodetool", "history");
36+
public static File emptyHistoryFile = FileUtils.createTempFile("test_nodetool_empty", "history");
37+
public static List<String> listOfCommands = List.of("oldest command",
38+
"older command",
39+
"newer command",
40+
"newest command");
41+
42+
@BeforeClass
43+
public static void populateHistoryFile()
44+
{
45+
FileUtils.write(tempHistoryFile, listOfCommands);
46+
}
47+
48+
@Test
49+
public void testHistory()
50+
{
51+
History history = new History()
52+
{
53+
@Override
54+
File getHistoryFile()
55+
{
56+
return HistoryTest.tempHistoryFile;
57+
}
58+
};
59+
60+
history.logger(Output.CONSOLE);
61+
File historyFile = history.getHistoryFile();
62+
63+
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.toArray(new String[0]));
64+
65+
history.commands = 2;
66+
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.subList(2, listOfCommands.size()).toArray(new String[0]));
67+
68+
history.commands = 4;
69+
assertThat(history.commandsToPrint(historyFile)).containsExactly(listOfCommands.toArray(new String[0]));
70+
}
71+
72+
@Test
73+
public void testEmptyHistory()
74+
{
75+
History history = new History()
76+
{
77+
@Override
78+
File getHistoryFile()
79+
{
80+
return HistoryTest.emptyHistoryFile;
81+
}
82+
};
83+
84+
history.logger(Output.CONSOLE);
85+
File historyFile = history.getHistoryFile();
86+
87+
assertThat(history.commandsToPrint(historyFile)).isEmpty();
88+
}
89+
90+
91+
@Test
92+
public void testHistoryOutputOnNonExistingHistoryFile()
93+
{
94+
History history = new History()
95+
{
96+
@Override
97+
File getHistoryFile()
98+
{
99+
return new File("/does/not/exist");
100+
}
101+
};
102+
103+
history.logger(Output.CONSOLE);
104+
105+
assertThatThrownBy(() -> history.execute(null))
106+
.isInstanceOf(IllegalStateException.class)
107+
.hasMessage("History file /does/not/exist does not exist.\n");
108+
}
109+
110+
@Test
111+
public void testInvalidNumberOfCommandsToPrint()
112+
{
113+
History history = new History();
114+
history.commands = 0;
115+
116+
assertThatThrownBy(() -> history.execute(null))
117+
.isInstanceOf(IllegalArgumentException.class)
118+
.hasMessage("Number of commands to display has to be at least 1.");
119+
}
120+
}

0 commit comments

Comments
 (0)