This is an automated email from the ASF dual-hosted git repository.
bereng 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 537d02d Expose all client options via system_views.clients and
nodetool clientstats
537d02d is described below
commit 537d02d25f1953f1907d44106f83874ac73e06b4
Author: Tibor Répási <[email protected]>
AuthorDate: Tue Jan 4 17:33:31 2022 +0100
Expose all client options via system_views.clients and nodetool clientstats
patch by Tibor Repasi reviewed by Benjamin Lerer, Berenguer Blasi,
Ekaterina Dimitrova for CASSANDRA-16378
---
CHANGES.txt | 1 +
NEWS.txt | 1 +
.../apache/cassandra/db/virtual/ClientsTable.java | 3 +
.../org/apache/cassandra/service/ClientState.java | 17 +++
.../cassandra/tools/nodetool/ClientStats.java | 5 +-
.../cassandra/transport/ConnectedClient.java | 11 ++
.../transport/messages/StartupMessage.java | 1 +
.../cassandra/db/virtual/ClientsTableTest.java | 74 ++++++++++
.../cassandra/tools/nodetool/ClientStatsTest.java | 162 +++++++++++++++++++++
9 files changed, 273 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 2beaa5c..92c14ee 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -2,6 +2,7 @@
* remove unused imports in cqlsh.py and cqlshlib (CASSANDRA-17413)
* deprecate property windows_timer_interval (CASSANDRA-17404)
* Expose streaming as a vtable (CASSANDRA-17390)
+ * Expose all client options via system_views.clients and nodetool clientstats
(CASSANDRA-16378)
* Make startup checks configurable (CASSANDRA-17220)
* Add guardrail for number of partition keys on IN queries (CASSANDRA-17186)
* update Python test framework from nose to pytest (CASSANDRA-17293)
diff --git a/NEWS.txt b/NEWS.txt
index 4f5742a..65cce3c 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -56,6 +56,7 @@ using the provided 'sstableupgrade' tool.
New features
------------
+ - Expose all client options via system_views.clients and nodetool
clientstats.
- Support for String concatenation has been added through the + operator.
- New configuration max_hints_size_per_host to limit the size of local
hints files per host in mebibytes. Setting to
non-positive value disables the limit, which is the default behavior.
Setting to a positive value to ensure
diff --git a/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
b/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
index 40e175b..d39c269 100644
--- a/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/ClientsTable.java
@@ -33,6 +33,7 @@ final class ClientsTable extends AbstractVirtualTable
private static final String USERNAME = "username";
private static final String CONNECTION_STAGE = "connection_stage";
private static final String PROTOCOL_VERSION = "protocol_version";
+ private static final String CLIENT_OPTIONS = "client_options";
private static final String DRIVER_NAME = "driver_name";
private static final String DRIVER_VERSION = "driver_version";
private static final String REQUEST_COUNT = "request_count";
@@ -52,6 +53,7 @@ final class ClientsTable extends AbstractVirtualTable
.addRegularColumn(USERNAME, UTF8Type.instance)
.addRegularColumn(CONNECTION_STAGE,
UTF8Type.instance)
.addRegularColumn(PROTOCOL_VERSION,
Int32Type.instance)
+ .addRegularColumn(CLIENT_OPTIONS,
MapType.getInstance(UTF8Type.instance, UTF8Type.instance, false))
.addRegularColumn(DRIVER_NAME, UTF8Type.instance)
.addRegularColumn(DRIVER_VERSION, UTF8Type.instance)
.addRegularColumn(REQUEST_COUNT, LongType.instance)
@@ -75,6 +77,7 @@ final class ClientsTable extends AbstractVirtualTable
.column(USERNAME, client.username().orElse(null))
.column(CONNECTION_STAGE,
client.stage().toString().toLowerCase())
.column(PROTOCOL_VERSION, client.protocolVersion())
+ .column(CLIENT_OPTIONS, client.clientOptions().orElse(null))
.column(DRIVER_NAME, client.driverName().orElse(null))
.column(DRIVER_VERSION, client.driverVersion().orElse(null))
.column(REQUEST_COUNT, client.requestCount())
diff --git a/src/java/org/apache/cassandra/service/ClientState.java
b/src/java/org/apache/cassandra/service/ClientState.java
index 24d6225..65c562d 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -23,11 +23,14 @@ import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -123,6 +126,9 @@ public class ClientState
// Driver String for the client
private volatile String driverName;
private volatile String driverVersion;
+
+ // Options provided by the client
+ private volatile Map<String,String> clientOptions;
// The biggest timestamp that was returned by getTimestamp/assigned to a
query. This is global to ensure that the
// timestamp assigned are strictly monotonic on a node, which is likely
what user expect intuitively (more likely,
@@ -155,6 +161,7 @@ public class ClientState
this.keyspace = source.keyspace;
this.driverName = source.driverName;
this.driverVersion = source.driverVersion;
+ this.clientOptions = source.clientOptions;
}
/**
@@ -285,6 +292,11 @@ public class ClientState
return Optional.ofNullable(driverVersion);
}
+ public Optional<Map<String,String>> getClientOptions()
+ {
+ return Optional.ofNullable(clientOptions);
+ }
+
public void setDriverName(String driverName)
{
this.driverName = driverName;
@@ -294,6 +306,11 @@ public class ClientState
{
this.driverVersion = driverVersion;
}
+
+ public void setClientOptions(Map<String,String> clientOptions)
+ {
+ this.clientOptions = ImmutableMap.copyOf(clientOptions);
+ }
public static QueryHandler getCQLQueryHandler()
{
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
index b9bf45e..ecaa5f3 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ClientStats.java
@@ -89,7 +89,7 @@ public class ClientStats extends NodeToolCmd
if (!clients.isEmpty())
{
TableBuilder table = new TableBuilder();
- table.add("Address", "SSL", "Cipher", "Protocol", "Version",
"User", "Keyspace", "Requests", "Driver-Name", "Driver-Version");
+ table.add("Address", "SSL", "Cipher", "Protocol", "Version",
"User", "Keyspace", "Requests", "Driver-Name", "Driver-Version",
"Client-Options");
for (Map<String, String> conn : clients)
{
table.add(conn.get(ConnectedClient.ADDRESS),
@@ -101,7 +101,8 @@ public class ClientStats extends NodeToolCmd
conn.get(ConnectedClient.KEYSPACE),
conn.get(ConnectedClient.REQUESTS),
conn.get(ConnectedClient.DRIVER_NAME),
- conn.get(ConnectedClient.DRIVER_VERSION));
+ conn.get(ConnectedClient.DRIVER_VERSION),
+ conn.get(ConnectedClient.CLIENT_OPTIONS));
}
table.printTo(out);
out.println();
diff --git a/src/java/org/apache/cassandra/transport/ConnectedClient.java
b/src/java/org/apache/cassandra/transport/ConnectedClient.java
index ca100f2..a4af32f 100644
--- a/src/java/org/apache/cassandra/transport/ConnectedClient.java
+++ b/src/java/org/apache/cassandra/transport/ConnectedClient.java
@@ -18,9 +18,11 @@
package org.apache.cassandra.transport;
import java.net.InetSocketAddress;
+import java.util.Collections;
import java.util.Map;
import java.util.Optional;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import io.netty.handler.ssl.SslHandler;
@@ -32,6 +34,7 @@ public final class ConnectedClient
public static final String ADDRESS = "address";
public static final String USER = "user";
public static final String VERSION = "version";
+ public static final String CLIENT_OPTIONS = "clientOptions";
public static final String DRIVER_NAME = "driverName";
public static final String DRIVER_VERSION = "driverVersion";
public static final String REQUESTS = "requests";
@@ -83,6 +86,11 @@ public final class ConnectedClient
return state().getDriverVersion();
}
+ public Optional<Map<String,String>> clientOptions()
+ {
+ return state().getClientOptions();
+ }
+
public long requestCount()
{
return connection.requests.getCount();
@@ -132,6 +140,9 @@ public final class ConnectedClient
.put(ADDRESS, remoteAddress().toString())
.put(USER, username().orElse(UNDEFINED))
.put(VERSION, String.valueOf(protocolVersion()))
+ .put(CLIENT_OPTIONS, Joiner.on(", ")
+
.withKeyValueSeparator("=")
+
.join(clientOptions().orElse(Collections.emptyMap())))
.put(DRIVER_NAME, driverName().orElse(UNDEFINED))
.put(DRIVER_VERSION,
driverVersion().orElse(UNDEFINED))
.put(REQUESTS, String.valueOf(requestCount()))
diff --git
a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
index 172768c..37afb22 100644
--- a/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
+++ b/src/java/org/apache/cassandra/transport/messages/StartupMessage.java
@@ -110,6 +110,7 @@ public class StartupMessage extends Message.Request
connection.setThrowOnOverload("1".equals(options.get(THROW_ON_OVERLOAD)));
ClientState clientState = state.getClientState();
+ clientState.setClientOptions(options);
String driverName = options.get(DRIVER_NAME);
if (null != driverName)
{
diff --git a/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
new file mode 100644
index 0000000..5b9aa14
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/ClientsTableTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.db.virtual;
+
+import java.net.InetAddress;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import org.apache.cassandra.cql3.CQLTester;
+import org.assertj.core.api.Assertions;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ClientsTableTest extends CQLTester
+{
+ private static final String KS_NAME = "vts";
+
+ private ClientsTable table;
+
+ @BeforeClass
+ public static void setUpClass()
+ {
+ CQLTester.setUpClass();
+ }
+
+ @Before
+ public void config()
+ {
+ table = new ClientsTable(KS_NAME);
+ VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME,
ImmutableList.of(table)));
+ }
+
+ @Test
+ public void testSelectAll() throws Throwable
+ {
+ ResultSet result = executeNet("SELECT * FROM vts.clients");
+
+ for (Row r : result)
+ {
+ Assert.assertEquals(InetAddress.getLoopbackAddress(),
r.getInet("address"));
+ r.getInt("port");
+ Assert.assertTrue(r.getInt("port") > 0);
+ Assert.assertNotNull(r.getMap("client_options", String.class,
String.class));
+ Assert.assertTrue(r.getLong("request_count") > 0 );
+ // the following are questionable if they belong here
+ Assert.assertEquals("localhost", r.getString("hostname"));
+ Assertions.assertThat(r.getMap("client_options", String.class,
String.class))
+ .hasEntrySatisfying("DRIVER_VERSION", value ->
assertThat(value.contains(r.getString("driver_name"))))
+ .hasEntrySatisfying("DRIVER_VERSION", value ->
assertThat(value.contains(r.getString("driver_version"))));
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
b/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
new file mode 100644
index 0000000..9869629
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/ClientStatsTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+
+import com.datastax.driver.core.ResultSet;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.service.CassandraDaemon;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.tools.ToolRunner;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.assertj.core.groups.Tuple;
+
+public class ClientStatsTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Throwable
+ {
+ CassandraDaemon daemon = new CassandraDaemon();
+ requireNetwork();
+ startJMXServer();
+ daemon.activate();
+ daemon.startNativeTransport();
+ StorageService.instance.registerDaemon(daemon);
+ }
+
+ @Before
+ public void config() throws Throwable
+ {
+ ResultSet result = executeNet("select release_version from
system.local");
+ }
+
+ @Test
+ public void testClientStatsHelp()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help",
"clientstats");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool clientstats - Print information
about connected clients\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>)] clientstats [--all]\n" +
+ " [--by-protocol] [--clear-history]\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " --all\n" +
+ " Lists all connections\n" +
+ "\n" +
+ " --by-protocol\n" +
+ " Lists most recent client connections by
protocol version\n" +
+ "\n" +
+ " --clear-history\n" +
+ " Clear the history of connected clients\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" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testClientStats()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats");
+ tool.assertOnCleanExit();
+ String stdout = tool.getStdout();
+ assertThat(stdout).contains("Total connected clients: 2");
+ assertThat(stdout).contains("User Connections");
+ assertThat(stdout).contains("anonymous 2");
+ }
+
+ @Test
+ public void testClientStatsByProtocol()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats",
"--by-protocol");
+ tool.assertOnCleanExit();
+ String stdout = tool.getStdout();
+ assertThat(stdout).contains("Clients by protocol version");
+ assertThat(stdout).contains("Protocol-Version IP-Address Last-Seen");
+ assertThat(stdout).containsPattern("[0-9]/v[0-9] +/127.0.0.1
[a-zA-Z]{3} [0-9]+, [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}");
+ }
+
+ @Test
+ public void testClientStatsAll()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats",
"--all");
+ tool.assertOnCleanExit();
+ String stdout = tool.getStdout();
+ assertThat(stdout).containsPattern("Address +SSL +Cipher +Protocol
+Version +User +Keyspace +Requests +Driver-Name +Driver-Version
+Client-Options");
+ assertThat(stdout).containsPattern("/127.0.0.1:[0-9]+ false undefined
undefined [0-9]+ +anonymous +[0-9]+ +DataStax Java Driver 3.11.0");
+ assertThat(stdout).containsPattern("DRIVER_NAME=DataStax Java Driver");
+ assertThat(stdout).containsPattern("DRIVER_VERSION=3.11.0");
+ assertThat(stdout).containsPattern("CQL_VERSION=3.0.0");
+ assertThat(stdout).contains("Total connected clients: 2");
+ assertThat(stdout).contains("User Connections");
+ assertThat(stdout).contains("anonymous 2");
+ }
+
+ @Test
+ public void testClientStatsClearHistory()
+ {
+ ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
+ Logger ssLogger = (Logger)
LoggerFactory.getLogger(StorageService.class);
+
+ ssLogger.addAppender(listAppender);
+ listAppender.start();
+
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("clientstats",
"--clear-history");
+ tool.assertOnCleanExit();
+ String stdout = tool.getStdout();
+ assertThat(stdout).contains("Clearing connection history");
+ assertThat(listAppender.list)
+ .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
+ .contains(Tuple.tuple("Cleared connection history",
Level.INFO));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]