diff --git a/src/java/org/apache/cassandra/metrics/CQLMetrics.java b/src/java/org/apache/cassandra/metrics/CQLMetrics.java index e7b6ec57d18b..4a134c9a4238 100644 --- a/src/java/org/apache/cassandra/metrics/CQLMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CQLMetrics.java @@ -39,6 +39,7 @@ public class CQLMetrics public final Gauge preparedStatementsCount; public final Gauge preparedStatementsRatio; public final Gauge preparedStatementsCacheSize; + public final Gauge preparedStatementsCacheCapacity; public CQLMetrics() { @@ -67,5 +68,6 @@ public double getDenominator() } }); preparedStatementsCacheSize = Metrics.register(factory.createMetricName("PreparedStatementsCacheSize"), QueryProcessor::preparedStatementsCacheMemoryUsedBytes); + preparedStatementsCacheCapacity = Metrics.register(factory.createMetricName("PreparedStatementsCacheCapacity"), () -> QueryProcessor.PREPARED_STATEMENT_CACHE_SIZE_BYTES); } } diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index c60e0427e59b..c6e774f5aaea 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -44,11 +44,16 @@ import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; +import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; import javax.management.JMX; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.management.ReflectionException; import javax.management.openmbean.CompositeData; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.TabularData; @@ -112,7 +117,9 @@ import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.locator.LocationInfoMBean; import org.apache.cassandra.metrics.CIDRAuthorizerMetrics; +import org.apache.cassandra.metrics.CQLMetrics; import org.apache.cassandra.metrics.CassandraMetricsRegistry; +import org.apache.cassandra.metrics.DefaultNameFactory; import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.ThreadPoolMetrics; @@ -1939,6 +1946,31 @@ public Object getBufferPoolMetric(String poolType, String metricName) } } + /** + * Retrieve a CQL metric value by name. Works generically for any metric registered under + * {@code org.apache.cassandra.metrics:type=CQL,name=} by inspecting the MBean + * attributes at runtime, without requiring knowledge of the underlying metric type. + */ + public Object getCQLMetric(String metricName) + { + try + { + ObjectName objectName = new ObjectName(DefaultNameFactory.GROUP_NAME + ":type=" + CQLMetrics.TYPE_NAME + ",name=" + metricName); + for (MBeanAttributeInfo attr : mbeanServerConn.getMBeanInfo(objectName).getAttributes()) + { + String name = attr.getName(); + if ("Value".equals(name) || "Count".equals(name)) + return mbeanServerConn.getAttribute(objectName, name); + } + throw new RuntimeException("No readable value attribute for CQL metric: " + metricName); + } + catch (MalformedObjectNameException | InstanceNotFoundException | IntrospectionException | + ReflectionException | AttributeNotFoundException | MBeanException | IOException e) + { + throw new RuntimeException(e); + } + } + private static Multimap getJmxThreadPools(MBeanServerConnection mbeanServerConn) { try diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java index b0b2dff019ab..381b3ec44a47 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Info.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java @@ -119,6 +119,23 @@ public void execute(NodeProbe probe) probe.getCacheMetric("CounterCache", "HitRate"), cacheService.getCounterCacheSavePeriodInSeconds()); + // Prepared Statement Cache: entries, size, capacity, executions, evictions + try + { + out.printf("%-23s: entries %d, size %s, capacity %s, %d executions, %d evictions%n", + "Prepared Statement Cache", + probe.getCQLMetric("PreparedStatementsCount"), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), + probe.getCQLMetric("PreparedStatementsExecuted"), + probe.getCQLMetric("PreparedStatementsEvicted")); + } + catch (RuntimeException e) + { + if (!(e.getCause() instanceof InstanceNotFoundException)) + throw e; + } + // Chunk Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds try { diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java index 62e0dad3ea1d..78183f7d2bd8 100644 --- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java +++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java @@ -174,6 +174,12 @@ public Object getCompactionMetric(String metricName) throw new UnsupportedOperationException(); } + @Override + public Object getCQLMetric(String metricName) + { + throw new UnsupportedOperationException(); + } + @Override public Object getClientMetric(String metricName) { diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java new file mode 100644 index 000000000000..9097cb9229ca --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java @@ -0,0 +1,62 @@ +/* + * 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.regex.Matcher; +import java.util.regex.Pattern; + +import com.datastax.driver.core.PreparedStatement; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.tools.ToolRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InfoTest extends CQLTester +{ + private static final Pattern PREPARED_STATEMENT_CACHE_PATTERN = + Pattern.compile("Prepared Statement Cache\\s+: entries (\\d+), size ([^,]+), capacity ([^,]+), (\\d+) executions, (\\d+) evictions"); + + @BeforeClass + public static void setup() throws Exception + { + requireNetwork(); + startJMXServer(); + } + + @Test + public void testInfoContainsPreparedStatementCache() + { + createTable("CREATE TABLE %s (id int PRIMARY KEY, val text)"); + PreparedStatement preparedStatement = sessionNet().prepare("INSERT INTO " + KEYSPACE + '.' + currentTable() + " (id, val) VALUES (?, ?)"); + sessionNet().execute(preparedStatement.bind(1, "value1")); + + ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("info"); + tool.assertOnCleanExit(); + String stdout = tool.getStdout(); + assertThat(stdout).contains("Prepared Statement Cache"); + Matcher matcher = PREPARED_STATEMENT_CACHE_PATTERN.matcher(stdout); + assertThat(matcher.find()).isTrue(); + assertThat(Integer.parseInt(matcher.group(1))).isGreaterThan(0); + assertThat(matcher.group(2)).isNotEqualTo("0 bytes"); + assertThat(Integer.parseInt(matcher.group(4))).isGreaterThan(0); + } +}