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 <r...@users.noreply.github.com>
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: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to