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