You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by mm...@apache.org on 2024/04/03 18:09:26 UTC

(cassandra) branch trunk updated: Expose all dropwizard metrics in virtual tables

This is an automated email from the ASF dual-hosted git repository.

mmuzaf 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 2e7def7626 Expose all dropwizard metrics in virtual tables
2e7def7626 is described below

commit 2e7def7626fe299318e0cf4c2bf8138a6c31e149
Author: Maxim Muzafarov <ma...@gmail.com>
AuthorDate: Wed Apr 3 17:07:14 2024 +0200

    Expose all dropwizard metrics in virtual tables
    
    patch by Maxim Muzafarov; reviewed by Chris Lohfink, Stefan Miklosovic for CASSANDRA-14572
---
 CHANGES.txt                                        |   1 +
 .../pages/managing/operating/metrics.adoc          |  10 +-
 .../pages/managing/operating/virtualtables.adoc    |  60 ++-
 pylib/cqlshlib/cql3handling.py                     |   5 +-
 pylib/cqlshlib/test/test_cqlsh_completion.py       |   8 +-
 .../cassandra/db/virtual/BatchMetricsTable.java    |  14 +-
 .../cassandra/db/virtual/CQLMetricsTable.java      |  11 +-
 .../db/virtual/CollectionVirtualTableAdapter.java  | 582 +++++++++++++++++++++
 .../cassandra/db/virtual/SystemViewsKeyspace.java  |  11 +-
 .../cassandra/db/virtual/ThreadPoolsTable.java     |  94 ----
 .../cassandra/db/virtual/VirtualKeyspace.java      |  12 +
 .../apache/cassandra/db/virtual/model/Column.java  |  64 +++
 .../virtual/model/CounterMetricRow.java}           |  38 +-
 .../virtual/model/GaugeMetricRow.java}             |  42 +-
 .../db/virtual/model/HistogramMetricRow.java       | 100 ++++
 .../cassandra/db/virtual/model/MeterMetricRow.java |  81 +++
 .../virtual/model/MetricGroupRow.java}             |  34 +-
 .../cassandra/db/virtual/model/MetricRow.java      |  80 +++
 .../cassandra/db/virtual/model/ThreadPoolRow.java  |  94 ++++
 .../cassandra/db/virtual/model/TimerMetricRow.java |  81 +++
 .../db/virtual/walker/CounterMetricRowWalker.java  |  61 +++
 .../db/virtual/walker/GaugeMetricRowWalker.java    |  61 +++
 .../virtual/walker/HistogramMetricRowWalker.java   |  75 +++
 .../db/virtual/walker/MeterMetricRowWalker.java    |  69 +++
 .../db/virtual/walker/MetricGroupRowWalker.java    |  58 ++
 .../db/virtual/walker/MetricRowWalker.java         |  63 +++
 .../cassandra/db/virtual/walker/RowWalker.java     |  87 +++
 .../db/virtual/walker/ThreadPoolRowWalker.java     |  75 +++
 .../db/virtual/walker/TimerMetricRowWalker.java    |  69 +++
 .../index/sai/metrics/AbstractMetrics.java         |   4 +-
 .../io/sstable/format/big/RowIndexEntry.java       |   3 +-
 .../org/apache/cassandra/metrics/BatchMetrics.java |   4 +-
 .../cassandra/metrics/BufferPoolMetrics.java       |   3 +-
 .../metrics/CASClientWriteRequestMetrics.java      |   4 +-
 .../cassandra/metrics/CIDRAuthorizerMetrics.java   |   3 +-
 .../org/apache/cassandra/metrics/CQLMetrics.java   |   3 +-
 .../org/apache/cassandra/metrics/CacheMetrics.java |   3 +-
 .../metrics/CassandraMetricsRegistry.java          | 559 ++++++++++++++++----
 .../cassandra/metrics/ChunkCacheMetrics.java       |   3 +-
 .../metrics/ClientMessageSizeMetrics.java          |   2 +-
 .../apache/cassandra/metrics/ClientMetrics.java    |   7 +-
 .../cassandra/metrics/ClientRequestMetrics.java    |   3 +-
 .../metrics/ClientRequestSizeMetrics.java          |   2 +-
 .../apache/cassandra/metrics/CommitLogMetrics.java |   3 +-
 .../cassandra/metrics/CompactionMetrics.java       |   3 +-
 .../apache/cassandra/metrics/DenylistMetrics.java  |   3 +-
 .../cassandra/metrics/DroppedMessageMetrics.java   |   2 +-
 .../cassandra/metrics/HintedHandoffMetrics.java    |   3 +-
 .../cassandra/metrics/HintsServiceMetrics.java     |   3 +-
 .../cassandra/metrics/InternodeInboundMetrics.java |   3 +-
 .../metrics/InternodeOutboundMetrics.java          |   2 +-
 .../apache/cassandra/metrics/KeyspaceMetrics.java  |   5 +-
 .../apache/cassandra/metrics/LatencyMetrics.java   |  17 +-
 .../apache/cassandra/metrics/MessagingMetrics.java |   3 +-
 .../apache/cassandra/metrics/MutualTlsMetrics.java |   3 +-
 .../org/apache/cassandra/metrics/PaxosMetrics.java |   3 +-
 .../cassandra/metrics/ReadRepairMetrics.java       |   3 +-
 .../apache/cassandra/metrics/StorageMetrics.java   |   1 +
 .../apache/cassandra/metrics/StreamingMetrics.java |   2 +-
 .../org/apache/cassandra/metrics/TCMMetrics.java   |   3 +-
 .../org/apache/cassandra/metrics/TableMetrics.java |  50 +-
 .../cassandra/metrics/ThreadPoolMetrics.java       |   7 +-
 .../cassandra/metrics/TrieMemtableMetricsView.java |  11 +-
 .../cassandra/metrics/UnweightedCacheMetrics.java  |   3 +-
 .../apache/cassandra/schema/SchemaConstants.java   |   1 +
 .../apache/cassandra/service/CassandraDaemon.java  |   8 +-
 .../apache/cassandra/utils/EstimatedHistogram.java |   6 +
 .../org/apache/cassandra/utils/FBUtilities.java    |   3 +
 .../cassandra/utils/memory/MemtablePool.java       |   4 +-
 test/unit/org/apache/cassandra/cql3/CQLTester.java | 108 +++-
 .../virtual/CollectionVirtualTableAdapterTest.java | 243 +++++++++
 .../db/virtual/model/CollectionEntry.java          | 126 +++++
 .../db/virtual/model/CollectionEntryTestRow.java   |  93 ++++
 .../db/virtual/model/PartitionEntryTestRow.java    |  40 +-
 .../walker/CollectionEntryTestRowWalker.java       |  75 +++
 .../walker/PartitionEntryTestRowWalker.java        |  61 +++
 .../apache/cassandra/metrics/BatchMetricsTest.java |  11 +
 .../apache/cassandra/metrics/CQLMetricsTest.java   |  21 +
 .../metrics/CassandraMetricsRegistryTest.java      | 117 ++++-
 .../metrics/ClientRequestMetricsTest.java          |  33 +-
 .../ClientRequestRowAndColumnMetricsTest.java      |   8 +-
 .../metrics/JmxVirtualTableMetricsTest.java        | 336 ++++++++++++
 .../cassandra/metrics/KeyspaceMetricsTest.java     |  34 ++
 .../cassandra/metrics/LatencyMetricsTest.java      |  10 +-
 .../apache/cassandra/metrics/TableMetricsTest.java |  21 +
 .../cassandra/metrics/TrieMemtableMetricsTest.java |  25 +-
 .../apache/cassandra/utils/FBUtilitiesTest.java    |   3 +-
 87 files changed, 3810 insertions(+), 353 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 1c5f1766fd..3703a0bf04 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 5.1
+ * Expose all dropwizard metrics in virtual tables (CASSANDRA-14572)
  * Ensured that PropertyFileSnitchTest do not overwrite cassandra-toploogy.properties (CASSANDRA-19502)
  * Add option for MutualTlsAuthenticator to restrict the certificate validity period (CASSANDRA-18951)
  * Fix StorageService::constructRangeToEndpointMap for non-distributed keyspaces (CASSANDRA-19255)
diff --git a/doc/modules/cassandra/pages/managing/operating/metrics.adoc b/doc/modules/cassandra/pages/managing/operating/metrics.adoc
index c4392fea82..78dbd16586 100644
--- a/doc/modules/cassandra/pages/managing/operating/metrics.adoc
+++ b/doc/modules/cassandra/pages/managing/operating/metrics.adoc
@@ -792,22 +792,24 @@ Reported name format:
 *Metric Name*::
   `org.apache.cassandra.metrics.Storage.<MetricName>`
 *JMX MBean*::
-  `org.apache.cassandra.metrics:type=Storage name=<MetricName>`
+  `org.apache.cassandra.metrics:type=Storage,name=<MetricName>`
 
 [cols=",,",options="header",]
 |===
 |Name |Type |Description
 |Exceptions |Counter |Number of internal exceptions caught. Under normal
 exceptions this should be zero.
-
 |Load |Counter |Size, in bytes, of the on disk data size this node
 manages.
-
 |TotalHints |Counter |Number of hint messages written to this node since
 [re]start. Includes one entry for each host to be hinted per hint.
-
 |TotalHintsInProgress |Counter |Number of hints attemping to be sent
 currently.
+|BootstrapFilesReceived | Gauage<Long> | Number of files received during bootstrap.
+|BootstrapFilesTotal | Gauge<Long> | Total number of files to be received during bootstrap.
+|BootstrapFilesThroughput | Meter | Throughput of files received during bootstrap.
+|BootstrapLastSeenError | Gauge<String> | Last error message seen during bootstrap.
+|BootstrapLastSeenStatus | Gauge<String> | Last status message seen during bootstrap.
 |===
 
 [[handoff-metrics]]
diff --git a/doc/modules/cassandra/pages/managing/operating/virtualtables.adoc b/doc/modules/cassandra/pages/managing/operating/virtualtables.adoc
index f4a5608507..d3b948e3d1 100644
--- a/doc/modules/cassandra/pages/managing/operating/virtualtables.adoc
+++ b/doc/modules/cassandra/pages/managing/operating/virtualtables.adoc
@@ -24,10 +24,11 @@ Since a virtual table is not replicated the partitioner sorts in order of partit
 
 == Virtual Keyspaces
 
-Apache Cassandra 4.0 has added two new keyspaces for virtual tables:
+Apache Cassandra has the following virtual keyspaces:
 
 * `system_virtual_schema`
-* `system_views`.
+* `system_views`
+* `system_metrics`
 
 The `system_virtual_schema` keyspace has three tables: `keyspaces`,
 `columns` and `tables` for the virtual keyspace, table, and column definitions, respectively.
@@ -36,6 +37,10 @@ It is used by Cassandra internally and a user should not access it directly.
 
 The `system_views` keyspace contains the actual virtual tables.
 
+The `system_metrics` keyspace contains the virtual tables of all the metrics available in Apache Cassandra
+which can be used to monitor the health of the cluster. These metrics are split into several tables each
+containing a different set of metrics corresponding to the type of metric or the component of the system.
+
 == Virtual Table Limitations
 
 Before discussing virtual keyspaces and tables, note that virtual keyspaces and tables have some limitations.
@@ -55,6 +60,57 @@ In fact, mutations for virtual and regular tables cannot occur in the same batch
 
 == Virtual Tables
 
+=== Virtual Tables _system_metrics_ Keyspace
+
+The `system_metrics` virtual keyspace contains the virtual tables of all the metrics available in Apache Cassandra.
+Currently, the `system_metrics` keyspace contains the following virtual tables (subject to change), which can be found
+in the `system_metrics.all_groups` virtual table:
+
+[source, console]
+----
+cqlsh> select * from system_metrics.all_groups  ;
+
+ group_name        | virtual_table
+-------------------+---------------------------
+             Batch |               batch_group
+        BufferPool |         buffer_pool_group
+    CIDRAuthorizer |     cidr_authorizer_group
+               CQL |                 cql_group
+             Cache |               cache_group
+        ChunkCache |         chunk_cache_group
+            Client |              client_group
+ ClientMessageSize | client_message_size_group
+     ClientRequest |      client_request_group
+ ClientRequestSize | client_request_size_group
+      ColumnFamily |       column_family_group
+         CommitLog |          commit_log_group
+        Compaction |          compaction_group
+        Connection |          connection_group
+    DroppedMessage |     dropped_message_group
+      HintsService |       hints_service_group
+             Index |               index_group
+ IndexColumnFamily | index_column_family_group
+        IndexTable |         index_table_group
+  InternodeInbound |   internode_inbound_group
+          Keyspace |            keyspace_group
+      MemtablePool |       memtable_pool_group
+         Messaging |           messaging_group
+             Paxos |               paxos_group
+        ReadRepair |         read_repair_group
+            Repair |              repair_group
+           Storage |             storage_group
+      StorageProxy |       storage_proxy_group
+         Streaming |           streaming_group
+               TCM |                 tcm_group
+             Table |               table_group
+       ThreadPools |        thread_pools_group
+      TrieMemtable |       trie_memtable_group
+   UnweightedCache |    unweighted_cache_group
+
+----
+
+=== Virtual Tables _system_views_ Keyspace
+
 Each of the virtual tables in the `system_views` virtual keyspace contain different information.
 
 The following table describes the virtual tables:
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index f795ee7dd0..c774da3c78 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -34,8 +34,9 @@ class UnexpectedTableStructure(UserWarning):
 
 
 SYSTEM_KEYSPACES = ('system', 'system_schema', 'system_traces', 'system_auth', 'system_distributed', 'system_views',
-                    'system_virtual_schema', 'system_cluster_metadata')
-NONALTERBALE_KEYSPACES = ('system', 'system_schema', 'system_views', 'system_virtual_schema', 'system_cluster_metadata')
+                    'system_metrics', 'system_virtual_schema', 'system_cluster_metadata')
+NONALTERBALE_KEYSPACES = ('system', 'system_schema', 'system_views', 'system_metrics', 'system_virtual_schema',
+                          'system_cluster_metadata')
 
 
 class Cql3ParsingRuleSet(CqlParsingRuleSet):
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index 793dcd7a8b..0f01aa5286 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -588,7 +588,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
         self.trycompletions('CREATE TYPE foo ', choices=['(', '.'])
 
     def test_complete_in_drop_type(self):
-        self.trycompletions('DROP TYPE ', choices=['IF', 'system_views.',
+        self.trycompletions('DROP TYPE ', choices=['IF', 'system_views.', 'system_metrics.',
                                                     'tags', 'system_traces.', 'system_distributed.', 'system_cluster_metadata.',
                                                     'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.',
                                                     'system_auth.', 'system_virtual_schema.', self.cqlsh.keyspace + '.'
@@ -895,7 +895,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
 
     def test_complete_in_use(self):
         self.trycompletions('US', immediate='E ')
-        self.trycompletions('USE ', choices=[self.cqlsh.keyspace, 'system', 'system_auth',
+        self.trycompletions('USE ', choices=[self.cqlsh.keyspace, 'system', 'system_auth', 'system_metrics',
                                            'system_distributed', 'system_schema', 'system_traces', 'system_views',
                                            'system_virtual_schema', 'system_cluster_metadata' ])
 
@@ -991,7 +991,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
                                                      'dynamic_columns',
                                                      'twenty_rows_composite_table',
                                                      'utf8_with_special_chars',
-                                                     'system_traces.', 'songs', 'system_views.',
+                                                     'system_traces.', 'songs', 'system_views.', 'system_metrics.',
                                                      'system_virtual_schema.',
                                                      'system_schema.', 'system_distributed.',
                                                      'system_cluster_metadata.',
@@ -1020,7 +1020,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
     def test_complete_in_alter_type(self):
         self.trycompletions('ALTER TYPE I', immediate='F EXISTS ')
         self.trycompletions('ALTER TYPE ', choices=['IF', 'system_views.',
-                                                    'tags', 'system_traces.', 'system_distributed.',
+                                                    'tags', 'system_traces.', 'system_distributed.', 'system_metrics.',
                                                     'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.',
                                                     'system_auth.', 'system_virtual_schema.', 'system_cluster_metadata.', self.cqlsh.keyspace + '.'
                                                     ])
diff --git a/src/java/org/apache/cassandra/db/virtual/BatchMetricsTable.java b/src/java/org/apache/cassandra/db/virtual/BatchMetricsTable.java
index 948f2a1e9c..d712566388 100644
--- a/src/java/org/apache/cassandra/db/virtual/BatchMetricsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/BatchMetricsTable.java
@@ -26,10 +26,16 @@ import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.dht.LocalPartitioner;
 import org.apache.cassandra.metrics.BatchMetrics;
 import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientWarn;
 
+/**
+ * @deprecated The "batch_metrics" virtual table is no longer used, and will be removed in a future release. Please,
+ * use either "system_metrics.batch_group", or "system_metrics.type_histogram" virtual tables instead.
+ */
+@Deprecated(since = "5.1")
 public class BatchMetricsTable extends AbstractVirtualTable
 {
-
+    protected static final String TABLE_NAME = "batch_metrics";
     private static final String PARTITIONS_PER_LOGGED_BATCH = "partitions_per_logged_batch";
     private static final String PARTITIONS_PER_UNLOGGED_BATCH = "partitions_per_unlogged_batch";
     private static final String PARTITIONS_PER_COUNTER_BATCH = "partitions_per_counter_batch";
@@ -40,7 +46,7 @@ public class BatchMetricsTable extends AbstractVirtualTable
 
     BatchMetricsTable(String keyspace)
     {
-        super(TableMetadata.builder(keyspace, "batch_metrics")
+        super(TableMetadata.builder(keyspace, TABLE_NAME)
                            .comment("Metrics specific to batch statements")
                            .kind(TableMetadata.Kind.VIRTUAL)
                            .partitioner(new LocalPartitioner(UTF8Type.instance))
@@ -61,6 +67,10 @@ public class BatchMetricsTable extends AbstractVirtualTable
         addRow(result, PARTITIONS_PER_UNLOGGED_BATCH, metrics.partitionsPerUnloggedBatch.getSnapshot());
         addRow(result, PARTITIONS_PER_COUNTER_BATCH, metrics.partitionsPerCounterBatch.getSnapshot());
 
+        ClientWarn.instance.warn(String.format("The \"%s\" virtual table is deprecated. " +
+                                               "Please, use either \"%s.batch_group\", or \"%s.type_histogram\" " +
+                                               "virtual tables instead.",
+                                               TABLE_NAME, metadata.keyspace, metadata.keyspace));
         return result;
     }
 
diff --git a/src/java/org/apache/cassandra/db/virtual/CQLMetricsTable.java b/src/java/org/apache/cassandra/db/virtual/CQLMetricsTable.java
index acd8947e2f..d86a393953 100644
--- a/src/java/org/apache/cassandra/db/virtual/CQLMetricsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/CQLMetricsTable.java
@@ -20,14 +20,19 @@ package org.apache.cassandra.db.virtual;
 
 import com.google.common.annotations.VisibleForTesting;
 
+import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.db.marshal.DoubleType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.dht.LocalPartitioner;
 import org.apache.cassandra.metrics.CQLMetrics;
 import org.apache.cassandra.schema.TableMetadata;
-import org.apache.cassandra.cql3.QueryProcessor;
-
+import org.apache.cassandra.service.ClientWarn;
 
+/**
+ * @deprecated The "cql_metrics" virtual table is no longer used, and will be removed in a future release. Please,
+ * use "system_metrics.cql_group" virtual table instead.
+ */
+@Deprecated(since = "5.1")
 final class CQLMetricsTable extends AbstractVirtualTable
 {
     public static final String TABLE_NAME = "cql_metrics";
@@ -70,6 +75,8 @@ final class CQLMetricsTable extends AbstractVirtualTable
         addRow(result, PREPARED_STATEMENTS_RATIO, cqlMetrics.preparedStatementsRatio.getValue());
         addRow(result, REGULAR_STATEMENTS_EXECUTED, cqlMetrics.regularStatementsExecuted.getCount());
 
+        ClientWarn.instance.warn("The \"" + TABLE_NAME + "\" virtual table is deprecated. " +
+                                 "Please, use \"system_metrics.cql_group\" virtual table instead.");
         return result;
     }
 
diff --git a/src/java/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapter.java b/src/java/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapter.java
new file mode 100644
index 0000000000..56eacf5b3e
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapter.java
@@ -0,0 +1,582 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DataRange;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.EmptyIterators;
+import org.apache.cassandra.db.filter.ClusteringIndexFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.ByteType;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.DoubleType;
+import org.apache.cassandra.db.marshal.FloatType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.ShortType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UUIDType;
+import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.AbstractUnfilteredRowIterator;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.EncodingStats;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.Rows;
+import org.apache.cassandra.db.rows.Unfiltered;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.walker.RowWalker;
+import org.apache.cassandra.dht.LocalPartitioner;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Pair;
+
+import static org.apache.cassandra.db.rows.Cell.NO_DELETION_TIME;
+import static org.apache.cassandra.utils.FBUtilities.camelToSnake;
+
+/**
+ * This is a virtual table that iteratively builds rows using a data set provided by internal collection.
+ * Some metric views might be too large to fit in memory, for example, virtual tables that contain metrics
+ * for all the keyspaces registered in the cluster. Such a technique is also facilitates keeping the low
+ * memory footprint of the virtual tables in general.
+ * <p>
+ * It doesn't require the input data set to be sorted, but it does require that the partition keys are
+ * provided in the order of the partitioner of the table metadata.
+ */
+public class CollectionVirtualTableAdapter<R> implements VirtualTable
+{
+    private static final Pattern ONLY_ALPHABET_PATTERN = Pattern.compile("[^a-zA-Z1-9]");
+    private static final List<Pair<String, String>> knownAbbreviations = Arrays.asList(Pair.create("CAS", "Cas"),
+                                                                                       Pair.create("CIDR", "Cidr"));
+
+    /** The map of the supported converters for the column types. */
+    private static final Map<Class<?>, ? extends AbstractType<?>> converters =
+        ImmutableMap.<Class<?>, AbstractType<?>>builder()
+                    .put(String.class, UTF8Type.instance)
+                    .put(Integer.class, Int32Type.instance)
+                    .put(Integer.TYPE, Int32Type.instance)
+                    .put(Long.class, LongType.instance)
+                    .put(Long.TYPE, LongType.instance)
+                    .put(Float.class, FloatType.instance)
+                    .put(Float.TYPE, FloatType.instance)
+                    .put(Double.class, DoubleType.instance)
+                    .put(Double.TYPE, DoubleType.instance)
+                    .put(Boolean.class, BooleanType.instance)
+                    .put(Boolean.TYPE, BooleanType.instance)
+                    .put(Byte.class, ByteType.instance)
+                    .put(Byte.TYPE, ByteType.instance)
+                    .put(Short.class, ShortType.instance)
+                    .put(Short.TYPE, ShortType.instance)
+                    .put(UUID.class, UUIDType.instance)
+                    .build();
+
+    /**
+     * The map is used to avoid getting column metadata for each regular column for each row.
+     */
+    private final ConcurrentHashMap<String, ColumnMetadata> columnMetas = new ConcurrentHashMap<>();
+    private final RowWalker<R> walker;
+    private final Iterable<R> data;
+    private final Function<DecoratedKey, R> decorateKeyToRowExtractor;
+    private final TableMetadata metadata;
+
+    private CollectionVirtualTableAdapter(String keySpaceName,
+                                          String tableName,
+                                          String description,
+                                          RowWalker<R> walker,
+                                          Iterable<R> data)
+    {
+        this(keySpaceName, tableName, description, walker, data, null);
+    }
+
+    private CollectionVirtualTableAdapter(String keySpaceName,
+                                          String tableName,
+                                          String description,
+                                          RowWalker<R> walker,
+                                          Iterable<R> data,
+                                          Function<DecoratedKey, R> keyToRowExtractor)
+    {
+        this.walker = walker;
+        this.data = data;
+        this.metadata = buildMetadata(keySpaceName, tableName, description, walker);
+        this.decorateKeyToRowExtractor = keyToRowExtractor;
+    }
+
+    public static <C, R> CollectionVirtualTableAdapter<R> create(String keySpaceName,
+                                                                 String rawTableName,
+                                                                 String description,
+                                                                 RowWalker<R> walker,
+                                                                 Iterable<C> container,
+                                                                 Function<C, R> rowFunc)
+    {
+        return new CollectionVirtualTableAdapter<>(keySpaceName,
+                                                   virtualTableNameStyle(rawTableName),
+                                                   description,
+                                                   walker,
+                                                   () -> StreamSupport.stream(container.spliterator(), false)
+                                                                      .map(rowFunc).iterator());
+    }
+
+    public static <K, C, R> CollectionVirtualTableAdapter<R> createSinglePartitionedKeyFiltered(String keySpaceName,
+                                                                                                String rawTableName,
+                                                                                                String description,
+                                                                                                RowWalker<R> walker,
+                                                                                                Map<K, C> map,
+                                                                                                Predicate<K> mapKeyFilter,
+                                                                                                BiFunction<K, C, R> rowConverter)
+    {
+        return createSinglePartitioned(keySpaceName, rawTableName, description, walker, map, mapKeyFilter,
+                                       Objects::nonNull, rowConverter);
+    }
+
+    public static <K, C, R> CollectionVirtualTableAdapter<R> createSinglePartitionedValueFiltered(String keySpaceName,
+                                                                                                  String rawTableName,
+                                                                                                  String description,
+                                                                                                  RowWalker<R> walker,
+                                                                                                  Map<K, C> map,
+                                                                                                  Predicate<C> mapValueFilter,
+                                                                                                  BiFunction<K, C, R> rowConverter)
+    {
+        return createSinglePartitioned(keySpaceName, rawTableName, description, walker, map, key -> true,
+                                       mapValueFilter, rowConverter);
+    }
+
+    private static <K, C, R> CollectionVirtualTableAdapter<R> createSinglePartitioned(String keySpaceName,
+                                                                                      String rawTableName,
+                                                                                      String description,
+                                                                                      RowWalker<R> walker,
+                                                                                      Map<K, C> map,
+                                                                                      Predicate<K> mapKeyFilter,
+                                                                                      Predicate<C> mapValueFilter,
+                                                                                      BiFunction<K, C, R> rowConverter)
+    {
+        assert walker.count(Column.Type.PARTITION_KEY) == 1 : "Partition key must be a single column";
+        assert walker.count(Column.Type.CLUSTERING) == 0 : "Clustering columns are not supported";
+
+        AtomicReference<Class<?>> partitionKeyClass = new AtomicReference<>();
+        walker.visitMeta(new RowWalker.MetadataVisitor()
+        {
+            @Override
+            public <T> void accept(Column.Type type, String columnName, Class<T> clazz)
+            {
+                if (type == Column.Type.PARTITION_KEY)
+                    partitionKeyClass.set(clazz);
+            }
+        });
+
+        return new CollectionVirtualTableAdapter<>(keySpaceName,
+                                                   virtualTableNameStyle(rawTableName),
+                                                   description,
+                                                   walker,
+                                                   () -> map.entrySet()
+                                                            .stream()
+                                                            .filter(e -> mapKeyFilter.test(e.getKey()))
+                                                            .filter(e -> mapValueFilter.test(e.getValue()))
+                                                            .map(e -> rowConverter.apply(e.getKey(), e.getValue()))
+                                                            .iterator(),
+                                                   decoratedKey ->
+                                                   {
+                                                       K partitionKey = compose(converters.get(partitionKeyClass.get()),
+                                                                                decoratedKey.getKey());
+                                                       boolean keyRequired = mapKeyFilter.test(partitionKey);
+                                                       if (!keyRequired)
+                                                           return null;
+
+                                                       C value = map.get(partitionKey);
+                                                       return mapValueFilter.test(value) ? rowConverter.apply(
+                                                           partitionKey, value) : null;
+                                                   });
+    }
+
+    public static String virtualTableNameStyle(String camel)
+    {
+        // Process sub names in the full metrics group name separately and then join them.
+        // For example: "ClientRequest.Write-EACH_QUORUM" will be converted to "client_request_write_each_quorum".
+        String[] subNames = ONLY_ALPHABET_PATTERN.matcher(camel).replaceAll(".").split("\\.");
+        return Arrays.stream(subNames)
+                     .map(CollectionVirtualTableAdapter::camelToSnakeWithAbbreviations)
+                     .reduce((a, b) -> a + '_' + b)
+                     .orElseThrow(() -> new IllegalArgumentException("Invalid table name: " + camel));
+    }
+
+    private static String camelToSnakeWithAbbreviations(String camel)
+    {
+        Pattern pattern = Pattern.compile("^[A-Z1-9_]+$");
+        // Contains only uppercase letters, numbers and underscores, so it's already snake case.
+        if (pattern.matcher(camel).matches())
+            return camel.toLowerCase();
+
+        // Some special cases must be handled manually.
+        String modifiedCamel = camel;
+        for (Pair<String, String> replacement : knownAbbreviations)
+            modifiedCamel = modifiedCamel.replace(replacement.left, replacement.right);
+
+        return camelToSnake(modifiedCamel);
+    }
+
+    private TableMetadata buildMetadata(String keyspaceName, String tableName, String description, RowWalker<R> walker)
+    {
+        TableMetadata.Builder builder = TableMetadata.builder(keyspaceName, tableName)
+                                                     .comment(description)
+                                                     .kind(TableMetadata.Kind.VIRTUAL);
+
+        List<AbstractType<?>> partitionKeyTypes = new ArrayList<>(walker.count(Column.Type.PARTITION_KEY));
+        walker.visitMeta(new RowWalker.MetadataVisitor()
+        {
+            @Override
+            public <T> void accept(Column.Type type, String columnName, Class<T> clazz)
+            {
+                switch (type)
+                {
+                    case PARTITION_KEY:
+                        partitionKeyTypes.add(converters.get(clazz));
+                        builder.addPartitionKeyColumn(columnName, converters.get(clazz));
+                        break;
+                    case CLUSTERING:
+                        builder.addClusteringColumn(columnName, converters.get(clazz));
+                        break;
+                    case REGULAR:
+                        builder.addRegularColumn(columnName, converters.get(clazz));
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown column type: " + type);
+                }
+            }
+        });
+
+        if (partitionKeyTypes.size() == 1)
+            builder.partitioner(new LocalPartitioner(partitionKeyTypes.get(0)));
+        else if (partitionKeyTypes.size() > 1)
+            builder.partitioner(new LocalPartitioner(CompositeType.getInstance(partitionKeyTypes)));
+
+        return builder.build();
+    }
+
+    @Override
+    public UnfilteredPartitionIterator select(DecoratedKey partitionKey,
+                                              ClusteringIndexFilter clusteringFilter,
+                                              ColumnFilter columnFilter)
+    {
+        if (!data.iterator().hasNext())
+            return EmptyIterators.unfilteredPartition(metadata);
+
+        NavigableMap<Clustering<?>, Row> rows = new TreeMap<>(metadata.comparator);
+        Stream<CollectionRow> stream;
+        if (decorateKeyToRowExtractor == null)
+        {
+            // The benchmark shows that if we continuously read the data from the virtual table e.g. by metric name,
+            // then the parallel stream is slightly faster to get the first result, but for continuous reads it gives us
+            // a higher GC pressure. The sequential stream is slightly slower to get the first result, but it has the
+            // same throughput as the parallel stream, and it gives us less GC pressure.
+            // See the details in the benchmark: https://gist.github.com/Mmuzaf/80c73b7f9441ff21f6d22efe5746541a
+            stream = StreamSupport.stream(data.spliterator(), false)
+                                  .map(row -> makeRow(row, columnFilter))
+                                  .filter(cr -> partitionKey.equals(cr.key.get()))
+                                  .filter(cr -> clusteringFilter.selects(cr.clustering));
+        }
+        else
+        {
+            R row = decorateKeyToRowExtractor.apply(partitionKey);
+            if (row == null)
+                return EmptyIterators.unfilteredPartition(metadata);
+            stream = Stream.of(makeRow(row, columnFilter));
+        }
+
+        // If there are no clustering columns, we've found a unique partition that matches the partition key,
+        // so we can stop the stream without looping through all the rows.
+        if (walker.count(Column.Type.CLUSTERING) == 0)
+            stream.findFirst().ifPresent(cr -> rows.put(cr.clustering, cr.rowSup.get()));
+        else
+            stream.forEach(cr -> rows.put(cr.clustering, cr.rowSup.get()));
+
+        return new SingletonUnfilteredPartitionIterator(new DataRowUnfilteredIterator(partitionKey,
+                                                                                      clusteringFilter,
+                                                                                      columnFilter,
+                                                                                      rows));
+    }
+
+    @Override
+    public UnfilteredPartitionIterator select(DataRange dataRange, ColumnFilter columnFilter)
+    {
+        return createPartitionIterator(metadata, new AbstractIterator<>()
+        {
+            private final Iterator<? extends UnfilteredRowIterator> partitions = buildDataRangeIterator(dataRange,
+                                                                                                        columnFilter);
+
+            @Override
+            protected UnfilteredRowIterator computeNext()
+            {
+                return partitions.hasNext() ? partitions.next() : endOfData();
+            }
+
+            private Iterator<? extends UnfilteredRowIterator> buildDataRangeIterator(DataRange dataRange,
+                                                                                     ColumnFilter columnFilter)
+            {
+                NavigableMap<DecoratedKey, NavigableMap<Clustering<?>, Row>> partitionMap = new ConcurrentSkipListMap<>(DecoratedKey.comparator);
+                StreamSupport.stream(data.spliterator(), true)
+                             .map(row -> makeRow(row, columnFilter))
+                             .filter(cr -> dataRange.keyRange().contains(cr.key.get()))
+                             .forEach(cr -> partitionMap.computeIfAbsent(cr.key.get(),
+                                                                         key -> new TreeMap<>(metadata.comparator))
+                                                        .put(cr.clustering, cr.rowSup.get()));
+
+                return partitionMap.entrySet().stream().map(
+                    e -> new DataRowUnfilteredIterator(e.getKey(), dataRange.clusteringIndexFilter(e.getKey()), columnFilter,
+                                                       e.getValue())).iterator();
+            }
+        });
+    }
+
+    private class DataRowUnfilteredIterator extends AbstractUnfilteredRowIterator
+    {
+        private final Iterator<Row> rows;
+
+        public DataRowUnfilteredIterator(DecoratedKey partitionKey,
+                                         ClusteringIndexFilter indexFilter,
+                                         ColumnFilter columnFilter,
+                                         NavigableMap<Clustering<?>, Row> data)
+        {
+            super(CollectionVirtualTableAdapter.this.metadata,
+                  partitionKey,
+                  DeletionTime.LIVE,
+                  columnFilter.queriedColumns(),
+                  Rows.EMPTY_STATIC_ROW,
+                  indexFilter.isReversed(),
+                  EncodingStats.NO_STATS);
+            this.rows = indexFilter.isReversed() ? data.descendingMap().values().iterator() : data.values().iterator();
+        }
+
+        @Override
+        protected Unfiltered computeNext()
+        {
+            return rows.hasNext() ? rows.next() : endOfData();
+        }
+    }
+
+    private CollectionRow makeRow(R row, ColumnFilter columnFilter)
+    {
+        assert metadata.partitionKeyColumns().size() == walker.count(Column.Type.PARTITION_KEY) :
+            "Invalid number of partition key columns";
+        assert metadata.clusteringColumns().size() == walker.count(Column.Type.CLUSTERING) :
+            "Invalid number of clustering columns";
+
+        Map<Column.Type, Object[]> fiterable = new EnumMap<>(Column.Type.class);
+        fiterable.put(Column.Type.PARTITION_KEY, new Object[metadata.partitionKeyColumns().size()]);
+        if (walker.count(Column.Type.CLUSTERING) > 0)
+            fiterable.put(Column.Type.CLUSTERING, new Object[metadata.clusteringColumns().size()]);
+
+        Map<ColumnMetadata, Supplier<?>> cells = new HashMap<>();
+
+        walker.visitRow(row, new RowWalker.RowMetadataVisitor()
+        {
+            private int pIdx, cIdx = 0;
+
+            @Override
+            public <T> void accept(Column.Type type, String columnName, Class<T> clazz, Supplier<T> value)
+            {
+                switch (type)
+                {
+                    case PARTITION_KEY:
+                        fiterable.get(type)[pIdx++] = value.get();
+                        break;
+                    case CLUSTERING:
+                        fiterable.get(type)[cIdx++] = value.get();
+                        break;
+                    case REGULAR:
+                    {
+                        if (columnFilter.equals(ColumnFilter.NONE))
+                            break;
+
+                        // Push down the column filter to the walker, so we don't have to process the value if it's not queried
+                        ColumnMetadata cm = columnMetas.computeIfAbsent(columnName, name -> metadata.getColumn(ByteBufferUtil.bytes(name)));
+                        if (columnFilter.queriedColumns().contains(cm))
+                            cells.put(cm, value);
+
+                        break;
+                    }
+                    default:
+                        throw new IllegalStateException("Unknown column type: " + type);
+                }
+            }
+        });
+
+        return new CollectionRow(() -> makeRowKey(metadata, fiterable.get(Column.Type.PARTITION_KEY)),
+                                 makeRowClustering(metadata, fiterable.get(Column.Type.CLUSTERING)),
+                                 clustering ->
+                                 {
+                                     Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+                                     rowBuilder.newRow(clustering);
+                                     cells.forEach((column, value) -> {
+                                         Object valueObj = value.get();
+                                         if (valueObj == null)
+                                             return;
+                                         rowBuilder.addCell(BufferCell.live(column, NO_DELETION_TIME,
+                                                                            decompose(column.type, valueObj)));
+                                     });
+                                     return rowBuilder.build();
+                                 });
+    }
+
+    private static class CollectionRow
+    {
+        private final Supplier<DecoratedKey> key;
+        private final Clustering<?> clustering;
+        private final Supplier<Row> rowSup;
+
+        public CollectionRow(Supplier<DecoratedKey> key, Clustering<?> clustering, Function<Clustering<?>, Row> rowSup)
+        {
+            this.key = new ValueHolder<>(key);
+            this.clustering = clustering;
+            this.rowSup = new ValueHolder<>(() -> rowSup.apply(clustering));
+        }
+
+        private static class ValueHolder<T> implements Supplier<T>
+        {
+            private final Supplier<T> delegate;
+            private volatile T value;
+
+            public ValueHolder(Supplier<T> delegate)
+            {
+                this.delegate = delegate;
+            }
+
+            @Override
+            public T get()
+            {
+                if (value == null)
+                    value = delegate.get();
+
+                return value;
+            }
+        }
+    }
+
+    private static Clustering<?> makeRowClustering(TableMetadata metadata, Object... clusteringValues)
+    {
+        if (clusteringValues == null || clusteringValues.length == 0)
+            return Clustering.EMPTY;
+
+        ByteBuffer[] clusteringByteBuffers = new ByteBuffer[clusteringValues.length];
+        for (int i = 0; i < clusteringValues.length; i++)
+            clusteringByteBuffers[i] = decompose(metadata.clusteringColumns().get(i).type, clusteringValues[i]);
+        return Clustering.make(clusteringByteBuffers);
+    }
+
+    /**
+     * @param table              the table metadata
+     * @param partitionKeyValues the partition key values
+     * @return the decorated key
+     */
+    private static DecoratedKey makeRowKey(TableMetadata table, Object... partitionKeyValues)
+    {
+        ByteBuffer key;
+        if (partitionKeyValues.length > 1)
+            key = ((CompositeType) table.partitionKeyType).decompose(partitionKeyValues);
+        else
+            key = decompose(table.partitionKeyType, partitionKeyValues[0]);
+        return table.partitioner.decorateKey(key);
+    }
+
+    private static UnfilteredPartitionIterator createPartitionIterator(TableMetadata metadata,
+                                                                       Iterator<UnfilteredRowIterator> partitions)
+    {
+        return new AbstractUnfilteredPartitionIterator()
+        {
+            public UnfilteredRowIterator next()
+            {
+                return partitions.next();
+            }
+
+            public boolean hasNext()
+            {
+                return partitions.hasNext();
+            }
+
+            public TableMetadata metadata()
+            {
+                return metadata;
+            }
+        };
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> ByteBuffer decompose(AbstractType<?> type, T value)
+    {
+        return ((AbstractType<T>) type).decompose(value);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T compose(AbstractType<?> type, ByteBuffer value)
+    {
+        return (T) type.compose(value);
+    }
+
+    @Override
+    public TableMetadata metadata()
+    {
+        return metadata;
+    }
+
+    @Override
+    public void apply(PartitionUpdate update)
+    {
+        throw new InvalidRequestException("Modification is not supported by table " + metadata);
+    }
+
+    @Override
+    public void truncate()
+    {
+        throw new InvalidRequestException("Truncate is not supported by table " + metadata);
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
index 7b7e28e30a..973be20304 100644
--- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
+++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
@@ -19,8 +19,11 @@ package org.apache.cassandra.db.virtual;
 
 import com.google.common.collect.ImmutableList;
 
+import org.apache.cassandra.db.virtual.model.ThreadPoolRow;
+import org.apache.cassandra.db.virtual.walker.ThreadPoolRowWalker;
 import org.apache.cassandra.index.sai.virtual.StorageAttachedIndexTables;
 
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_VIEWS;
 
 public final class SystemViewsKeyspace extends VirtualKeyspace
@@ -35,7 +38,13 @@ public final class SystemViewsKeyspace extends VirtualKeyspace
                     .add(new SettingsTable(VIRTUAL_VIEWS))
                     .add(new SystemPropertiesTable(VIRTUAL_VIEWS))
                     .add(new SSTableTasksTable(VIRTUAL_VIEWS))
-                    .add(new ThreadPoolsTable(VIRTUAL_VIEWS))
+                    // Fully backward/forward compatible with the legace ThreadPoolsTable under the same "system_views.thread_pools" name.
+                    .add(CollectionVirtualTableAdapter.create(VIRTUAL_VIEWS,
+                                                              "thread_pools",
+                                                              "Thread pool metrics for all thread pools",
+                                                              new ThreadPoolRowWalker(),
+                                                              Metrics.allThreadPoolMetrics(),
+                                                              ThreadPoolRow::new))
                     .add(new InternodeOutboundTable(VIRTUAL_VIEWS))
                     .add(new InternodeInboundTable(VIRTUAL_VIEWS))
                     .add(new PendingHintsTable(VIRTUAL_VIEWS))
diff --git a/src/java/org/apache/cassandra/db/virtual/ThreadPoolsTable.java b/src/java/org/apache/cassandra/db/virtual/ThreadPoolsTable.java
deleted file mode 100644
index 4bd4624d58..0000000000
--- a/src/java/org/apache/cassandra/db/virtual/ThreadPoolsTable.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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 org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.marshal.Int32Type;
-import org.apache.cassandra.db.marshal.LongType;
-import org.apache.cassandra.db.marshal.UTF8Type;
-import org.apache.cassandra.dht.LocalPartitioner;
-import org.apache.cassandra.metrics.ThreadPoolMetrics;
-import org.apache.cassandra.schema.TableMetadata;
-
-import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
-
-final class ThreadPoolsTable extends AbstractVirtualTable
-{
-    private static final String NAME = "name";
-    private static final String ACTIVE_TASKS = "active_tasks";
-    private static final String ACTIVE_TASKS_LIMIT = "active_tasks_limit";
-    private static final String PENDING_TASKS = "pending_tasks";
-    private static final String COMPLETED_TASKS = "completed_tasks";
-    private static final String BLOCKED_TASKS = "blocked_tasks";
-    private static final String BLOCKED_TASKS_ALL_TIME = "blocked_tasks_all_time";
-    private static final String CORE_POOL_SIZE = "core_pool_size";
-    private static final String MAX_POOL_SIZE = "max_pool_size";
-    private static final String MAX_TASKS_QUEUED = "max_tasks_queued";
-
-    ThreadPoolsTable(String keyspace)
-    {
-        super(TableMetadata.builder(keyspace, "thread_pools")
-                           .kind(TableMetadata.Kind.VIRTUAL)
-                           .partitioner(new LocalPartitioner(UTF8Type.instance))
-                           .addPartitionKeyColumn(NAME, UTF8Type.instance)
-                           .addRegularColumn(ACTIVE_TASKS, Int32Type.instance)
-                           .addRegularColumn(ACTIVE_TASKS_LIMIT, Int32Type.instance)
-                           .addRegularColumn(PENDING_TASKS, Int32Type.instance)
-                           .addRegularColumn(COMPLETED_TASKS, LongType.instance)
-                           .addRegularColumn(BLOCKED_TASKS, LongType.instance)
-                           .addRegularColumn(BLOCKED_TASKS_ALL_TIME, LongType.instance)
-                           .addRegularColumn(CORE_POOL_SIZE, Int32Type.instance)
-                           .addRegularColumn(MAX_POOL_SIZE, Int32Type.instance)
-                           .addRegularColumn(MAX_TASKS_QUEUED, Int32Type.instance)
-                           .build());
-    }
-
-    @Override
-    public DataSet data(DecoratedKey partitionKey)
-    {
-        String poolName = UTF8Type.instance.compose(partitionKey.getKey());
-
-        SimpleDataSet result = new SimpleDataSet(metadata());
-        Metrics.getThreadPoolMetrics(poolName)
-               .ifPresent(metrics -> addRow(result, metrics));
-        return result;
-    }
-
-    @Override
-    public DataSet data()
-    {
-        SimpleDataSet result = new SimpleDataSet(metadata());
-        Metrics.allThreadPoolMetrics()
-               .forEach(metrics -> addRow(result, metrics));
-        return result;
-    }
-
-    private void addRow(SimpleDataSet dataSet, ThreadPoolMetrics metrics)
-    {
-        dataSet.row(metrics.poolName)
-               .column(ACTIVE_TASKS, metrics.activeTasks.getValue())
-               .column(ACTIVE_TASKS_LIMIT, metrics.maxPoolSize.getValue())
-               .column(PENDING_TASKS, metrics.pendingTasks.getValue())
-               .column(COMPLETED_TASKS, metrics.completedTasks.getValue())
-               .column(BLOCKED_TASKS, metrics.currentBlocked.getCount())
-               .column(BLOCKED_TASKS_ALL_TIME, metrics.totalBlocked.getCount())
-               .column(CORE_POOL_SIZE, metrics.corePoolSize.getValue())
-               .column(MAX_POOL_SIZE, metrics.maxPoolSize.getValue())
-               .column(MAX_TASKS_QUEUED, metrics.maxTasksQueued.getValue());
-    }
-}
diff --git a/src/java/org/apache/cassandra/db/virtual/VirtualKeyspace.java b/src/java/org/apache/cassandra/db/virtual/VirtualKeyspace.java
index 675021547c..044c11476b 100644
--- a/src/java/org/apache/cassandra/db/virtual/VirtualKeyspace.java
+++ b/src/java/org/apache/cassandra/db/virtual/VirtualKeyspace.java
@@ -18,6 +18,9 @@
 package org.apache.cassandra.db.virtual;
 
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
@@ -38,6 +41,15 @@ public class VirtualKeyspace
         this.name = name;
         this.tables = ImmutableList.copyOf(tables);
 
+        List<String> duplicates = tables.stream()
+                                        .map(VirtualTable::name)
+                                        .distinct()
+                                        .filter(entry -> Collections.frequency(tables, entry) > 1)
+                                        .collect(Collectors.toList());
+
+        if (!duplicates.isEmpty())
+            throw new IllegalArgumentException(String.format("Duplicate table names in virtual keyspace %s: %s", name, duplicates));
+
         metadata = KeyspaceMetadata.virtual(name, Tables.of(Iterables.transform(tables, VirtualTable::metadata)));
     }
 
diff --git a/src/java/org/apache/cassandra/db/virtual/model/Column.java b/src/java/org/apache/cassandra/db/virtual/model/Column.java
new file mode 100644
index 0000000000..6d8f329ad7
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/Column.java
@@ -0,0 +1,64 @@
+/*
+ * 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.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.cassandra.db.virtual.walker.RowWalker;
+
+/**
+ * Annotation to mark a walk order for a {@link RowWalker}. Annotation is used on a method that returns a value and
+ * represents a column value for a row. The columns order is defined by the column type and the column name
+ * in lexicographical order (case-insensitive) and the same way as in CQL (partition key columns first,
+ * then clustering columns, then regular columns).
+ * <p>
+ * The column {@link Column.Type} type is the same as the existing CQL {@code ColumnMetadata.Kind} and is used to
+ * define the column types for the virtual tables.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+public @interface Column
+{
+    /**
+     * The type of the column
+     */
+    Type type() default Type.REGULAR;
+
+    /**
+     * Be sure the order of the enum values is preserved as they are used in ordinal comparisons for the virtual tables.
+     */
+    enum Type
+    {
+        /**
+         * The column is a partition key column.
+         */
+        PARTITION_KEY,
+        /**
+         * The column is a clustering column.
+         */
+        CLUSTERING,
+        /**
+         * The column is a regular column.
+         */
+        REGULAR
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java b/src/java/org/apache/cassandra/db/virtual/model/CounterMetricRow.java
similarity index 56%
copy from src/java/org/apache/cassandra/metrics/PaxosMetrics.java
copy to src/java/org/apache/cassandra/db/virtual/model/CounterMetricRow.java
index 1e1ba89ba2..81d32ef990 100644
--- a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
+++ b/src/java/org/apache/cassandra/db/virtual/model/CounterMetricRow.java
@@ -16,18 +16,42 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.metrics;
+package org.apache.cassandra.db.virtual.model;
 
 import com.codahale.metrics.Counter;
-import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
-public class PaxosMetrics
+/**
+ * Counter metric representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class CounterMetricRow
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Paxos");
-    public static final Counter linearizabilityViolations = Metrics.counter(factory.createMetricName("LinearizabilityViolations"));
-    public static final Meter repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairPaxosTopologyRetries"));
+    private final String key;
+    private final Counter value;
+
+    public CounterMetricRow(String key, Metric value)
+    {
+        this.key = key;
+        this.value = (Counter) value;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
 
-    public static void initialize() {}
+    @Column
+    public long value()
+    {
+        return value.getCount();
+    }
 }
diff --git a/src/java/org/apache/cassandra/metrics/BatchMetrics.java b/src/java/org/apache/cassandra/db/virtual/model/GaugeMetricRow.java
similarity index 52%
copy from src/java/org/apache/cassandra/metrics/BatchMetrics.java
copy to src/java/org/apache/cassandra/db/virtual/model/GaugeMetricRow.java
index 9bea162116..11c7677d2a 100644
--- a/src/java/org/apache/cassandra/metrics/BatchMetrics.java
+++ b/src/java/org/apache/cassandra/db/virtual/model/GaugeMetricRow.java
@@ -15,24 +15,44 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.cassandra.metrics;
 
-import com.codahale.metrics.Histogram;
+package org.apache.cassandra.db.virtual.model;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Metric;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.getGaugeValue;
 
-public class BatchMetrics
+/**
+ * Gauge metric representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class GaugeMetricRow
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Batch");
+    private final String key;
+    private final Gauge<?> value;
+
+    public GaugeMetricRow(String key, Metric value)
+    {
+        this.key = key;
+        this.value = (Gauge<?>) value;
+    }
 
-    public final Histogram partitionsPerLoggedBatch;
-    public final Histogram partitionsPerUnloggedBatch;
-    public final Histogram partitionsPerCounterBatch;
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
 
-    public BatchMetrics()
+    @Column
+    public String value()
     {
-        partitionsPerLoggedBatch = Metrics.histogram(factory.createMetricName("PartitionsPerLoggedBatch"), false);
-        partitionsPerUnloggedBatch = Metrics.histogram(factory.createMetricName("PartitionsPerUnloggedBatch"), false);
-        partitionsPerCounterBatch = Metrics.histogram(factory.createMetricName("PartitionsPerCounterBatch"), false);
+        return getGaugeValue(value);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/virtual/model/HistogramMetricRow.java b/src/java/org/apache/cassandra/db/virtual/model/HistogramMetricRow.java
new file mode 100644
index 0000000000..057872b7dd
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/HistogramMetricRow.java
@@ -0,0 +1,100 @@
+/*
+ * 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.model;
+
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.Snapshot;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Historgam metric representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class HistogramMetricRow
+{
+    private final String key;
+    private final Snapshot value;
+
+    public HistogramMetricRow(String key, Metric value)
+    {
+        this.key = key;
+        this.value = ((Histogram) value).getSnapshot();
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public double p75th()
+    {
+        return value.get75thPercentile();
+    }
+
+    @Column
+    public double p95th()
+    {
+        return value.get95thPercentile();
+    }
+
+    @Column
+    public double p98th()
+    {
+        return value.get98thPercentile();
+    }
+
+    @Column
+    public double p99th()
+    {
+        return value.get99thPercentile();
+    }
+
+    @Column
+    public double p999th()
+    {
+        return value.get999thPercentile();
+    }
+
+    @Column
+    public long max()
+    {
+        return value.getMax();
+    }
+
+    @Column
+    public double mean()
+    {
+        return value.getMean();
+    }
+
+    @Column
+    public long min()
+    {
+        return value.getMin();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/model/MeterMetricRow.java b/src/java/org/apache/cassandra/db/virtual/model/MeterMetricRow.java
new file mode 100644
index 0000000000..e4857a5a39
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/MeterMetricRow.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Meter metric representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class MeterMetricRow
+{
+    private final String key;
+    private final Meter value;
+
+    public MeterMetricRow(String key, Metric value)
+    {
+        this.key = key;
+        this.value = (Meter) value;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
+
+    @Column
+    public long count()
+    {
+        return value.getCount();
+    }
+
+    @Column
+    public double fifteenMinuteRate()
+    {
+        return value.getFifteenMinuteRate();
+    }
+
+    @Column
+    public double fiveMinuteRate()
+    {
+        return value.getFiveMinuteRate();
+    }
+
+    @Column
+    public double meanRate()
+    {
+        return value.getMeanRate();
+    }
+
+    @Column
+    public double oneMinuteRate()
+    {
+        return value.getOneMinuteRate();
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java b/src/java/org/apache/cassandra/db/virtual/model/MetricGroupRow.java
similarity index 56%
copy from src/java/org/apache/cassandra/metrics/PaxosMetrics.java
copy to src/java/org/apache/cassandra/db/virtual/model/MetricGroupRow.java
index 1e1ba89ba2..b7326bad3e 100644
--- a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
+++ b/src/java/org/apache/cassandra/db/virtual/model/MetricGroupRow.java
@@ -16,18 +16,32 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.metrics;
+package org.apache.cassandra.db.virtual.model;
 
-import com.codahale.metrics.Counter;
-import com.codahale.metrics.Meter;
+import static org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter.virtualTableNameStyle;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRICS_GROUP_POSTFIX;
 
-import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
-
-public class PaxosMetrics
+/**
+ * Metric group row representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class MetricGroupRow
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Paxos");
-    public static final Counter linearizabilityViolations = Metrics.counter(factory.createMetricName("LinearizabilityViolations"));
-    public static final Meter repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairPaxosTopologyRetries"));
+    private final String group;
+
+    public MetricGroupRow(String group)
+    {
+        this.group = group;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String groupName()
+    {
+        return group;
+    }
 
-    public static void initialize() {}
+    @Column
+    public String virtualTable()
+    {
+        return METRICS_GROUP_POSTFIX.apply(virtualTableNameStyle(group));
+    }
 }
diff --git a/src/java/org/apache/cassandra/db/virtual/model/MetricRow.java b/src/java/org/apache/cassandra/db/virtual/model/MetricRow.java
new file mode 100644
index 0000000000..88ed98146b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/MetricRow.java
@@ -0,0 +1,80 @@
+/*
+ * 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.model;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Metric row representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class MetricRow
+{
+    private final String key;
+    private final Metric metricEntry;
+
+    public MetricRow(String key, Metric metricEntry)
+    {
+        this.key = key;
+        this.metricEntry = metricEntry;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
+
+    @Column
+    public String type()
+    {
+        Class<?> clazz = metricEntry.getClass();
+        if (Counter.class.isAssignableFrom(clazz))
+            return "counter";
+        else if (Gauge.class.isAssignableFrom(clazz))
+            return "gauge";
+        else if (Histogram.class.isAssignableFrom(clazz))
+            return "histogram";
+        else if (Meter.class.isAssignableFrom(clazz))
+            return "meter";
+        else if (Timer.class.isAssignableFrom(clazz))
+            return "timer";
+        else
+            throw new IllegalStateException("Unknown metric type: " + metricEntry.getClass());
+    }
+
+    @Column
+    public String value()
+    {
+        return CassandraMetricsRegistry.getValueAsString(metricEntry);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/model/ThreadPoolRow.java b/src/java/org/apache/cassandra/db/virtual/model/ThreadPoolRow.java
new file mode 100644
index 0000000000..a14cf19e21
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/ThreadPoolRow.java
@@ -0,0 +1,94 @@
+/*
+ * 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.model;
+
+import org.apache.cassandra.metrics.ThreadPoolMetrics;
+
+/**
+ * Thread pool metrics representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class ThreadPoolRow
+{
+    private final ThreadPoolMetrics entry;
+
+    public ThreadPoolRow(ThreadPoolMetrics entry)
+    {
+        this.entry = entry;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return entry.poolName;
+    }
+
+    @Column
+    public Integer activeTasks()
+    {
+        return entry.activeTasks.getValue();
+    }
+
+    @Column
+    public Integer activeTasksLimit()
+    {
+        return entry.maxPoolSize.getValue();
+    }
+
+    @Column
+    public Integer pendingTasks()
+    {
+        return entry.pendingTasks.getValue();
+    }
+
+    @Column
+    public Long completedTasks()
+    {
+        return entry.completedTasks.getValue();
+    }
+
+    @Column
+    public long blockedTasks()
+    {
+        return entry.currentBlocked.getCount();
+    }
+
+    @Column
+    public long blockedTasksAllTime()
+    {
+        return entry.totalBlocked.getCount();
+    }
+
+    @Column
+    public Integer corePoolSize()
+    {
+        return entry.corePoolSize.getValue();
+    }
+
+    @Column
+    public Integer maxPoolSize()
+    {
+        return entry.maxPoolSize.getValue();
+    }
+
+    @Column
+    public Integer maxTasksQueued()
+    {
+        return entry.maxTasksQueued.getValue();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/model/TimerMetricRow.java b/src/java/org/apache/cassandra/db/virtual/model/TimerMetricRow.java
new file mode 100644
index 0000000000..4fd4b81758
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/model/TimerMetricRow.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.Timer;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Timer metric representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class TimerMetricRow
+{
+    private final String key;
+    private final Timer value;
+
+    public TimerMetricRow(String key, Metric value)
+    {
+        this.key = key;
+        this.value = (Timer) value;
+    }
+
+    @Column
+    public String scope()
+    {
+        return Metrics.getMetricScope(key);
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String name()
+    {
+        return key;
+    }
+
+    @Column
+    public long count()
+    {
+        return value.getCount();
+    }
+
+    @Column
+    public double fifteenMinuteRate()
+    {
+        return value.getFifteenMinuteRate();
+    }
+
+    @Column
+    public double fiveMinuteRate()
+    {
+        return value.getFiveMinuteRate();
+    }
+
+    @Column
+    public double meanRate()
+    {
+        return value.getMeanRate();
+    }
+
+    @Column
+    public double oneMinuteRate()
+    {
+        return value.getOneMinuteRate();
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/CounterMetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/CounterMetricRowWalker.java
new file mode 100644
index 0000000000..8f6876afa3
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/CounterMetricRowWalker.java
@@ -0,0 +1,61 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.CounterMetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.CounterMetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.CounterMetricRow
+ */
+public class CounterMetricRowWalker implements RowWalker<CounterMetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+        visitor.accept(Column.Type.REGULAR, "value", Long.TYPE);
+    }
+
+    @Override
+    public void visitRow(CounterMetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+        visitor.accept(Column.Type.REGULAR, "value", Long.TYPE, row::value);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 2;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/GaugeMetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/GaugeMetricRowWalker.java
new file mode 100644
index 0000000000..e510f33b5d
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/GaugeMetricRowWalker.java
@@ -0,0 +1,61 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.GaugeMetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.GaugeMetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.GaugeMetricRow
+ */
+public class GaugeMetricRowWalker implements RowWalker<GaugeMetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+        visitor.accept(Column.Type.REGULAR, "value", String.class);
+    }
+
+    @Override
+    public void visitRow(GaugeMetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+        visitor.accept(Column.Type.REGULAR, "value", String.class, row::value);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 2;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/HistogramMetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/HistogramMetricRowWalker.java
new file mode 100644
index 0000000000..9347a0a715
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/HistogramMetricRowWalker.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.HistogramMetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.HistogramMetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.HistogramMetricRow
+ */
+public class HistogramMetricRowWalker implements RowWalker<HistogramMetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "max", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "mean", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "min", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "p75th", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "p95th", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "p98th", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "p999th", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "p99th", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+    }
+
+    @Override
+    public void visitRow(HistogramMetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "max", Long.TYPE, row::max);
+        visitor.accept(Column.Type.REGULAR, "mean", Double.TYPE, row::mean);
+        visitor.accept(Column.Type.REGULAR, "min", Long.TYPE, row::min);
+        visitor.accept(Column.Type.REGULAR, "p75th", Double.TYPE, row::p75th);
+        visitor.accept(Column.Type.REGULAR, "p95th", Double.TYPE, row::p95th);
+        visitor.accept(Column.Type.REGULAR, "p98th", Double.TYPE, row::p98th);
+        visitor.accept(Column.Type.REGULAR, "p999th", Double.TYPE, row::p999th);
+        visitor.accept(Column.Type.REGULAR, "p99th", Double.TYPE, row::p99th);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 9;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/MeterMetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/MeterMetricRowWalker.java
new file mode 100644
index 0000000000..3039bb924b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/MeterMetricRowWalker.java
@@ -0,0 +1,69 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.MeterMetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.MeterMetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.MeterMetricRow
+ */
+public class MeterMetricRowWalker implements RowWalker<MeterMetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "count", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "fifteen_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "five_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "mean_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "one_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+    }
+
+    @Override
+    public void visitRow(MeterMetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "count", Long.TYPE, row::count);
+        visitor.accept(Column.Type.REGULAR, "fifteen_minute_rate", Double.TYPE, row::fifteenMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "five_minute_rate", Double.TYPE, row::fiveMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "mean_rate", Double.TYPE, row::meanRate);
+        visitor.accept(Column.Type.REGULAR, "one_minute_rate", Double.TYPE, row::oneMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 6;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/MetricGroupRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/MetricGroupRowWalker.java
new file mode 100644
index 0000000000..0cb7c6f345
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/MetricGroupRowWalker.java
@@ -0,0 +1,58 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.MetricGroupRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.MetricGroupRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.MetricGroupRow
+ */
+public class MetricGroupRowWalker implements RowWalker<MetricGroupRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "group_name", String.class);
+        visitor.accept(Column.Type.REGULAR, "virtual_table", String.class);
+    }
+
+    @Override
+    public void visitRow(MetricGroupRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "group_name", String.class, row::groupName);
+        visitor.accept(Column.Type.REGULAR, "virtual_table", String.class, row::virtualTable);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+            case REGULAR:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/MetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/MetricRowWalker.java
new file mode 100644
index 0000000000..f5e46c021b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/MetricRowWalker.java
@@ -0,0 +1,63 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.MetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.MetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.MetricRow
+ */
+public class MetricRowWalker implements RowWalker<MetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+        visitor.accept(Column.Type.REGULAR, "type", String.class);
+        visitor.accept(Column.Type.REGULAR, "value", String.class);
+    }
+
+    @Override
+    public void visitRow(MetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+        visitor.accept(Column.Type.REGULAR, "type", String.class, row::type);
+        visitor.accept(Column.Type.REGULAR, "value", String.class, row::value);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 3;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/RowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/RowWalker.java
new file mode 100644
index 0000000000..e45b183486
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/RowWalker.java
@@ -0,0 +1,87 @@
+/*
+ * 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.walker;
+
+import java.util.function.Supplier;
+
+import org.apache.cassandra.db.virtual.model.Column;
+
+/**
+ * Utility class for quick iteration over row attributes and row values.
+ * Walk order is defined by {@link Column} annotations and is the same as
+ * in CQL (partition key columns first, then clustering columns, then regular columns).
+ */
+public interface RowWalker<R>
+{
+    /**
+     * Returns the number of columns of the given type in the row.
+     *
+     * @param type the type of column
+     * @return the number of columns of the given type in the row.
+     */
+    int count(Column.Type type);
+
+    /**
+     * Visit the metadata of the row.
+     *
+     * @param visitor the visitor to accept the metadata
+     */
+    void visitMeta(MetadataVisitor visitor);
+
+    /**
+     * Visit the row.
+     *
+     * @param row     the row to visit
+     * @param visitor the visitor to accept the row
+     */
+    void visitRow(R row, RowMetadataVisitor visitor);
+
+    /**
+     * Visitor for metadata of the row.
+     */
+    interface MetadataVisitor
+    {
+        /**
+         * Process the metadata.
+         *
+         * @param type  the type of column
+         * @param name  the name of the column
+         * @param clazz the class of the column
+         * @param <T>   the type of the column
+         */
+        <T> void accept(Column.Type type, String name, Class<T> clazz);
+    }
+
+    /**
+     * Visitor for a row value and its metadata.
+     */
+    interface RowMetadataVisitor
+    {
+        /**
+         * Process the row metadata.
+         *
+         * @param type  the type of column
+         * @param name  the name of the column
+         * @param clazz the class of the column
+         * @param value the value of the column
+         * @param <T>   the type of the column
+         */
+        <T> void accept(Column.Type type, String name, Class<T> clazz, Supplier<T> value);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/ThreadPoolRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/ThreadPoolRowWalker.java
new file mode 100644
index 0000000000..68cbc8c693
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/ThreadPoolRowWalker.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.ThreadPoolRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.ThreadPoolRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.ThreadPoolRow
+ */
+public class ThreadPoolRowWalker implements RowWalker<ThreadPoolRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "active_tasks", Integer.class);
+        visitor.accept(Column.Type.REGULAR, "active_tasks_limit", Integer.class);
+        visitor.accept(Column.Type.REGULAR, "blocked_tasks", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "blocked_tasks_all_time", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "completed_tasks", Long.class);
+        visitor.accept(Column.Type.REGULAR, "core_pool_size", Integer.class);
+        visitor.accept(Column.Type.REGULAR, "max_pool_size", Integer.class);
+        visitor.accept(Column.Type.REGULAR, "max_tasks_queued", Integer.class);
+        visitor.accept(Column.Type.REGULAR, "pending_tasks", Integer.class);
+    }
+
+    @Override
+    public void visitRow(ThreadPoolRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "active_tasks", Integer.class, row::activeTasks);
+        visitor.accept(Column.Type.REGULAR, "active_tasks_limit", Integer.class, row::activeTasksLimit);
+        visitor.accept(Column.Type.REGULAR, "blocked_tasks", Long.TYPE, row::blockedTasks);
+        visitor.accept(Column.Type.REGULAR, "blocked_tasks_all_time", Long.TYPE, row::blockedTasksAllTime);
+        visitor.accept(Column.Type.REGULAR, "completed_tasks", Long.class, row::completedTasks);
+        visitor.accept(Column.Type.REGULAR, "core_pool_size", Integer.class, row::corePoolSize);
+        visitor.accept(Column.Type.REGULAR, "max_pool_size", Integer.class, row::maxPoolSize);
+        visitor.accept(Column.Type.REGULAR, "max_tasks_queued", Integer.class, row::maxTasksQueued);
+        visitor.accept(Column.Type.REGULAR, "pending_tasks", Integer.class, row::pendingTasks);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 9;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/walker/TimerMetricRowWalker.java b/src/java/org/apache/cassandra/db/virtual/walker/TimerMetricRowWalker.java
new file mode 100644
index 0000000000..418495e24b
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/walker/TimerMetricRowWalker.java
@@ -0,0 +1,69 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.TimerMetricRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.TimerMetricRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.TimerMetricRow
+ */
+public class TimerMetricRowWalker implements RowWalker<TimerMetricRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class);
+        visitor.accept(Column.Type.REGULAR, "count", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "fifteen_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "five_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "mean_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "one_minute_rate", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class);
+    }
+
+    @Override
+    public void visitRow(TimerMetricRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "name", String.class, row::name);
+        visitor.accept(Column.Type.REGULAR, "count", Long.TYPE, row::count);
+        visitor.accept(Column.Type.REGULAR, "fifteen_minute_rate", Double.TYPE, row::fifteenMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "five_minute_rate", Double.TYPE, row::fiveMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "mean_rate", Double.TYPE, row::meanRate);
+        visitor.accept(Column.Type.REGULAR, "one_minute_rate", Double.TYPE, row::oneMinuteRate);
+        visitor.accept(Column.Type.REGULAR, "scope", String.class, row::scope);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case PARTITION_KEY:
+                return 1;
+            case CLUSTERING:
+                return 0;
+            case REGULAR:
+                return 6;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java b/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java
index 90956e4b32..b305720ebb 100644
--- a/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java
+++ b/src/java/org/apache/cassandra/index/sai/metrics/AbstractMetrics.java
@@ -34,7 +34,7 @@ public abstract class AbstractMetrics
     protected final String table;
     private final String index;
     private final String scope;
-    protected final List<CassandraMetricsRegistry.MetricName> tracked = new ArrayList<>();
+    protected final List<String> tracked = new ArrayList<>();
 
     AbstractMetrics(IndexIdentifier indexIdentifier, String scope)
     {
@@ -77,7 +77,7 @@ public abstract class AbstractMetrics
 
         CassandraMetricsRegistry.MetricName metricName = new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME,
                                                                                                  TYPE, name, metricScope, createMBeanName(name, scope));
-        tracked.add(metricName);
+        tracked.add(metricName.getMetricName());
         return metricName;
     }
 
diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java b/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java
index 9d2ab6e93a..7828599e14 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/RowIndexEntry.java
@@ -137,6 +137,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class RowIndexEntry extends AbstractRowIndexEntry
 {
+    public static final String TYPE_NAME = "Index";
     private static final BigFormat FORMAT = BigFormat.getInstance();
     private static final long EMPTY_SIZE = ObjectSizes.measure(new RowIndexEntry(0));
 
@@ -151,7 +152,7 @@ public class RowIndexEntry extends AbstractRowIndexEntry
     static final Histogram indexInfoReadsHistogram;
     static
     {
-        MetricNameFactory factory = new DefaultNameFactory("Index", "RowIndexEntry");
+        MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, "RowIndexEntry");
         indexEntrySizeHistogram = Metrics.histogram(factory.createMetricName("IndexedEntrySize"), false);
         indexInfoCountHistogram = Metrics.histogram(factory.createMetricName("IndexInfoCount"), false);
         indexInfoGetsHistogram = Metrics.histogram(factory.createMetricName("IndexInfoGets"), false);
diff --git a/src/java/org/apache/cassandra/metrics/BatchMetrics.java b/src/java/org/apache/cassandra/metrics/BatchMetrics.java
index 9bea162116..28bf4cfbb2 100644
--- a/src/java/org/apache/cassandra/metrics/BatchMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/BatchMetrics.java
@@ -23,8 +23,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class BatchMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Batch");
-
+    public static final String TYPE_NAME = "Batch";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
     public final Histogram partitionsPerLoggedBatch;
     public final Histogram partitionsPerUnloggedBatch;
     public final Histogram partitionsPerCounterBatch;
diff --git a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
index 71373b35e8..afcf62a09b 100644
--- a/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/BufferPoolMetrics.java
@@ -25,6 +25,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class BufferPoolMetrics
 {
+    public static final String TYPE_NAME = "BufferPool";
     /** Total number of hits */
     public final Meter hits;
 
@@ -49,7 +50,7 @@ public class BufferPoolMetrics
 
     public BufferPoolMetrics(String scope, BufferPool bufferPool)
     {
-        MetricNameFactory factory = new DefaultNameFactory("BufferPool", scope);
+        MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, scope);
 
         hits = Metrics.meter(factory.createMetricName("Hits"));
 
diff --git a/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java b/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java
index 87c0d53541..f42f49592a 100644
--- a/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CASClientWriteRequestMetrics.java
@@ -48,8 +48,8 @@ public class CASClientWriteRequestMetrics extends CASClientRequestMetrics
     public void release()
     {
         super.release();
-        Metrics.remove(factory.createMetricName("ConditionNotMet"),
-                       DefaultNameFactory.createMetricName("ClientRequest", "ConditionNotMet", "CASRead"));
+        // Aliases are already known to the parent metrics, so we don't need to remove them here.
+        Metrics.remove(factory.createMetricName("ConditionNotMet"));
         Metrics.remove(factory.createMetricName("MutationSizeHistogram"));
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java b/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java
index 0de51dfb09..39a6e357c3 100644
--- a/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java
@@ -30,7 +30,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class CIDRAuthorizerMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("CIDRAuthorizer");
+    public static final String TYPE_NAME = "CIDRAuthorizer";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     // Number of user accesses rejected by CIDR authorization
     public static final String CIDR_ACCESSES_REJECTED_COUNT_PREFIX = "CIDRAccessesRejectedCount - ";
diff --git a/src/java/org/apache/cassandra/metrics/CQLMetrics.java b/src/java/org/apache/cassandra/metrics/CQLMetrics.java
index ce91333942..edf13dfb4e 100644
--- a/src/java/org/apache/cassandra/metrics/CQLMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CQLMetrics.java
@@ -26,7 +26,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class CQLMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("CQL");
+    public static final String TYPE_NAME = "CQL";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     public final Counter regularStatementsExecuted;
     public final Counter preparedStatementsExecuted;
diff --git a/src/java/org/apache/cassandra/metrics/CacheMetrics.java b/src/java/org/apache/cassandra/metrics/CacheMetrics.java
index a6017a39f8..574b0f065c 100644
--- a/src/java/org/apache/cassandra/metrics/CacheMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CacheMetrics.java
@@ -27,6 +27,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class CacheMetrics extends AbstractCacheMetrics
 {
+    public static final String TYPE_NAME = "Cache";
     /** Cache capacity in bytes */
     public final Gauge<Long> capacity;
 
@@ -44,7 +45,7 @@ public class CacheMetrics extends AbstractCacheMetrics
      */
     public CacheMetrics(String type, CacheSize cache)
     {
-        super(new DefaultNameFactory("Cache", type), type);
+        super(new DefaultNameFactory(TYPE_NAME, type), type);
         capacity = Metrics.register(factory.createMetricName("Capacity"), cache::capacity);
         size = Metrics.register(factory.createMetricName("Size"), cache::weightedSize);
         entries = Metrics.register(factory.createMetricName("Entries"), cache::size);
diff --git a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
index cd98cebce9..a687ff997e 100644
--- a/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
+++ b/src/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java
@@ -18,85 +18,348 @@
 package org.apache.cassandra.metrics;
 
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.management.MalformedObjectNameException;
 import javax.management.ObjectName;
 
 import com.google.common.annotations.VisibleForTesting;
-
-import com.codahale.metrics.*;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang3.ArrayUtils;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metered;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.MetricRegistryListener;
+import com.codahale.metrics.MetricSet;
+import com.codahale.metrics.Timer;
+import org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter;
+import org.apache.cassandra.db.virtual.VirtualTable;
+import org.apache.cassandra.db.virtual.model.CounterMetricRow;
+import org.apache.cassandra.db.virtual.model.GaugeMetricRow;
+import org.apache.cassandra.db.virtual.model.HistogramMetricRow;
+import org.apache.cassandra.db.virtual.model.MeterMetricRow;
+import org.apache.cassandra.db.virtual.model.MetricGroupRow;
+import org.apache.cassandra.db.virtual.model.MetricRow;
+import org.apache.cassandra.db.virtual.model.TimerMetricRow;
+import org.apache.cassandra.db.virtual.walker.CounterMetricRowWalker;
+import org.apache.cassandra.db.virtual.walker.GaugeMetricRowWalker;
+import org.apache.cassandra.db.virtual.walker.HistogramMetricRowWalker;
+import org.apache.cassandra.db.virtual.walker.MeterMetricRowWalker;
+import org.apache.cassandra.db.virtual.walker.MetricGroupRowWalker;
+import org.apache.cassandra.db.virtual.walker.MetricRowWalker;
+import org.apache.cassandra.db.virtual.walker.TimerMetricRowWalker;
+import org.apache.cassandra.index.sai.metrics.AbstractMetrics;
+import org.apache.cassandra.io.sstable.format.big.RowIndexEntry;
 import org.apache.cassandra.utils.MBeanWrapper;
+import org.apache.cassandra.utils.memory.MemtablePool;
+
+import static java.util.Optional.ofNullable;
+import static org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter.createSinglePartitionedKeyFiltered;
+import static org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter.createSinglePartitionedValueFiltered;
+import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_METRICS;
 
 /**
- * Makes integrating 3.0 metrics API with 2.0.
- * <p>
- * The 3.0 API comes with poor JMX integration
- * </p>
+ * Dropwizard metrics registry extension for Cassandra, as of for now uses the latest version of Dropwizard metrics
+ * library {@code 4.2.x} that has a pretty good integration with JMX. The registry is used by Cassandra to
+ * store all metrics and expose them to JMX and {@link org.apache.cassandra.db.virtual.VirtualTable}.
+ * In addition to that, the registry provides a way to store aliases for metrics and group metrics by
+ * the Cassandra-specific metric groups, which are used to expose metrics in that way.
+ *
+ * @see org.apache.cassandra.db.virtual.VirtualTable
+ * @see org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter
  */
 public class CassandraMetricsRegistry extends MetricRegistry
 {
-    public static final CassandraMetricsRegistry Metrics = new CassandraMetricsRegistry();
+    public static final UnaryOperator<String> METRICS_GROUP_POSTFIX = name -> name + "_group";
+
+    /**
+     * A map of metric name constructed by {@link com.codahale.metrics.MetricRegistry#name(String, String...)} and
+     * its full name in the way how it is represented in JMX. The map is used by {@link CassandraJmxMetricsExporter}
+     * to export metrics to JMX.
+     */
+    private static final ConcurrentMap<String, Deque<MetricName>> ALIASES = new ConcurrentHashMap<>();
+
+    /** A set of all known metric groups, used to validate metric groups that are statically defined in Cassandra. */
+    static final Set<String> metricGroups;
+
+    /**
+     * Root metrics registry that is used by Cassandra to store all metrics.
+     * All modifications to the registry are delegated to the corresponding listeners as well.
+     * Metrics from the root registry are exported to JMX by {@link CassandraJmxMetricsExporter} and to virtual tables
+     * via {@link #createMetricsKeyspaceTables()}.
+     */
+    public static final CassandraMetricsRegistry Metrics = init();
+    private final MetricRegistryListener jmxExporter = new CassandraJmxMetricsExporter(ALIASES);
+
+    /** We have to make sure that this metrics listener is called the last, so that it can clean up aliases. */
+    private final MetricRegistryListener housekeepingListener = new BaseMetricRegistryListener()
+    {
+        @Override
+        protected void onMetricRemove(String name)
+        {
+            ALIASES.remove(name);
+        }
+    };
+
     private final Map<String, ThreadPoolMetrics> threadPoolMetrics = new ConcurrentHashMap<>();
+    public static final String METRIC_SCOPE_UNDEFINED = "undefined";
+    public static final TimeUnit DEFAULT_TIMER_UNIT = TimeUnit.MICROSECONDS;
 
-    private final MBeanWrapper mBeanServer = MBeanWrapper.instance;
-    public final static TimeUnit DEFAULT_TIMER_UNIT = TimeUnit.MICROSECONDS;
+    static
+    {
+        // We have to initialize metric group names like this, because we can't register them dynamically
+        // as it is done for the dropwizard metrics. So we have to be sure, that all these metric groups are
+        // initialized at the time #start() method is called. The virtual kespaces are immutable, drivers also
+        // rely on the fact that virtual keyspaces are immutable, so they won't receive any updates if we change them.
+        //
+        // For example, if a new metric group is added, the appropriate tests will fail, because of the missing
+        // group in this collection, which in turn guarantees that all metric groups are registered and exposed
+        // for virtual tables.
+        metricGroups = ImmutableSet.<String>builder()
+                                   .add(AbstractMetrics.TYPE)
+                                   .add(BatchMetrics.TYPE_NAME)
+                                   .add(BufferPoolMetrics.TYPE_NAME)
+                                   .add(CIDRAuthorizerMetrics.TYPE_NAME)
+                                   .add(CQLMetrics.TYPE_NAME)
+                                   .add(CacheMetrics.TYPE_NAME)
+                                   .add(ChunkCacheMetrics.TYPE_NAME)
+                                   .add(ClientMessageSizeMetrics.TYPE)
+                                   .add(ClientMetrics.TYPE_NAME)
+                                   .add(ClientRequestMetrics.TYPE_NAME)
+                                   .add(ClientRequestSizeMetrics.TYPE)
+                                   .add(CommitLogMetrics.TYPE_NAME)
+                                   .add(CompactionMetrics.TYPE_NAME)
+                                   .add(DenylistMetrics.TYPE_NAME)
+                                   .add(DroppedMessageMetrics.TYPE)
+                                   .add(HintedHandoffMetrics.TYPE_NAME)
+                                   .add(HintsServiceMetrics.TYPE_NAME)
+                                   .add(InternodeInboundMetrics.TYPE_NAME)
+                                   .add(InternodeOutboundMetrics.TYPE_NAME)
+                                   .add(KeyspaceMetrics.TYPE_NAME)
+                                   .add(MemtablePool.TYPE_NAME)
+                                   .add(MessagingMetrics.TYPE_NAME)
+                                   .add(MutualTlsMetrics.TYPE_NAME)
+                                   .add(PaxosMetrics.TYPE_NAME)
+                                   .add(ReadRepairMetrics.TYPE_NAME)
+                                   .add(RepairMetrics.TYPE_NAME)
+                                   .add(RowIndexEntry.TYPE_NAME)
+                                   .add(StorageMetrics.TYPE_NAME)
+                                   .add(StreamingMetrics.TYPE_NAME)
+                                   .add(TCMMetrics.TYPE_NAME)
+                                   .add(TableMetrics.ALIAS_TYPE_NAME)
+                                   .add(TableMetrics.TYPE_NAME)
+                                   .add(TableMetrics.INDEX_TYPE_NAME)
+                                   .add(TableMetrics.INDEX_ALIAS_TYPE_NAME)
+                                   .add(ThreadPoolMetrics.TYPE_NAME)
+                                   .add(TrieMemtableMetricsView.TYPE_NAME)
+                                   .add(UnweightedCacheMetrics.TYPE_NAME)
+                                   .build();
+    }
 
     private CassandraMetricsRegistry()
     {
-        super();
     }
 
-    public Counter counter(MetricName name)
+    private static CassandraMetricsRegistry init()
     {
-        Counter counter = counter(name.getMetricName());
-        registerMBean(counter, name.getMBeanName());
+        CassandraMetricsRegistry registry = new CassandraMetricsRegistry();
+        // Adding listeners to the root registry, so that they can be notified about all metrics changes.
+        registry.addListener(registry.jmxExporter);
+        registry.addListener(registry.housekeepingListener);
+        return registry;
+    }
 
-        return counter;
+    @SuppressWarnings("rawtypes")
+    public static String getValueAsString(Metric metric)
+    {
+        if (metric instanceof Counter)
+            return Long.toString(((Counter) metric).getCount());
+        else if (metric instanceof Gauge)
+            return getGaugeValue((Gauge) metric);
+        else if (metric instanceof Histogram)
+            return Double.toString(((Histogram) metric).getSnapshot().getMedian());
+        else if (metric instanceof Meter)
+            return Long.toString(((Meter) metric).getCount());
+        else if (metric instanceof Timer)
+            return Long.toString(((Timer) metric).getCount());
+        else
+            throw new IllegalStateException("Unknown metric type: " + metric.getClass().getName());
     }
 
-    public Counter counter(MetricName name, MetricName alias)
+    public static String getGaugeValue(Gauge<?> gauge)
     {
-        Counter counter = counter(name);
-        registerAlias(name, alias);
-        return counter;
+        Object value = gauge.getValue();
+        if (value == null)
+            return "null";
+        else if (value instanceof long[])
+            return Arrays.toString((long[]) value);
+        else if (value instanceof double[])
+            return Arrays.toString((double[]) value);
+        else if (value instanceof short[])
+            return Arrays.toString((short[]) value);
+        else if (value instanceof int[])
+            return Arrays.toString((int[]) value);
+        else if (value instanceof float[])
+            return Arrays.toString((float[]) value);
+        else if (value instanceof byte[])
+            return Arrays.toString((byte[]) value);
+        else if (value instanceof Object[])
+            return Arrays.toString((Object[]) value);
+        else
+            return value.toString();
     }
 
-    public Meter meter(MetricName name)
+    public static List<VirtualTable> createMetricsKeyspaceTables()
     {
-        Meter meter = meter(name.getMetricName());
-        registerMBean(meter, name.getMBeanName());
+        ImmutableList.Builder<VirtualTable> builder = ImmutableList.builder();
+        metricGroups.forEach(groupName -> {
+            // This is a very efficient way to filter metrics by group name, so make sure that metrics group name
+            // and metric type following the same order as it constructed in MetricName class.
+            final String groupPrefix = DefaultNameFactory.GROUP_NAME + '.' + groupName + '.';
+            builder.add(createSinglePartitionedKeyFiltered(VIRTUAL_METRICS,
+                                                           METRICS_GROUP_POSTFIX.apply(groupName),
+                                                           "All metrics for \"" + groupName + "\" metric group",
+                                                           new MetricRowWalker(),
+                                                           Metrics.getMetrics(),
+                                                           key -> key.startsWith(groupPrefix),
+                                                           MetricRow::new));
+        });
+        // Register virtual table of all known metric groups.
+        builder.add(CollectionVirtualTableAdapter.create(VIRTUAL_METRICS,
+                                                         "all_groups",
+                                                         "All metric group names",
+                                                         new MetricGroupRowWalker(),
+                                                         metricGroups,
+                                                         MetricGroupRow::new))
+               // Register virtual tables of all metrics types similar to the JMX MBean structure,
+               // e.g.: HistogramJmxMBean, MeterJmxMBean, etc.
+               .add(createSinglePartitionedValueFiltered(VIRTUAL_METRICS,
+                                                         "type_counter",
+                                                         "All metrics with type \"Counter\"",
+                                                         new CounterMetricRowWalker(),
+                                                         Metrics.getMetrics(),
+                                                         Counter.class::isInstance,
+                                                         CounterMetricRow::new))
+               .add(createSinglePartitionedValueFiltered(VIRTUAL_METRICS,
+                                                         "type_gauge",
+                                                         "All metrics with type \"Gauge\"",
+                                                         new GaugeMetricRowWalker(),
+                                                         Metrics.getMetrics(),
+                                                         Gauge.class::isInstance,
+                                                         GaugeMetricRow::new))
+               .add(createSinglePartitionedValueFiltered(VIRTUAL_METRICS,
+                                                         "type_histogram",
+                                                         "All metrics with type \"Histogram\"",
+                                                         new HistogramMetricRowWalker(),
+                                                         Metrics.getMetrics(),
+                                                         Histogram.class::isInstance,
+                                                         HistogramMetricRow::new))
+               .add(createSinglePartitionedValueFiltered(VIRTUAL_METRICS,
+                                                         "type_meter",
+                                                         "All metrics with type \"Meter\"",
+                                                         new MeterMetricRowWalker(),
+                                                         Metrics.getMetrics(),
+                                                         Meter.class::isInstance,
+                                                         MeterMetricRow::new))
+               .add(createSinglePartitionedValueFiltered(VIRTUAL_METRICS,
+                                                         "type_timer",
+                                                         "All metrics with type \"Timer\"",
+                                                         new TimerMetricRowWalker(),
+                                                         Metrics.getMetrics(),
+                                                         Timer.class::isInstance,
+                                                         TimerMetricRow::new));
+        return builder.build();
+    }
 
-        return meter;
+    private static void addUnknownMetric(MetricName newMetricName)
+    {
+        String type = newMetricName.getType();
+        if (type.indexOf('.') >= 0)
+            throw new IllegalStateException(
+                "Metric type must not contain '.' character as it results in the efficiency of the metric collection traversal: " + type);
+        if (!metricGroups.contains(newMetricName.getType()))
+            throw new IllegalStateException("Unknown metric group: " + newMetricName.getType());
+        if (!metricGroups.contains(newMetricName.getSystemViewName()))
+            throw new IllegalStateException("Metric view name must match statically registered groups: " +
+                                            newMetricName.getSystemViewName());
+
+        // We have to be sure that aliases are registered the same order as they are added to the registry.
+        ALIASES.computeIfAbsent(newMetricName.getMetricName(), k -> new LinkedList<>())
+               .add(newMetricName);
+    }
+
+    private static void setAliases(MetricName... names)
+    {
+        Arrays.asList(names).forEach(CassandraMetricsRegistry::addUnknownMetric);
+        ALIASES.get(names[0].getMetricName()).addAll(Arrays.asList(names));
+    }
+
+    public String getMetricScope(String metricName)
+    {
+        Deque<MetricName> deque = ALIASES.get(metricName);
+        return deque == null ? METRIC_SCOPE_UNDEFINED :
+               deque.stream().findFirst().map(MetricName::getScope).orElse(METRIC_SCOPE_UNDEFINED);
+    }
+
+    public Counter counter(MetricName... name)
+    {
+        setAliases(name);
+        Counter counter = super.counter(name[0].getMetricName());
+        Stream.of(name).skip(1).forEach(n -> register(n, counter));
+        return counter;
     }
 
-    public Meter meter(MetricName name, MetricName alias)
+    public Meter meter(MetricName... name)
     {
-        Meter meter = meter(name);
-        registerAlias(name, alias);
+        setAliases(name);
+        Meter meter = super.meter(name[0].getMetricName());
+        Stream.of(name).skip(1).forEach(n -> register(n, meter));
         return meter;
     }
 
     public Histogram histogram(MetricName name, boolean considerZeroes)
     {
-        Histogram histogram = register(name, new ClearableHistogram(new DecayingEstimatedHistogramReservoir(considerZeroes)));
-        registerMBean(histogram, name.getMBeanName());
-
-        return histogram;
+        return register(name, new ClearableHistogram(new DecayingEstimatedHistogramReservoir(considerZeroes)));
     }
 
     public Histogram histogram(MetricName name, MetricName alias, boolean considerZeroes)
     {
+        setAliases(name, alias);
         Histogram histogram = histogram(name, considerZeroes);
-        registerAlias(name, alias);
+        register(alias, histogram);
         return histogram;
     }
 
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public <T extends Gauge> T gauge(MetricName name, MetricName alias, MetricSupplier<T> gauge)
+    {
+        setAliases(name, alias);
+        Gauge gaugeLoc = super.gauge(name.getMetricName(), gauge);
+        register(alias, gaugeLoc);
+        return (T) gaugeLoc;
+    }
+
     public Timer timer(MetricName name)
     {
         return timer(name, DEFAULT_TIMER_UNIT);
@@ -107,17 +370,16 @@ public class CassandraMetricsRegistry extends MetricRegistry
         return timer(name, alias, DEFAULT_TIMER_UNIT);
     }
 
-    public SnapshottingTimer timer(MetricName name, TimeUnit durationUnit)
+    private SnapshottingTimer timer(MetricName name, TimeUnit durationUnit)
     {
-        SnapshottingTimer timer = register(name, new SnapshottingTimer(CassandraMetricsRegistry.createReservoir(durationUnit)));
-        registerMBean(timer, name.getMBeanName());
-        return timer;
+        return register(name, new SnapshottingTimer(CassandraMetricsRegistry.createReservoir(durationUnit)));
     }
 
     public SnapshottingTimer timer(MetricName name, MetricName alias, TimeUnit durationUnit)
     {
+        setAliases(name, alias);
         SnapshottingTimer timer = timer(name, durationUnit);
-        registerAlias(name, alias);
+        register(alias, timer);
         return timer;
     }
 
@@ -144,11 +406,13 @@ public class CassandraMetricsRegistry extends MetricRegistry
 
     public <T extends Metric> T register(MetricName name, T metric)
     {
+        if (metric instanceof MetricSet)
+            throw new IllegalArgumentException("MetricSet registration using MetricName is not supported");
+
         try
         {
-            register(name.getMetricName(), metric);
-            registerMBean(metric, name.getMBeanName());
-            return metric;
+            addUnknownMetric(name);
+            return super.register(name.getMetricName(), metric);
         }
         catch (IllegalArgumentException e)
         {
@@ -164,7 +428,7 @@ public class CassandraMetricsRegistry extends MetricRegistry
 
     public Optional<ThreadPoolMetrics> getThreadPoolMetrics(String poolName)
     {
-        return Optional.ofNullable(threadPoolMetrics.get(poolName));
+        return ofNullable(threadPoolMetrics.get(poolName));
     }
 
     ThreadPoolMetrics register(ThreadPoolMetrics metrics)
@@ -178,45 +442,35 @@ public class CassandraMetricsRegistry extends MetricRegistry
         threadPoolMetrics.remove(metrics.poolName, metrics);
     }
 
-    public <T extends Metric> T register(MetricName name, MetricName aliasName, T metric)
-    {
-        T ret = register(name, metric);
-        registerAlias(name, aliasName);
-        return ret;
-    }
-
     public <T extends Metric> T register(MetricName name, T metric, MetricName... aliases)
     {
-        T ret = register(name, metric);
-        for (MetricName aliasName : aliases)
-        {
-            registerAlias(name, aliasName);
-        }
-        return ret;
+        setAliases(ArrayUtils.addAll(new MetricName[]{name}, aliases));
+        T metricLoc = register(name, metric);
+        Stream.of(aliases).forEach(n -> register(n, metricLoc));
+        return metricLoc;
     }
 
-    public boolean remove(MetricName name)
+    public void remove(MetricName name)
     {
-        boolean removed = remove(name.getMetricName());
-
-        mBeanServer.unregisterMBean(name.getMBeanName(), MBeanWrapper.OnException.IGNORE);
-        return removed;
+        // Aliases are removed in onMetricRemoved by metrics listener.
+        remove(name.getMetricName());
     }
 
-    public boolean remove(MetricName name, MetricName... aliases)
+    public boolean remove(String name)
     {
-        if (remove(name))
-        {
-            for (MetricName alias : aliases)
-            {
-                removeAlias(alias);
-            }
-            return true;
-        }
-        return false;
+        LinkedList<String> delete = ofNullable(ALIASES.get(name))
+                                        .map(s -> s.stream().map(MetricName::getMetricName)
+                                                   .collect(Collectors.toCollection(LinkedList::new)))
+                                        .orElse(new LinkedList<>(Collections.singletonList(name)));
+        // Aliases are removed in onMetricRemoved by metrics listener.
+        Iterator<String> iter = delete.descendingIterator();
+        boolean removed = true;
+        while (iter.hasNext())
+            removed &= super.remove(iter.next());
+        return removed;
     }
 
-    public void registerMBean(Metric metric, ObjectName name)
+    private void registerMBean(Metric metric, ObjectName name, MBeanWrapper mBeanServer)
     {
         AbstractBean mbean;
 
@@ -233,22 +487,13 @@ public class CassandraMetricsRegistry extends MetricRegistry
         else
             throw new IllegalArgumentException("Unknown metric type: " + metric.getClass());
 
-        if (!mBeanServer.isRegistered(name))
+        if (mBeanServer != null && !mBeanServer.isRegistered(name))
             mBeanServer.registerMBean(mbean, name, MBeanWrapper.OnException.LOG);
     }
 
-    private void registerAlias(MetricName existingName, MetricName aliasName)
+    private void unregisterMBean(ObjectName name, MBeanWrapper mBeanWrapper)
     {
-        Metric existing = Metrics.getMetrics().get(existingName.getMetricName());
-        assert existing != null : existingName + " not registered";
-
-        registerMBean(existing, aliasName.getMBeanName());
-    }
-
-    private void removeAlias(MetricName name)
-    {
-        if (mBeanServer.isRegistered(name.getMBeanName()))
-            MBeanWrapper.instance.unregisterMBean(name.getMBeanName(), MBeanWrapper.OnException.IGNORE);
+        mBeanWrapper.unregisterMBean(name, MBeanWrapper.OnException.IGNORE);
     }
     
     /**
@@ -284,7 +529,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         }
     }
 
-
+    /**
+     * Exports a gauge as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.GaugeMetricRow}
+     * for the same functionality for virtual tables.
+     */
     public interface JmxGaugeMBean extends MetricMBean
     {
         Object getValue();
@@ -307,6 +555,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         }
     }
 
+    /**
+     * Exports a histogram as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.HistogramMetricRow}
+     * for the same functionality for virtual tables.
+     */
     public interface JmxHistogramMBean extends MetricMBean
     {
         long getCount();
@@ -437,6 +689,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         }
     }
 
+    /**
+     * Exports a counter as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.CounterMetricRow}
+     * for the same functionality for virtual tables.
+     */
     public interface JmxCounterMBean extends MetricMBean
     {
         long getCount();
@@ -459,6 +715,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         }
     }
 
+    /**
+     * Exports a meter as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.MeterMetricRow}
+     * for the same functionality for virtual tables.
+     */
     public interface JmxMeterMBean extends MetricMBean
     {
         long getCount();
@@ -474,6 +734,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         String getRateUnit();
     }
 
+    /**
+     * Exports a timer as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.TimerMetricRow}
+     * for the same functionality for virtual tables.
+     */
     private static class JmxMeter extends AbstractBean implements JmxMeterMBean
     {
         private final Metered metric;
@@ -531,6 +795,10 @@ public class CassandraMetricsRegistry extends MetricRegistry
         }
     }
 
+    /**
+     * Exports a timer as a JMX MBean, check corresponding {@link org.apache.cassandra.db.virtual.model.TimerMetricRow}
+     * for the same functionality for virtual tables.
+     */
     public interface JmxTimerMBean extends JmxMeterMBean
     {
         double getMin();
@@ -693,11 +961,13 @@ public class CassandraMetricsRegistry extends MetricRegistry
      */
     public static class MetricName implements Comparable<MetricName>
     {
+        public static final MetricName EMPTY = new MetricName(MetricName.class, "EMPTY");
         private final String group;
         private final String type;
         private final String name;
         private final String scope;
         private final String mBeanName;
+        private final String systemViewName;
 
         /**
          * Creates a new {@link MetricName} without a scope.
@@ -750,17 +1020,22 @@ public class CassandraMetricsRegistry extends MetricRegistry
             this(group, type, name, scope, createMBeanName(group, type, name, scope));
         }
 
+        public MetricName(String group, String type, String name, String scope, String mBeanName)
+        {
+            this(group, type, name, scope, mBeanName, type);
+        }
+
         /**
          * Creates a new {@link MetricName} without a scope.
          *
-         * @param group     the group to which the {@link Metric} belongs
-         * @param type      the type to which the {@link Metric} belongs
-         * @param name      the name of the {@link Metric}
-         * @param scope     the scope of the {@link Metric}
-         * @param mBeanName the 'ObjectName', represented as a string, to use when registering the
-         *                  MBean.
+         * @param group the group to which the {@link Metric} belongs
+         * @param type the type to which the {@link Metric} belongs
+         * @param name the name of the {@link Metric}
+         * @param scope the scope of the {@link Metric}
+         * @param mBeanName the 'ObjectName', represented as a string, to use when registering the MBean.
+         * @param systemViewName the name of the virtual table to which the {@link Metric} belongs.
          */
-        public MetricName(String group, String type, String name, String scope, String mBeanName)
+        public MetricName(String group, String type, String name, String scope, String mBeanName, String systemViewName)
         {
             if (group == null || type == null)
             {
@@ -775,6 +1050,7 @@ public class CassandraMetricsRegistry extends MetricRegistry
             this.name = name;
             this.scope = scope;
             this.mBeanName = mBeanName;
+            this.systemViewName = systemViewName;
         }
 
         /**
@@ -863,6 +1139,11 @@ public class CassandraMetricsRegistry extends MetricRegistry
             }
         }
 
+        public String getSystemViewName()
+        {
+            return systemViewName;
+        }
+
         @Override
         public boolean equals(Object o)
         {
@@ -963,6 +1244,106 @@ public class CassandraMetricsRegistry extends MetricRegistry
             return name;
         }
     }
-}
 
+    private static class CassandraJmxMetricsExporter extends BaseMetricRegistryListener
+    {
+        private final MBeanWrapper mBeanWrapper = MBeanWrapper.instance;
+        private final Map<String, Deque<MetricName>> aliases;
 
+        public CassandraJmxMetricsExporter(Map<String, Deque<MetricName>> aliases)
+        {
+            this.aliases = aliases;
+        }
+
+        protected void onMetricAdded(String name, Metric metric)
+        {
+            Deque<MetricName> deque = aliases.get(name);
+            if (deque == null)
+                return;
+
+            assert deque.getFirst().getMetricName().equals(name);
+            Metrics.registerMBean(metric, deque.getFirst().getMBeanName(), mBeanWrapper);
+        }
+
+        protected void onMetricRemove(String name)
+        {
+            Deque<MetricName> deque = aliases.get(name);
+            if (deque == null)
+                return;
+
+            assert deque.getFirst().getMetricName().equals(name);
+            Metrics.unregisterMBean(deque.getFirst().getMBeanName(), mBeanWrapper);
+        }
+    }
+
+    private static abstract class BaseMetricRegistryListener implements MetricRegistryListener
+    {
+        protected void onMetricAdded(String name, Metric metric)
+        {
+        }
+
+        protected void onMetricRemove(String name)
+        {
+        }
+
+        @Override
+        public void onGaugeAdded(String metricName, Gauge<?> gauge)
+        {
+            onMetricAdded(metricName, gauge);
+        }
+
+        @Override
+        public void onGaugeRemoved(String metricName)
+        {
+            onMetricRemove(metricName);
+        }
+
+        @Override
+        public void onCounterAdded(String metricName, Counter counter)
+        {
+            onMetricAdded(metricName, counter);
+        }
+
+        @Override
+        public void onCounterRemoved(String metricName)
+        {
+            onMetricRemove(metricName);
+        }
+
+        @Override
+        public void onHistogramAdded(String metricName, Histogram histogram)
+        {
+            onMetricAdded(metricName, histogram);
+        }
+
+        @Override
+        public void onHistogramRemoved(String metricName)
+        {
+            onMetricRemove(metricName);
+        }
+
+        @Override
+        public void onMeterAdded(String metricName, Meter meter)
+        {
+            onMetricAdded(metricName, meter);
+        }
+
+        @Override
+        public void onMeterRemoved(String metricName)
+        {
+            onMetricRemove(metricName);
+        }
+
+        @Override
+        public void onTimerAdded(String metricName, Timer timer)
+        {
+            onMetricAdded(metricName, timer);
+        }
+
+        @Override
+        public void onTimerRemoved(String metricName)
+        {
+            onMetricRemove(metricName);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java b/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java
index 8195aafbf3..a208998c9a 100644
--- a/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ChunkCacheMetrics.java
@@ -33,6 +33,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class ChunkCacheMetrics extends CacheMetrics implements StatsCounter
 {
+    public static final String TYPE_NAME = "ChunkCache";
     /** Latency of misses */
     public final Timer missLatency;
 
@@ -43,7 +44,7 @@ public class ChunkCacheMetrics extends CacheMetrics implements StatsCounter
      */
     public ChunkCacheMetrics(ChunkCache cache)
     {
-        super("ChunkCache", cache);
+        super(TYPE_NAME, cache);
         missLatency = Metrics.timer(factory.createMetricName("MissLatency"));
     }
 
diff --git a/src/java/org/apache/cassandra/metrics/ClientMessageSizeMetrics.java b/src/java/org/apache/cassandra/metrics/ClientMessageSizeMetrics.java
index acdbf2851b..416e6b44b4 100644
--- a/src/java/org/apache/cassandra/metrics/ClientMessageSizeMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientMessageSizeMetrics.java
@@ -28,7 +28,7 @@ import com.codahale.metrics.Histogram;
  */
 public class ClientMessageSizeMetrics
 {
-    private static final String TYPE = "ClientMessageSize";
+    public static final String TYPE = "ClientMessageSize";
     public static final Counter bytesReceived = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "BytesReceived", null));
     public static final Counter bytesSent = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "BytesSent", null));
     public static final Histogram bytesReceivedPerRequest = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesReceivedPerRequest", null), true);
diff --git a/src/java/org/apache/cassandra/metrics/ClientMetrics.java b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
index a8801bad5f..1a465506a6 100644
--- a/src/java/org/apache/cassandra/metrics/ClientMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientMetrics.java
@@ -47,9 +47,10 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public final class ClientMetrics
 {
+    public static final String TYPE_NAME = "Client";
     public static final ClientMetrics instance = new ClientMetrics();
 
-    private static final MetricNameFactory factory = new DefaultNameFactory("Client");
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     private volatile boolean initialized = false;
     private Server server = null;
@@ -282,9 +283,7 @@ public final class ClientMetrics
 
     private <T> Gauge<T> registerGauge(String name, String deprecated, Gauge<T> gauge)
     {
-        Gauge<T> registeredGauge = registerGauge(name, gauge);
-        Metrics.registerMBean(registeredGauge, factory.createMetricName(deprecated).getMBeanName());
-        return registeredGauge;
+        return Metrics.gauge(factory.createMetricName(name), factory.createMetricName(deprecated), () -> gauge);
     }
 
     private Meter registerMeter(String name)
diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java b/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java
index 408087051e..61fcc34bf1 100644
--- a/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientRequestMetrics.java
@@ -31,6 +31,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class ClientRequestMetrics extends LatencyMetrics
 {
+    public static final String TYPE_NAME = "ClientRequest";
     public final Meter timeouts;
     public final Meter unavailables;
     public final Meter failures;
@@ -42,7 +43,7 @@ public class ClientRequestMetrics extends LatencyMetrics
 
     public ClientRequestMetrics(String scope)
     {
-        super("ClientRequest", scope);
+        super(TYPE_NAME, scope);
 
         timeouts = Metrics.meter(factory.createMetricName("Timeouts"));
         unavailables = Metrics.meter(factory.createMetricName("Unavailables"));
diff --git a/src/java/org/apache/cassandra/metrics/ClientRequestSizeMetrics.java b/src/java/org/apache/cassandra/metrics/ClientRequestSizeMetrics.java
index 4def87ea16..f7719762a4 100644
--- a/src/java/org/apache/cassandra/metrics/ClientRequestSizeMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ClientRequestSizeMetrics.java
@@ -34,7 +34,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class ClientRequestSizeMetrics
 {
-    private static final String TYPE = "ClientRequestSize";
+    public static final String TYPE = "ClientRequestSize";
 
     public static final Counter totalColumnsRead = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "ColumnsRead", null));
     public static final Counter totalRowsRead = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "RowsRead", null));
diff --git a/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java b/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
index cb53575b2b..1c7390522a 100644
--- a/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CommitLogMetrics.java
@@ -30,7 +30,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class CommitLogMetrics
 {
-    public static final MetricNameFactory factory = new DefaultNameFactory("CommitLog");
+    public static final String TYPE_NAME = "CommitLog";
+    public static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     /** Number of completed tasks */
     public Gauge<Long> completedTasks;
diff --git a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
index 0fe1ec7418..8bd48e520e 100644
--- a/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/CompactionMetrics.java
@@ -39,7 +39,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class CompactionMetrics
 {
-    public static final MetricNameFactory factory = new DefaultNameFactory("Compaction");
+    public static final String TYPE_NAME = "Compaction";
+    public static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     /** Estimated number of compactions remaining to perform */
     public final Gauge<Integer> pendingTasks;
diff --git a/src/java/org/apache/cassandra/metrics/DenylistMetrics.java b/src/java/org/apache/cassandra/metrics/DenylistMetrics.java
index 0372787af9..e3c34ab357 100644
--- a/src/java/org/apache/cassandra/metrics/DenylistMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/DenylistMetrics.java
@@ -24,6 +24,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class DenylistMetrics
 {
+    public static final String TYPE_NAME = "StorageProxy";
     private final Meter writesRejected;
     private final Meter readsRejected;
     private final Meter rangeReadsRejected;
@@ -31,7 +32,7 @@ public class DenylistMetrics
 
     public DenylistMetrics()
     {
-        final MetricNameFactory factory = new DefaultNameFactory("StorageProxy", "PartitionDenylist");
+        final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, "PartitionDenylist");
         writesRejected = Metrics.meter(factory.createMetricName("WriteRejected"));
         readsRejected = Metrics.meter(factory.createMetricName("ReadRejected"));
         rangeReadsRejected = Metrics.meter(factory.createMetricName("RangeReadRejected"));
diff --git a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
index 91a680e50d..e17c24de7a 100644
--- a/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/DroppedMessageMetrics.java
@@ -35,7 +35,7 @@ import static org.apache.cassandra.metrics.DefaultNameFactory.createMetricName;
  */
 public class DroppedMessageMetrics
 {
-    private static final String TYPE = "DroppedMessage";
+    public static final String TYPE = "DroppedMessage";
 
     // backward compatibility for request metrics which names have changed in 4.0 as part of CASSANDRA-15066
     private static final ImmutableMap<Verb, String> REQUEST_VERB_ALIAS;
diff --git a/src/java/org/apache/cassandra/metrics/HintedHandoffMetrics.java b/src/java/org/apache/cassandra/metrics/HintedHandoffMetrics.java
index bc87218bbd..187f5afe43 100644
--- a/src/java/org/apache/cassandra/metrics/HintedHandoffMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/HintedHandoffMetrics.java
@@ -38,9 +38,10 @@ import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
  */
 public class HintedHandoffMetrics
 {
+    public static final String TYPE_NAME = "HintsService";
     private static final Logger logger = LoggerFactory.getLogger(HintedHandoffMetrics.class);
 
-    private static final MetricNameFactory factory = new DefaultNameFactory("HintsService");
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     /** Total number of hints which are not stored, This is not a cache. */
     private final LoadingCache<InetAddressAndPort, DifferencingCounter> notStored = Caffeine.newBuilder()
diff --git a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
index bcff389577..2a8ce92776 100644
--- a/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/HintsServiceMetrics.java
@@ -34,9 +34,10 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public final class HintsServiceMetrics
 {
+    public static final String TYPE_NAME = "HintsService";
     private static final Logger logger = LoggerFactory.getLogger(HintsServiceMetrics.class);
 
-    private static final MetricNameFactory factory = new DefaultNameFactory("HintsService");
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     public static final Meter hintsSucceeded = Metrics.meter(factory.createMetricName("HintsSucceeded"));
     public static final Meter hintsFailed    = Metrics.meter(factory.createMetricName("HintsFailed"));
diff --git a/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java b/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java
index cc8dae9f4c..16870321be 100644
--- a/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/InternodeInboundMetrics.java
@@ -27,6 +27,7 @@ import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName;
  */
 public class InternodeInboundMetrics
 {
+    public static final String TYPE_NAME = "InternodeInbound";
     private final MetricName corruptFramesRecovered;
     private final MetricName corruptFramesUnrecovered;
     private final MetricName errorBytes;
@@ -50,7 +51,7 @@ public class InternodeInboundMetrics
     public InternodeInboundMetrics(InetAddressAndPort peer, InboundMessageHandlers handlers)
     {
         // ipv6 addresses will contain colons, which are invalid in a JMX ObjectName
-        MetricNameFactory factory = new DefaultNameFactory("InboundConnection", peer.getHostAddressAndPortForJMX());
+        MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, peer.getHostAddressAndPortForJMX());
 
         register(corruptFramesRecovered = factory.createMetricName("CorruptFramesRecovered"), handlers::corruptFramesRecovered);
         register(corruptFramesUnrecovered = factory.createMetricName("CorruptFramesUnrecovered"), handlers::corruptFramesUnrecovered);
diff --git a/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java b/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java
index 2b0348eb55..f2a1a8bb6f 100644
--- a/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/InternodeOutboundMetrics.java
@@ -119,7 +119,7 @@ public class InternodeOutboundMetrics
         // ipv6 addresses will contain colons, which are invalid in a JMX ObjectName
         address = ip.getHostAddressAndPortForJMX();
 
-        factory = new DefaultNameFactory("Connection", address);
+        factory = new DefaultNameFactory(TYPE_NAME, address);
 
         largeMessagePendingTasks = Metrics.register(factory.createMetricName("LargeMessagePendingTasks"), messagingPool.large::pendingCount);
         largeMessagePendingBytes = Metrics.register(factory.createMetricName("LargeMessagePendingBytes"), messagingPool.large::pendingBytes);
diff --git a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
index ae15bbf95a..83a1651fa7 100644
--- a/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/KeyspaceMetrics.java
@@ -43,6 +43,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class KeyspaceMetrics
 {
+    public static final String TYPE_NAME = "keyspace";
     /** Total amount of live data stored in the memtable, excluding any data structure overhead */
     public final Gauge<Long> memtableLiveDataSize;
     /** Total amount of data stored in the memtable that resides on-heap, including column related overhead and partitions overwritten. */
@@ -424,11 +425,11 @@ public class KeyspaceMetrics
 
             StringBuilder mbeanName = new StringBuilder();
             mbeanName.append(groupName).append(":");
-            mbeanName.append("type=Keyspace");
+            mbeanName.append("type=").append("Keyspace");
             mbeanName.append(",keyspace=").append(keyspaceName);
             mbeanName.append(",name=").append(metricName);
 
-            return new MetricName(groupName, "keyspace", metricName, keyspaceName, mbeanName.toString());
+            return new MetricName(groupName, TYPE_NAME, metricName, keyspaceName, mbeanName.toString());
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/LatencyMetrics.java b/src/java/org/apache/cassandra/metrics/LatencyMetrics.java
index af8ad71be1..e4abd40bbf 100644
--- a/src/java/org/apache/cassandra/metrics/LatencyMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/LatencyMetrics.java
@@ -99,8 +99,8 @@ public class LatencyMetrics
         }
         else
         {
-            latency = Metrics.register(factory.createMetricName(namePrefix + "Latency"), aliasFactory.createMetricName(namePrefix + "Latency"), timer);
-            totalLatency = Metrics.register(factory.createMetricName(namePrefix + "TotalLatency"), aliasFactory.createMetricName(namePrefix + "TotalLatency"), counter);
+            latency = Metrics.register(factory.createMetricName(namePrefix + "Latency"), timer, aliasFactory.createMetricName(namePrefix + "Latency"));
+            totalLatency = Metrics.register(factory.createMetricName(namePrefix + "TotalLatency"), counter, aliasFactory.createMetricName(namePrefix + "TotalLatency"));
         }
     }
     
@@ -164,16 +164,9 @@ public class LatencyMetrics
         {
             parent.removeChildren(this);
         }
-        if (aliasFactory == null)
-        {
-            Metrics.remove(factory.createMetricName(namePrefix + "Latency"));
-            Metrics.remove(factory.createMetricName(namePrefix + "TotalLatency"));
-        }
-        else
-        {
-            Metrics.remove(factory.createMetricName(namePrefix + "Latency"), aliasFactory.createMetricName(namePrefix + "Latency"));
-            Metrics.remove(factory.createMetricName(namePrefix + "TotalLatency"), aliasFactory.createMetricName(namePrefix + "TotalLatency"));
-        }
+        // Aliases are already known to the parent metrics, so we don't need to remove them here.
+        Metrics.remove(factory.createMetricName(namePrefix + "Latency"));
+        Metrics.remove(factory.createMetricName(namePrefix + "TotalLatency"));
     }
 
     public class LatencyMetricsTimer extends Timer
diff --git a/src/java/org/apache/cassandra/metrics/MessagingMetrics.java b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java
index bef6d08737..6151f57ee4 100644
--- a/src/java/org/apache/cassandra/metrics/MessagingMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/MessagingMetrics.java
@@ -49,7 +49,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class MessagingMetrics implements InboundMessageHandlers.GlobalMetricCallbacks
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Messaging");
+    public static final String TYPE_NAME = "Messaging";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
     private static final Logger logger = LoggerFactory.getLogger(MessagingMetrics.class);
     private static final int LOG_DROPPED_INTERVAL_IN_MS = 5000;
     
diff --git a/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java b/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java
index bb32f0416f..c16cbe4060 100644
--- a/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java
@@ -28,7 +28,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class MutualTlsMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("MutualTls");
+    public static final String TYPE_NAME = "MutualTls";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     public final static MutualTlsMetrics instance = new MutualTlsMetrics();
 
diff --git a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java b/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
index 1e1ba89ba2..45088a1d17 100644
--- a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
@@ -25,7 +25,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class PaxosMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Paxos");
+    public static final String TYPE_NAME = "Paxos";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
     public static final Counter linearizabilityViolations = Metrics.counter(factory.createMetricName("LinearizabilityViolations"));
     public static final Meter repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairPaxosTopologyRetries"));
 
diff --git a/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java b/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
index 32d62efd20..1adb5dcd7f 100644
--- a/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
@@ -26,7 +26,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class ReadRepairMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("ReadRepair");
+    public static final String TYPE_NAME = "ReadRepair";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     public static final Meter repairedBlocking = Metrics.meter(factory.createMetricName("RepairedBlocking"));
     public static final Meter reconcileRead = Metrics.meter(factory.createMetricName("ReconcileRead"));
diff --git a/src/java/org/apache/cassandra/metrics/StorageMetrics.java b/src/java/org/apache/cassandra/metrics/StorageMetrics.java
index 57b57e831f..1cd65c0896 100644
--- a/src/java/org/apache/cassandra/metrics/StorageMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/StorageMetrics.java
@@ -32,6 +32,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class StorageMetrics
 {
+    public static final String TYPE_NAME = "Storage";
     public static final MetricNameFactory factory = new DefaultNameFactory("Storage");
 
     public static final Counter load = Metrics.counter(factory.createMetricName("Load"));
diff --git a/src/java/org/apache/cassandra/metrics/StreamingMetrics.java b/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
index 5246908637..eb5d840063 100644
--- a/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
@@ -78,7 +78,7 @@ public class StreamingMetrics
 
     public StreamingMetrics(final InetAddressAndPort peer)
     {
-        MetricNameFactory factory = new DefaultNameFactory("Streaming", peer.toString().replace(':', '.'));
+        MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, peer.toString().replace(':', '.'));
         incomingBytes = Metrics.counter(factory.createMetricName("IncomingBytes"));
         outgoingBytes= Metrics.counter(factory.createMetricName("OutgoingBytes"));
         incomingProcessTime = Metrics.timer(factory.createMetricName("IncomingProcessTime"));
diff --git a/src/java/org/apache/cassandra/metrics/TCMMetrics.java b/src/java/org/apache/cassandra/metrics/TCMMetrics.java
index 61a3b9d1e9..134a1a34e2 100644
--- a/src/java/org/apache/cassandra/metrics/TCMMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/TCMMetrics.java
@@ -33,7 +33,8 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
 
 public class TCMMetrics
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("TCM");
+    public static final String TYPE_NAME = "TCM";
+    private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
 
     public static final TCMMetrics instance = new TCMMetrics();
 
diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java
index 410562cdaa..7ab1cbbfbf 100644
--- a/src/java/org/apache/cassandra/metrics/TableMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java
@@ -71,13 +71,17 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime;
  */
 public class TableMetrics
 {
+    public static final String TYPE_NAME = "Table";
+    public static final String INDEX_TYPE_NAME = "IndexTable";
+    public static final String ALIAS_TYPE_NAME = "ColumnFamily";
+    public static final String INDEX_ALIAS_TYPE_NAME = "IndexColumnFamily";
     /**
      * stores metrics that will be rolled into a single global metric
      */
     private static final ConcurrentMap<String, Set<Metric>> ALL_TABLE_METRICS = Maps.newConcurrentMap();
     public static final long[] EMPTY = new long[0];
-    private static final MetricNameFactory GLOBAL_FACTORY = new AllTableMetricNameFactory("Table");
-    private static final MetricNameFactory GLOBAL_ALIAS_FACTORY = new AllTableMetricNameFactory("ColumnFamily");
+    private static final MetricNameFactory GLOBAL_FACTORY = new AllTableMetricNameFactory(TYPE_NAME);
+    private static final MetricNameFactory GLOBAL_ALIAS_FACTORY = new AllTableMetricNameFactory(ALIAS_TYPE_NAME);
 
     public final static LatencyMetrics GLOBAL_READ_LATENCY = new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Read");
     public final static LatencyMetrics GLOBAL_WRITE_LATENCY = new LatencyMetrics(GLOBAL_FACTORY, GLOBAL_ALIAS_FACTORY, "Write");
@@ -403,8 +407,8 @@ public class TableMetrics
      */
     public TableMetrics(final ColumnFamilyStore cfs, ReleasableMetric memtableMetrics)
     {
-        factory = new TableMetricNameFactory(cfs, "Table");
-        aliasFactory = new TableMetricNameFactory(cfs, "ColumnFamily");
+        factory = new TableMetricNameFactory(cfs, cfs.isIndex() ? INDEX_TYPE_NAME : TYPE_NAME);
+        aliasFactory = new TableMetricNameFactory(cfs, cfs.isIndex() ? INDEX_ALIAS_TYPE_NAME : ALIAS_TYPE_NAME);
 
         if (memtableMetrics != null)
         {
@@ -932,10 +936,10 @@ public class TableMetrics
 
     protected <G,T> Gauge<T> createTableGauge(String name, String alias, Gauge<T> gauge, Gauge<G> globalGauge)
     {
-        Gauge<T> cfGauge = Metrics.register(factory.createMetricName(name), aliasFactory.createMetricName(alias), gauge);
+        Gauge<T> cfGauge = Metrics.register(factory.createMetricName(name), gauge, aliasFactory.createMetricName(alias));
         if (register(name, alias, cfGauge) && globalGauge != null)
         {
-            Metrics.register(GLOBAL_FACTORY.createMetricName(name), GLOBAL_ALIAS_FACTORY.createMetricName(alias), globalGauge);
+            Metrics.register(GLOBAL_FACTORY.createMetricName(name), globalGauge, GLOBAL_ALIAS_FACTORY.createMetricName(alias));
         }
         return cfGauge;
     }
@@ -984,19 +988,14 @@ public class TableMetrics
         if (register(name, alias, cfCounter))
         {
             Metrics.register(GLOBAL_FACTORY.createMetricName(name),
-                             GLOBAL_ALIAS_FACTORY.createMetricName(alias),
-                             new Gauge<Long>()
-            {
-                public Long getValue()
-                {
-                    long total = 0;
-                    for (Metric cfGauge : ALL_TABLE_METRICS.get(name))
+                    (Gauge<Long>) () ->
                     {
-                        total += ((Counter) cfGauge).getCount();
-                    }
-                    return total;
-                }
-            });
+                        long total = 0;
+                        for (Metric cfGauge : ALL_TABLE_METRICS.get(name))
+                            total += ((Counter) cfGauge).getCount();
+                        return total;
+                    },
+                    GLOBAL_ALIAS_FACTORY.createMetricName(alias));
         }
         return cfCounter;
     }
@@ -1151,16 +1150,8 @@ public class TableMetrics
         {
             // Metric will be null if we are releasing a view metric.  Views have null for ViewLockAcquireTime and ViewLockReadTime
             ALL_TABLE_METRICS.get(tableMetricName).remove(metric);
-            CassandraMetricsRegistry.MetricName cfAlias = aliasFactory.createMetricName(cfMetricName);
-            
-            if (tableMetricAlias != null)
-            {
-                Metrics.remove(name, cfAlias, factory.createMetricName(tableMetricAlias), aliasFactory.createMetricName(tableMetricAlias));
-            }
-            else
-            {
-                Metrics.remove(name, cfAlias);
-            }
+            // Aliases are already known to the parent metrics, so we don't need to remove them here.
+            Metrics.remove(name);
         }
     }
 
@@ -1258,21 +1249,18 @@ public class TableMetrics
     {
         private final String keyspaceName;
         private final String tableName;
-        private final boolean isIndex;
         private final String type;
 
         TableMetricNameFactory(ColumnFamilyStore cfs, String type)
         {
             this.keyspaceName = cfs.getKeyspaceName();
             this.tableName = cfs.name;
-            this.isIndex = cfs.isIndex();
             this.type = type;
         }
 
         public CassandraMetricsRegistry.MetricName createMetricName(String metricName)
         {
             String groupName = TableMetrics.class.getPackage().getName();
-            String type = isIndex ? "Index" + this.type : this.type;
 
             StringBuilder mbeanName = new StringBuilder();
             mbeanName.append(groupName).append(":");
diff --git a/src/java/org/apache/cassandra/metrics/ThreadPoolMetrics.java b/src/java/org/apache/cassandra/metrics/ThreadPoolMetrics.java
index 95ab601fe5..c8a4dedc63 100644
--- a/src/java/org/apache/cassandra/metrics/ThreadPoolMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ThreadPoolMetrics.java
@@ -33,6 +33,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class ThreadPoolMetrics
 {
+    public static final String TYPE_NAME = "ThreadPools";
     public static final String ACTIVE_TASKS = "ActiveTasks";
     public static final String PENDING_TASKS = "PendingTasks";
     public static final String COMPLETED_TASKS = "CompletedTasks";
@@ -123,10 +124,10 @@ public class ThreadPoolMetrics
     private static MetricName makeMetricName(String path, String poolName, String metricName)
     {
         return new MetricName("org.apache.cassandra.metrics",
-                              "ThreadPools",
+                              TYPE_NAME,
                               metricName,
                               path + '.' + poolName,
-                              format("org.apache.cassandra.metrics:type=ThreadPools,path=%s,scope=%s,name=%s",
-                                     path, poolName, metricName));
+                              format("org.apache.cassandra.metrics:type=%s,path=%s,scope=%s,name=%s",
+                                     TYPE_NAME, path, poolName, metricName));
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java b/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java
index 9343503999..a308fbbab8 100644
--- a/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java
+++ b/src/java/org/apache/cassandra/metrics/TrieMemtableMetricsView.java
@@ -21,9 +21,11 @@ package org.apache.cassandra.metrics;
 import com.codahale.metrics.Counter;
 
 import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+import static org.apache.cassandra.metrics.DefaultNameFactory.GROUP_NAME;
 
 public class TrieMemtableMetricsView
 {
+    public static final String TYPE_NAME = "TrieMemtable";
     private static final String UNCONTENDED_PUTS = "Uncontended memtable puts";
     private static final String CONTENDED_PUTS = "Contended memtable puts";
     private static final String CONTENTION_TIME = "Contention time";
@@ -74,17 +76,14 @@ public class TrieMemtableMetricsView
 
         public CassandraMetricsRegistry.MetricName createMetricName(String metricName)
         {
-            String groupName = TableMetrics.class.getPackage().getName();
-            String type = "TrieMemtable";
-
             StringBuilder mbeanName = new StringBuilder();
-            mbeanName.append(groupName).append(":");
-            mbeanName.append("type=").append(type);
+            mbeanName.append(GROUP_NAME).append(":");
+            mbeanName.append("type=").append(TYPE_NAME);
             mbeanName.append(",keyspace=").append(keyspace);
             mbeanName.append(",scope=").append(table);
             mbeanName.append(",name=").append(metricName);
 
-            return new CassandraMetricsRegistry.MetricName(groupName, type, metricName, keyspace + "." + table, mbeanName.toString());
+            return new CassandraMetricsRegistry.MetricName(GROUP_NAME, TYPE_NAME, metricName, keyspace + '.' + table, mbeanName.toString());
         }
     }
 }
diff --git a/src/java/org/apache/cassandra/metrics/UnweightedCacheMetrics.java b/src/java/org/apache/cassandra/metrics/UnweightedCacheMetrics.java
index ff40f3a038..b651a51995 100644
--- a/src/java/org/apache/cassandra/metrics/UnweightedCacheMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/UnweightedCacheMetrics.java
@@ -27,6 +27,7 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
  */
 public class UnweightedCacheMetrics extends AbstractCacheMetrics
 {
+    public static final String TYPE_NAME = "UnweightedCache";
     /**
      * Cache capacity (maximum number of entries)
      */
@@ -45,7 +46,7 @@ public class UnweightedCacheMetrics extends AbstractCacheMetrics
      */
     public UnweightedCacheMetrics(String type, UnweightedCacheSize cache)
     {
-        super(new DefaultNameFactory("UnweightedCache", type), type);
+        super(new DefaultNameFactory(TYPE_NAME, type), type);
 
         maxEntries = Metrics.register(factory.createMetricName("MaxEntries"), cache::maxEntries);
         entries = Metrics.register(factory.createMetricName("Entries"), cache::entries);
diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java
index a6561e2522..bc733d367e 100644
--- a/src/java/org/apache/cassandra/schema/SchemaConstants.java
+++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java
@@ -50,6 +50,7 @@ public final class SchemaConstants
     public static final String VIRTUAL_SCHEMA = "system_virtual_schema";
 
     public static final String VIRTUAL_VIEWS = "system_views";
+    public static final String VIRTUAL_METRICS = "system_metrics";
 
     public static final String DUMMY_KEYSPACE_OR_TABLE_NAME = "--dummy--";
 
diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java
index 8518a9ba99..4ae0413529 100644
--- a/src/java/org/apache/cassandra/service/CassandraDaemon.java
+++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java
@@ -32,7 +32,6 @@ import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
-import javax.management.ObjectName;
 import javax.management.StandardMBean;
 import javax.management.remote.JMXConnectorServer;
 
@@ -57,6 +56,7 @@ import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.db.SystemKeyspaceMigrator41;
 import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.db.virtual.SystemViewsKeyspace;
+import org.apache.cassandra.db.virtual.VirtualKeyspace;
 import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
 import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -100,6 +100,8 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VERSI
 import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VM_NAME;
 import static org.apache.cassandra.config.CassandraRelevantProperties.SIZE_RECORDER_INTERVAL;
 import static org.apache.cassandra.config.CassandraRelevantProperties.START_NATIVE_TRANSPORT;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.createMetricsKeyspaceTables;
+import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_METRICS;
 
 /**
  * The <code>CassandraDaemon</code> is an abstraction for a Cassandra daemon
@@ -132,8 +134,7 @@ public class CassandraDaemon
                 int separator = metricName.lastIndexOf('.');
                 String appenderName = metricName.substring(0, separator);
                 String metric = metricName.substring(separator + 1); // remove "."
-                ObjectName name = DefaultNameFactory.createMetricName(appenderName, metric, null).getMBeanName();
-                CassandraMetricsRegistry.Metrics.registerMBean(meter, name);
+                CassandraMetricsRegistry.Metrics.register(DefaultNameFactory.createMetricName(appenderName, metric, null), meter);
             }
         });
         logger = LoggerFactory.getLogger(CassandraDaemon.class);
@@ -546,6 +547,7 @@ public class CassandraDaemon
     {
         VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance);
         VirtualKeyspaceRegistry.instance.register(SystemViewsKeyspace.instance);
+        VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(VIRTUAL_METRICS, createMetricsKeyspaceTables()));
 
         // flush log messages to system_views.system_logs virtual table as there were messages already logged
         // before that virtual table was instantiated
diff --git a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
index 198f92286f..7cd8dc4a20 100644
--- a/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
+++ b/src/java/org/apache/cassandra/utils/EstimatedHistogram.java
@@ -442,4 +442,10 @@ public class EstimatedHistogram implements DoubleToLongFunction
             return size;
         }
     }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf(mean());
+    }
 }
diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java
index d3728f95d8..414811aff3 100644
--- a/src/java/org/apache/cassandra/utils/FBUtilities.java
+++ b/src/java/org/apache/cassandra/utils/FBUtilities.java
@@ -1402,6 +1402,9 @@ public class FBUtilities
 
     public static String camelToSnake(String camel)
     {
+        if (camel.chars().allMatch(Character::isUpperCase))
+            return camel.toLowerCase();
+
         StringBuilder sb = new StringBuilder();
         for (char c : camel.toCharArray())
         {
diff --git a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
index 26c47912a2..f77ea2fab1 100644
--- a/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
+++ b/src/java/org/apache/cassandra/utils/memory/MemtablePool.java
@@ -29,6 +29,7 @@ import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Timer;
 import org.apache.cassandra.metrics.CassandraMetricsRegistry;
 import org.apache.cassandra.metrics.DefaultNameFactory;
+import org.apache.cassandra.metrics.MetricNameFactory;
 import org.apache.cassandra.utils.concurrent.WaitQueue;
 import org.apache.cassandra.utils.ExecutorUtils;
 
@@ -41,6 +42,7 @@ import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue;
  */
 public abstract class MemtablePool
 {
+    public static final String TYPE_NAME = "MemtablePool";
     final MemtableCleanerThread<?> cleaner;
 
     // the total memory used by this pool
@@ -59,7 +61,7 @@ public abstract class MemtablePool
         this.onHeap = getSubPool(maxOnHeapMemory, cleanThreshold);
         this.offHeap = getSubPool(maxOffHeapMemory, cleanThreshold);
         this.cleaner = getCleaner(cleaner);
-        DefaultNameFactory nameFactory = new DefaultNameFactory("MemtablePool");
+        MetricNameFactory nameFactory = new DefaultNameFactory(TYPE_NAME);
         blockedOnAllocating = CassandraMetricsRegistry.Metrics.timer(nameFactory.createMetricName("BlockedOnAllocation"));
         numPendingTasks = CassandraMetricsRegistry.Metrics.register(nameFactory.createMetricName("PendingFlushTasks"),
                                                                     () -> (long) this.cleaner.numPendingTasks());
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index cb96bfff24..4eb5039094 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -51,6 +51,9 @@ import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 import javax.annotation.Nullable;
 import javax.management.MBeanServerConnection;
 import javax.management.remote.JMXConnector;
@@ -88,6 +91,7 @@ import com.datastax.driver.core.Session;
 import com.datastax.driver.core.SimpleStatement;
 import com.datastax.driver.core.SocketOptions;
 import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.TypeCodec;
 import com.datastax.driver.core.UDTValue;
 import com.datastax.driver.core.UserType;
 import com.datastax.driver.core.exceptions.UnauthorizedException;
@@ -134,6 +138,7 @@ import org.apache.cassandra.db.marshal.TupleType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.db.marshal.VectorType;
+import org.apache.cassandra.db.virtual.VirtualKeyspace;
 import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
 import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace;
 import org.apache.cassandra.dht.Murmur3Partitioner;
@@ -160,9 +165,9 @@ import org.apache.cassandra.serializers.TypeSerializer;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.tcm.ClusterMetadataService;
 import org.apache.cassandra.transport.Event;
 import org.apache.cassandra.transport.Message;
-import org.apache.cassandra.tcm.ClusterMetadataService;
 import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.transport.SimpleClient;
@@ -187,8 +192,10 @@ import static org.apache.cassandra.cql3.SchemaElement.SchemaElementType.FUNCTION
 import static org.apache.cassandra.cql3.SchemaElement.SchemaElementType.MATERIALIZED_VIEW;
 import static org.apache.cassandra.cql3.SchemaElement.SchemaElementType.TABLE;
 import static org.apache.cassandra.cql3.SchemaElement.SchemaElementType.TYPE;
-import static org.junit.Assert.assertNotNull;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.createMetricsKeyspaceTables;
+import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_METRICS;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -477,6 +484,11 @@ public abstract class CQLTester
         user = null;
     }
 
+    protected static void addMetricsKeyspace()
+    {
+        VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(VIRTUAL_METRICS, createMetricsKeyspaceTables()));
+    }
+
     protected void resetSchema() throws Throwable
     {
         for (TableMetadata table : SchemaKeyspace.metadata().tables)
@@ -1578,7 +1590,7 @@ public abstract class CQLTester
         return sessions.computeIfAbsent(new ClusterSettings(user, protocolVersion, useEncryption, useClientCert), settings -> cluster.connect());
     }
 
-    private Cluster getCluster(ProtocolVersion protocolVersion)
+    protected Cluster getCluster(ProtocolVersion protocolVersion)
     {
         return clusters.computeIfAbsent(new ClusterSettings(user, protocolVersion, useEncryption, useClientCert), settings -> initClientCluster(user, protocolVersion, useEncryption, useClientCert));
     }
@@ -1764,6 +1776,96 @@ public abstract class CQLTester
                                         rows.length>i ? "less" : "more", rows.length, i, protocolVersion), i == rows.length);
     }
 
+    public void assertRowsContains(ResultSet result, Object[]... rows)
+    {
+        assertRowsContains(getCluster(getDefaultVersion()), result, rows);
+    }
+
+    public void assertRowsContains(ResultSet result, List<Object[]> rows)
+    {
+        assertRowsContains(getCluster(getDefaultVersion()), result, rows);
+    }
+
+    public static void assertRowsContains(Cluster cluster, ResultSet result, Object[]... rows)
+    {
+        assertRowsContains(cluster, result, rows == null ? Collections.emptyList() : Arrays.asList(rows));
+    }
+
+    public static void assertRowsContains(Cluster cluster, ResultSet result, List<Object[]> rows)
+    {
+        if (result == null && rows.isEmpty())
+            return;
+        assertNotNull(String.format("No rows returned by query but %d expected", rows.size()), result);
+        assertTrue(result.iterator().hasNext());
+
+        // It is necessary that all rows match the column definitions
+        for (Object[] row : rows)
+        {
+            if (row == null || row.length == 0)
+                Assert.fail("Rows must not be null or empty");
+
+            if (result.getColumnDefinitions().size() == row.length)
+                continue;
+
+            Assert.fail(String.format("Rows do not match column definitions. Expected %d columns but got %d",
+                                      result.getColumnDefinitions().size(), row.length));
+        }
+
+        ColumnDefinitions defs = result.getColumnDefinitions();
+        int size = defs.size();
+        List<ByteBuffer[]> resultSetValues = StreamSupport.stream(result.spliterator(), false)
+                .map(row -> IntStream.range(0, size)
+                                .mapToObj(row::getBytesUnsafe)
+                                .toArray(ByteBuffer[]::new))
+                .collect(Collectors.toList());
+
+        AtomicInteger columnCounter = new AtomicInteger();
+        com.datastax.driver.core.ProtocolVersion version = com.datastax.driver.core.ProtocolVersion.fromInt(getDefaultVersion().asInt());
+        List<ByteBuffer[]> expectedRowsValues = rows.stream()
+                .map(row -> Stream.of(row)
+                        .map(cell -> {
+                            int index = columnCounter.getAndIncrement() % size;
+                            return cell instanceof ByteBuffer ? (ByteBuffer) cell :
+                                    cluster.getConfiguration()
+                                            .getCodecRegistry()
+                                            .codecFor(defs.getType(index))
+                                            .serialize(cell, version);
+                        })
+                        .toArray(ByteBuffer[]::new))
+                .collect(Collectors.toList());
+
+        int found = 0;
+        for (ByteBuffer[] expected : expectedRowsValues)
+            for (ByteBuffer[] actual : resultSetValues)
+                if (Arrays.equals(expected, actual))
+                    found++;
+
+        if (found == expectedRowsValues.size())
+            return;
+
+        Assert.fail(String.format("Result set does not contain expected rows. Result set %s doesn't contain %s",
+                resultSetValues.stream().map(e -> deserializeCells(e, cluster.getConfiguration().getCodecRegistry(),
+                        result.getColumnDefinitions(), version)).collect(Collectors.joining(", ")),
+                expectedRowsValues.stream().map(e -> deserializeCells(e, cluster.getConfiguration().getCodecRegistry(),
+                        result.getColumnDefinitions(), version)).collect(Collectors.joining(", "))));
+    }
+
+    private static String deserializeCells(ByteBuffer[] row,
+                                           com.datastax.driver.core.CodecRegistry codecRegistry,
+                                           ColumnDefinitions columnDefinitions,
+                                           com.datastax.driver.core.ProtocolVersion version)
+    {
+        AtomicInteger index = new AtomicInteger();
+        int size = columnDefinitions.size();
+        return Stream.of(row)
+                .map(b -> {
+                    int idx = index.getAndIncrement() % size;
+                    TypeCodec<Object> codec = codecRegistry.codecFor(columnDefinitions.getType(idx));
+                    return codec.format(codec.deserialize(b, version));
+                })
+                .collect(Collectors.joining(", ", "[", "]"));
+    }
+
     private static String safeToString(Supplier<String> fn)
     {
         try
diff --git a/test/unit/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapterTest.java b/test/unit/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapterTest.java
new file mode 100644
index 0000000000..2a009b791c
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/CollectionVirtualTableAdapterTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.virtual.model.CollectionEntry;
+import org.apache.cassandra.db.virtual.model.CollectionEntryTestRow;
+import org.apache.cassandra.db.virtual.model.PartitionEntryTestRow;
+import org.apache.cassandra.db.virtual.walker.CollectionEntryTestRowWalker;
+import org.apache.cassandra.db.virtual.walker.PartitionEntryTestRowWalker;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRIC_SCOPE_UNDEFINED;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test that verifies the functionality of the collection virtual table adapter, which is used to expose internal
+ * collection data as a virtual table. This test is a bit more complex than the other virtual table tests because it
+ * tests the functionality of the virtual table adapter itself, rather than just the virtual table.
+ */
+public class CollectionVirtualTableAdapterTest extends CQLTester
+{
+    private static final String KS_NAME = "vts";
+    private static final String VT_NAME = "collection_virtual_table";
+    private static final String VT_NAME_1 = "map_key_filter_virtual_table";
+    private static final String VT_NAME_2 = "map_value_filter_virtual_table";
+    private final List<VirtualTable> tables = new ArrayList<>();
+    private final List<CollectionEntry> internalTestCollection = new ArrayList<>();
+    private final Map<String, CollectionEntry> internalTestMap = new HashMap<>();
+
+    private static void addSinglePartitionData(Collection<CollectionEntry> list)
+    {
+        list.add(new CollectionEntry("1984", "key", 3, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+        list.add(new CollectionEntry("1984", "key", 2, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+        list.add(new CollectionEntry("1984", "key", 1, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+    }
+
+    private static void addMultiPartitionData(Collection<CollectionEntry> list)
+    {
+        addSinglePartitionData(list);
+        list.add(new CollectionEntry("1985", "key", 3, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+        list.add(new CollectionEntry("1985", "key", 2, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+        list.add(new CollectionEntry("1985", "key", 1, "value",
+                                     1, 1, 1, (short) 1, (byte) 1, true));
+    }
+
+    @Before
+    public void config() throws Exception
+    {
+        tables.add(CollectionVirtualTableAdapter.create(
+            KS_NAME,
+            VT_NAME,
+            "The collection virtual table",
+            new CollectionEntryTestRowWalker(),
+            internalTestCollection,
+            CollectionEntryTestRow::new));
+        tables.add(CollectionVirtualTableAdapter.createSinglePartitionedKeyFiltered(
+            KS_NAME,
+            VT_NAME_1,
+            "The partition key filtered virtual table",
+            new PartitionEntryTestRowWalker(),
+            internalTestMap,
+            internalTestMap::containsKey,
+            PartitionEntryTestRow::new));
+        tables.add(CollectionVirtualTableAdapter.createSinglePartitionedValueFiltered(
+            KS_NAME,
+            VT_NAME_2,
+            "The partition value filtered virtual table",
+            new PartitionEntryTestRowWalker(),
+            internalTestMap,
+            value -> value instanceof CollectionEntryExt,
+            PartitionEntryTestRow::new));
+        VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, tables));
+    }
+
+    @After
+    public void postCleanup()
+    {
+        internalTestCollection.clear();
+        internalTestMap.clear();
+    }
+
+    @Test
+    public void testSelectAll()
+    {
+        addSinglePartitionData(internalTestCollection);
+        List<CollectionEntry> sortedClustering = new ArrayList<>(internalTestCollection);
+        sortedClustering.sort(Comparator.comparingLong(CollectionEntry::getOrderedKey));
+        ResultSet result = executeNet(String.format("SELECT * FROM %s.%s", KS_NAME, VT_NAME));
+
+        int index = 0;
+        for (Row row : result)
+        {
+            assertEquals(sortedClustering.get(index).getPrimaryKey(), row.getString("primary_key"));
+            assertEquals(sortedClustering.get(index).getSecondaryKey(), row.getString("secondary_key"));
+            assertEquals(sortedClustering.get(index).getOrderedKey(), row.getLong("ordered_key"));
+            assertEquals(sortedClustering.get(index).getIntValue(), row.getInt("int_value"));
+            assertEquals(sortedClustering.get(index).getLongValue(), row.getLong("long_value"));
+            assertEquals(sortedClustering.get(index).getValue(), row.getString("value"));
+            assertEquals(sortedClustering.get(index).getDoubleValue(), row.getDouble("double_value"), 0.0);
+            assertEquals(sortedClustering.get(index).getShortValue(), row.getShort("short_value"));
+            assertEquals(sortedClustering.get(index).getByteValue(), row.getByte("byte_value"));
+            assertEquals(sortedClustering.get(index).getBooleanValue(), row.getBool("boolean_value"));
+            index++;
+        }
+        assertEquals(sortedClustering.size(), index);
+    }
+
+    @Test
+    public void testSelectPartition()
+    {
+        addMultiPartitionData(internalTestCollection);
+        ResultSet result = executeNet(
+            String.format("SELECT * FROM %s.%s WHERE primary_key = ? AND secondary_key = ?", KS_NAME, VT_NAME),
+            "1984", "key");
+
+        AtomicInteger size = new AtomicInteger(3);
+        result.forEach(row -> {
+            assertEquals("1984", row.getString("primary_key"));
+            assertEquals("key", row.getString("secondary_key"));
+            size.decrementAndGet();
+        });
+        assertEquals(0, size.get());
+    }
+
+    @Test
+    public void testSelectPartitionMap()
+    {
+        internalTestMap.put("1984", new CollectionEntry("primary", "key", 3, "value",
+                                                        1, 1, 1, (short) 1, (byte) 1, true));
+        ResultSet result = executeNet(String.format("SELECT * FROM %s.%s WHERE key = ?", KS_NAME, VT_NAME_1),
+                                      "1984");
+
+        AtomicInteger size = new AtomicInteger(1);
+        result.forEach(row -> {
+            assertEquals("1984", row.getString("key"));
+            assertEquals("primary", row.getString("primary_key"));
+            assertEquals("key", row.getString("secondary_key"));
+            size.decrementAndGet();
+        });
+        assertEquals(0, size.get());
+    }
+
+    @Test
+    public void testSelectPartitionUnknownKey()
+    {
+        internalTestMap.put("1984", new CollectionEntry("primary", "key", 3, "value",
+                                                        1, 1, 1, (short) 1, (byte) 1, true));
+        ResultSet first = executeNet(String.format("SELECT * FROM %s.%s WHERE key = ?", KS_NAME, VT_NAME_1),
+                                     METRIC_SCOPE_UNDEFINED);
+        assertEquals(0, first.all().size());
+
+        addSinglePartitionData(internalTestCollection);
+        ResultSet second = executeNet(
+            String.format("SELECT * FROM %s.%s WHERE primary_key = ? AND secondary_key = ?", KS_NAME, VT_NAME),
+            METRIC_SCOPE_UNDEFINED, "key");
+        assertEquals(0, second.all().size());
+    }
+
+    @Test
+    public void testSelectPartitionValueFitered()
+    {
+        internalTestMap.put("1984", new CollectionEntry("primary", "key", 3, "value",
+                                                        1, 1, 1, (short) 1, (byte) 1, true));
+        internalTestMap.put("1985", new CollectionEntryExt("primary", "key", 3, "value",
+                                                           1, 1, 1, (short) 1, (byte) 1, true, "extra"));
+
+        ResultSet first = executeNet(String.format("SELECT * FROM %s.%s WHERE key = ?", KS_NAME, VT_NAME_2),
+                                     "1984");
+        assertEquals(0, first.all().size());
+
+        ResultSet second = executeNet(String.format("SELECT * FROM %s.%s WHERE key = ?", KS_NAME, VT_NAME_2),
+                                      "1985");
+        assertEquals(1, second.all().size());
+    }
+
+    @Test
+    public void testSelectEmptyPartition()
+    {
+        addSinglePartitionData(internalTestCollection);
+        assertRowsNet(executeNet(String.format("SELECT * FROM %s.%s WHERE primary_key = 'EMPTY'", KS_NAME, VT_NAME)));
+    }
+
+    @Test
+    public void testSelectEmptyCollection()
+    {
+        internalTestCollection.clear();
+        assertRowsNet(executeNet(String.format("SELECT * FROM %s.%s", KS_NAME, VT_NAME)));
+    }
+
+    private static class CollectionEntryExt extends CollectionEntry
+    {
+        public CollectionEntryExt(String primaryKey,
+                                  String secondaryKey,
+                                  long orderedKey,
+                                  String value,
+                                  int intValue,
+                                  long longValue,
+                                  double doubleValue,
+                                  short shortValue,
+                                  byte byteValue,
+                                  boolean booleanValue,
+                                  String extraColumn)
+        {
+            super(primaryKey, secondaryKey, orderedKey, value, intValue, longValue, doubleValue, shortValue, byteValue,
+                  booleanValue);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntry.java b/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntry.java
new file mode 100644
index 0000000000..70c86f5a51
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntry.java
@@ -0,0 +1,126 @@
+/*
+ * 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.model;
+
+/**
+ * Test collection entry represents an internal collection entry of a Cassandra collection.
+ */
+public class CollectionEntry
+{
+    private final String primaryKey;
+    private final String secondaryKey;
+    private final long orderedKey;
+    private final String value;
+    private final int intValue;
+    private final long longValue;
+    private final double doubleValue;
+    private final short shortValue;
+    private final byte byteValue;
+    private final boolean booleanValue;
+
+    public CollectionEntry(String primaryKey,
+                           String secondaryKey,
+                           long orderedKey,
+                           String value,
+                           int intValue,
+                           long longValue,
+                           double doubleValue,
+                           short shortValue,
+                           byte byteValue,
+                           boolean booleanValue)
+    {
+        this.primaryKey = primaryKey;
+        this.secondaryKey = secondaryKey;
+        this.orderedKey = orderedKey;
+        this.value = value;
+        this.intValue = intValue;
+        this.longValue = longValue;
+        this.doubleValue = doubleValue;
+        this.shortValue = shortValue;
+        this.byteValue = byteValue;
+        this.booleanValue = booleanValue;
+    }
+
+    public String getPrimaryKey()
+    {
+        return primaryKey;
+    }
+
+    public String getSecondaryKey()
+    {
+        return secondaryKey;
+    }
+
+    public long getOrderedKey()
+    {
+        return orderedKey;
+    }
+
+    public String getValue()
+    {
+        return value;
+    }
+
+    public int getIntValue()
+    {
+        return intValue;
+    }
+
+    public long getLongValue()
+    {
+        return longValue;
+    }
+
+    public double getDoubleValue()
+    {
+        return doubleValue;
+    }
+
+    public short getShortValue()
+    {
+        return shortValue;
+    }
+
+    public byte getByteValue()
+    {
+        return byteValue;
+    }
+
+    public boolean getBooleanValue()
+    {
+        return booleanValue;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "CollectionEntry{" +
+               "primaryKey='" + primaryKey + '\'' +
+               ", secondaryKey='" + secondaryKey + '\'' +
+               ", orderedKey=" + orderedKey +
+               ", value='" + value + '\'' +
+               ", intValue=" + intValue +
+               ", longValue=" + longValue +
+               ", doubleValue=" + doubleValue +
+               ", shortValue=" + shortValue +
+               ", byteValue=" + byteValue +
+               ", booleanValue=" + booleanValue +
+               '}';
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntryTestRow.java b/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntryTestRow.java
new file mode 100644
index 0000000000..8d12f4e50d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/model/CollectionEntryTestRow.java
@@ -0,0 +1,93 @@
+/*
+ * 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.model;
+
+
+/**
+ * Test meter metric test representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class CollectionEntryTestRow
+{
+    private final CollectionEntry collectionEntry;
+
+    public CollectionEntryTestRow(CollectionEntry collectionEntry)
+    {
+        this.collectionEntry = collectionEntry;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String primaryKey()
+    {
+        return collectionEntry.getPrimaryKey();
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String secondaryKey()
+    {
+        return collectionEntry.getSecondaryKey();
+    }
+
+    @Column(type = Column.Type.CLUSTERING)
+    public long orderedKey()
+    {
+        return collectionEntry.getOrderedKey();
+    }
+
+    @Column
+    public long longValue()
+    {
+        return collectionEntry.getLongValue();
+    }
+
+    @Column
+    public int intValue()
+    {
+        return collectionEntry.getIntValue();
+    }
+
+    @Column
+    public short shortValue()
+    {
+        return collectionEntry.getShortValue();
+    }
+
+    @Column
+    public byte byteValue()
+    {
+        return collectionEntry.getByteValue();
+    }
+
+    @Column
+    public double doubleValue()
+    {
+        return collectionEntry.getDoubleValue();
+    }
+
+    @Column
+    public String value()
+    {
+        return collectionEntry.getValue();
+    }
+
+    @Column
+    public boolean booleanValue()
+    {
+        return collectionEntry.getBooleanValue();
+    }
+}
diff --git a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java b/test/unit/org/apache/cassandra/db/virtual/model/PartitionEntryTestRow.java
similarity index 51%
copy from src/java/org/apache/cassandra/metrics/PaxosMetrics.java
copy to test/unit/org/apache/cassandra/db/virtual/model/PartitionEntryTestRow.java
index 1e1ba89ba2..10ebdefd5b 100644
--- a/src/java/org/apache/cassandra/metrics/PaxosMetrics.java
+++ b/test/unit/org/apache/cassandra/db/virtual/model/PartitionEntryTestRow.java
@@ -16,18 +16,38 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.metrics;
+package org.apache.cassandra.db.virtual.model;
 
-import com.codahale.metrics.Counter;
-import com.codahale.metrics.Meter;
 
-import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
-
-public class PaxosMetrics
+/**
+ * Test meter metric test representation for a {@link org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter}.
+ */
+public class PartitionEntryTestRow
 {
-    private static final MetricNameFactory factory = new DefaultNameFactory("Paxos");
-    public static final Counter linearizabilityViolations = Metrics.counter(factory.createMetricName("LinearizabilityViolations"));
-    public static final Meter repairPaxosTopologyRetries = Metrics.meter(factory.createMetricName("RepairPaxosTopologyRetries"));
+    private final String key;
+    private final CollectionEntry collectionEntry;
+
+    public PartitionEntryTestRow(String key, CollectionEntry collectionEntry)
+    {
+        this.key = key;
+        this.collectionEntry = collectionEntry;
+    }
+
+    @Column(type = Column.Type.PARTITION_KEY)
+    public String key()
+    {
+        return key;
+    }
+
+    @Column
+    public String primaryKey()
+    {
+        return collectionEntry.getPrimaryKey();
+    }
 
-    public static void initialize() {}
+    @Column
+    public String secondaryKey()
+    {
+        return collectionEntry.getSecondaryKey();
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/virtual/walker/CollectionEntryTestRowWalker.java b/test/unit/org/apache/cassandra/db/virtual/walker/CollectionEntryTestRowWalker.java
new file mode 100644
index 0000000000..f6ea8d6904
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/walker/CollectionEntryTestRowWalker.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.CollectionEntryTestRow;
+import org.apache.cassandra.db.virtual.model.Column;
+
+/**
+ * aThe {@link org.apache.cassandra.db.virtual.model.CollectionEntryTestRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.CollectionEntryTestRow
+ */
+public class CollectionEntryTestRowWalker implements RowWalker<CollectionEntryTestRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "primary_key", String.class);
+        visitor.accept(Column.Type.PARTITION_KEY, "secondary_key", String.class);
+        visitor.accept(Column.Type.CLUSTERING, "ordered_key", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "boolean_value", Boolean.TYPE);
+        visitor.accept(Column.Type.REGULAR, "byte_value", Byte.TYPE);
+        visitor.accept(Column.Type.REGULAR, "double_value", Double.TYPE);
+        visitor.accept(Column.Type.REGULAR, "int_value", Integer.TYPE);
+        visitor.accept(Column.Type.REGULAR, "long_value", Long.TYPE);
+        visitor.accept(Column.Type.REGULAR, "short_value", Short.TYPE);
+        visitor.accept(Column.Type.REGULAR, "value", String.class);
+    }
+
+    @Override
+    public void visitRow(CollectionEntryTestRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "primary_key", String.class, row::primaryKey);
+        visitor.accept(Column.Type.PARTITION_KEY, "secondary_key", String.class, row::secondaryKey);
+        visitor.accept(Column.Type.CLUSTERING, "ordered_key", Long.TYPE, row::orderedKey);
+        visitor.accept(Column.Type.REGULAR, "boolean_value", Boolean.TYPE, row::booleanValue);
+        visitor.accept(Column.Type.REGULAR, "byte_value", Byte.TYPE, row::byteValue);
+        visitor.accept(Column.Type.REGULAR, "double_value", Double.TYPE, row::doubleValue);
+        visitor.accept(Column.Type.REGULAR, "int_value", Integer.TYPE, row::intValue);
+        visitor.accept(Column.Type.REGULAR, "long_value", Long.TYPE, row::longValue);
+        visitor.accept(Column.Type.REGULAR, "short_value", Short.TYPE, row::shortValue);
+        visitor.accept(Column.Type.REGULAR, "value", String.class, row::value);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case CLUSTERING:
+                return 1;
+            case PARTITION_KEY:
+                return 2;
+            case REGULAR:
+                return 7;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/virtual/walker/PartitionEntryTestRowWalker.java b/test/unit/org/apache/cassandra/db/virtual/walker/PartitionEntryTestRowWalker.java
new file mode 100644
index 0000000000..241f3b68df
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/walker/PartitionEntryTestRowWalker.java
@@ -0,0 +1,61 @@
+/*
+ * 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
+ *
+ *      https://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.walker;
+
+import org.apache.cassandra.db.virtual.model.Column;
+import org.apache.cassandra.db.virtual.model.PartitionEntryTestRow;
+
+/**
+ * The {@link org.apache.cassandra.db.virtual.model.PartitionEntryTestRow} row metadata and data walker.
+ *
+ * @see org.apache.cassandra.db.virtual.model.PartitionEntryTestRow
+ */
+public class PartitionEntryTestRowWalker implements RowWalker<PartitionEntryTestRow>
+{
+    @Override
+    public void visitMeta(MetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "key", String.class);
+        visitor.accept(Column.Type.REGULAR, "primary_key", String.class);
+        visitor.accept(Column.Type.REGULAR, "secondary_key", String.class);
+    }
+
+    @Override
+    public void visitRow(PartitionEntryTestRow row, RowMetadataVisitor visitor)
+    {
+        visitor.accept(Column.Type.PARTITION_KEY, "key", String.class, row::key);
+        visitor.accept(Column.Type.REGULAR, "primary_key", String.class, row::primaryKey);
+        visitor.accept(Column.Type.REGULAR, "secondary_key", String.class, row::secondaryKey);
+    }
+
+    @Override
+    public int count(Column.Type type)
+    {
+        switch (type)
+        {
+            case CLUSTERING:
+                return 0;
+            case PARTITION_KEY:
+                return 1;
+            case REGULAR:
+                return 2;
+            default:
+                throw new IllegalStateException("Unknown column type: " + type);
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/metrics/BatchMetricsTest.java b/test/unit/org/apache/cassandra/metrics/BatchMetricsTest.java
index b90f19a7fc..6cd0886a01 100644
--- a/test/unit/org/apache/cassandra/metrics/BatchMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/BatchMetricsTest.java
@@ -34,7 +34,10 @@ import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.EmbeddedCassandraService;
 
+import static org.apache.cassandra.cql3.CQLTester.assertRowsContains;
+import static org.apache.cassandra.cql3.CQLTester.row;
 import static org.apache.cassandra.cql3.statements.BatchStatement.metrics;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRIC_SCOPE_UNDEFINED;
 import static org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir.EstimatedHistogramReservoirSnapshot;
 import static org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir.Range;
 import static org.junit.Assert.assertEquals;
@@ -164,6 +167,14 @@ public class BatchMetricsTest
             assertEquals(expectedPartitionsPerLoggedBatchCount, metrics.partitionsPerLoggedBatch.getCount());
             assertEquals(expectedPartitionsPerCounterBatchCount, metrics.partitionsPerCounterBatch.getCount());
 
+            assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.batch_group"),
+                    row("org.apache.cassandra.metrics.Batch.PartitionsPerUnloggedBatch",
+                        METRIC_SCOPE_UNDEFINED, "histogram", String.valueOf(metrics.partitionsPerUnloggedBatch.getSnapshot().getMedian())),
+                    row("org.apache.cassandra.metrics.Batch.PartitionsPerLoggedBatch",
+                        METRIC_SCOPE_UNDEFINED, "histogram", String.valueOf(metrics.partitionsPerLoggedBatch.getSnapshot().getMedian())),
+                    row("org.apache.cassandra.metrics.Batch.PartitionsPerCounterBatch",
+                        METRIC_SCOPE_UNDEFINED, "histogram", String.valueOf(metrics.partitionsPerCounterBatch.getSnapshot().getMedian())));
+
             EstimatedHistogramReservoirSnapshot partitionsPerLoggedBatchSnapshot = (EstimatedHistogramReservoirSnapshot) metrics.partitionsPerLoggedBatch.getSnapshot();
             EstimatedHistogramReservoirSnapshot partitionsPerUnloggedBatchSnapshot = (EstimatedHistogramReservoirSnapshot) metrics.partitionsPerUnloggedBatch.getSnapshot();
             EstimatedHistogramReservoirSnapshot partitionsPerCounterBatchSnapshot = (EstimatedHistogramReservoirSnapshot) metrics.partitionsPerCounterBatch.getSnapshot();
diff --git a/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java b/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java
index c41ded37db..4c447a7012 100644
--- a/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/CQLMetricsTest.java
@@ -35,6 +35,9 @@ import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.EmbeddedCassandraService;
 
+import static org.apache.cassandra.cql3.CQLTester.assertRowsContains;
+import static org.apache.cassandra.cql3.CQLTester.row;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRIC_SCOPE_UNDEFINED;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -85,6 +88,10 @@ public class CQLMetricsTest
         {
             DatabaseDescriptor.setUseStatementsEnabled(true);
         }
+
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.cql_group"),
+                row("org.apache.cassandra.metrics.CQL.UseStatementsExecuted", METRIC_SCOPE_UNDEFINED, "counter",
+                        String.valueOf(useCountBefore)));
     }
 
     @Test
@@ -96,6 +103,10 @@ public class CQLMetricsTest
         Assert.assertEquals(useCountBefore + 1, QueryProcessor.metrics.useStatementsExecuted.getCount());
         session.prepare("SELECT * FROM junit.metricstest WHERE id = ?");
         assertEquals(n+2, (int) QueryProcessor.metrics.preparedStatementsCount.getValue());
+
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.cql_group"),
+                row("org.apache.cassandra.metrics.CQL.PreparedStatementsCount", METRIC_SCOPE_UNDEFINED, "gauge",
+                        String.valueOf(QueryProcessor.metrics.preparedStatementsCount.getValue())));
     }
 
     @Test
@@ -128,6 +139,12 @@ public class CQLMetricsTest
 
         assertEquals(10, QueryProcessor.metrics.preparedStatementsExecuted.getCount());
         assertEquals(0, QueryProcessor.metrics.regularStatementsExecuted.getCount());
+
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.cql_group"),
+                row("org.apache.cassandra.metrics.CQL.RegularStatementsExecuted", METRIC_SCOPE_UNDEFINED, "counter",
+                        String.valueOf(QueryProcessor.metrics.regularStatementsExecuted.getCount())),
+                row("org.apache.cassandra.metrics.CQL.PreparedStatementsExecuted", METRIC_SCOPE_UNDEFINED, "counter",
+                        String.valueOf(QueryProcessor.metrics.preparedStatementsExecuted.getCount())));
     }
 
     @Test
@@ -145,6 +162,10 @@ public class CQLMetricsTest
         for (int i = 0; i < 10; i++)
             session.execute(String.format("INSERT INTO junit.metricstest (id, val) VALUES (%d, '%s')", i, "val" + i));
         assertEquals(0.5, QueryProcessor.metrics.preparedStatementsRatio.getValue(), 0.0);
+
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.cql_group"),
+                row("org.apache.cassandra.metrics.CQL.PreparedStatementsRatio", METRIC_SCOPE_UNDEFINED, "gauge",
+                        String.valueOf(QueryProcessor.metrics.preparedStatementsRatio.getValue())));
     }
 
     private void clearMetrics()
diff --git a/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java b/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java
index 02e4228c3f..897265ef4d 100644
--- a/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java
+++ b/test/unit/org/apache/cassandra/metrics/CassandraMetricsRegistryTest.java
@@ -20,22 +20,37 @@
  */
 package org.apache.cassandra.metrics;
 
-import static org.junit.Assert.*;
-
 import java.lang.management.ManagementFactory;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
-import com.codahale.metrics.Timer;
-import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName;
-
+import org.junit.Before;
 import org.junit.Test;
 
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistryListener;
+import com.codahale.metrics.Timer;
 import com.codahale.metrics.jvm.BufferPoolMetricSet;
 import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
 import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
+import org.apache.cassandra.metrics.CassandraMetricsRegistry.MetricName;
 import org.apache.cassandra.utils.EstimatedHistogram;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 
 public class CassandraMetricsRegistryTest
 {
@@ -44,6 +59,12 @@ public class CassandraMetricsRegistryTest
     {
     }
 
+    @Before
+    public void prepare()
+    {
+        CassandraMetricsRegistry.Metrics.getNames().forEach(CassandraMetricsRegistry.Metrics::remove);
+    }
+
     @Test
     public void testChooseType()
     {
@@ -90,6 +111,14 @@ public class CassandraMetricsRegistryTest
         }
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testMetricSetRegistration()
+    {
+        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
+        DefaultNameFactory factory = new DefaultNameFactory("ThreadPools", "TestMetricSetRegistration");
+        registry.register(factory.createMetricName("jvm.buffers"), new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
+    }
+
     @Test
     public void testDeltaBaseCase()
     {
@@ -156,6 +185,84 @@ public class CassandraMetricsRegistryTest
         assertEquals(expectedBucketsWithValues, bucketsWithValues);
     }
 
+    @Test
+    public void testMetricAliasesOrder()
+    {
+        String dummy = "defaultName";
+        LinkedList<MetricName> aliases = new LinkedList<>();
+        int size = ThreadLocalRandom.current().nextInt(10, 1000);
+        MetricName first = DefaultNameFactory.createMetricName("Table", "FirstTestMetricAliasesOrder", "FirstScope");
+        for (int i = 0; i < size; i++)
+            aliases.add(DefaultNameFactory.createMetricName("Table", "FirstTestMetricAliasesOrder" + UUID.randomUUID(),
+                                                            UUID.randomUUID().toString()));
+
+        Meter metric = CassandraMetricsRegistry.Metrics.meter(dummy);
+        LinkedList<MetricName> verify = new LinkedList<>(aliases);
+        verify.addFirst(first);
+        MetricRegistryListener listener;
+        CassandraMetricsRegistry.Metrics.addListener(listener = new MetricRegistryListener.Base()
+        {
+            @Override
+            public void onMeterAdded(String name, Meter meter)
+            {
+                if (dummy.equals(name))
+                    return;
+
+                assertEquals(verify.removeFirst().getMetricName(), name);
+            }
+        });
+        CassandraMetricsRegistry.Metrics.register(first, metric, aliases.toArray(new MetricName[size]));
+        CassandraMetricsRegistry.Metrics.removeListener(listener);
+
+        List<String> all = CassandraMetricsRegistry.Metrics.getMetrics().keySet().
+                                                           stream()
+                                                           .filter(m -> m.contains("FirstTestMetricAliasesOrder"))
+                                                           .collect(Collectors.toList());
+
+        assertNotNull(all);
+        assertEquals(size + 1, all.size());
+
+        CassandraMetricsRegistry.Metrics.remove(first.getMetricName());
+        Map<String, Metric> metrics = CassandraMetricsRegistry.Metrics.getMetrics();
+        assertEquals(1, metrics.size());
+        assertEquals(dummy, metrics.keySet().iterator().next());
+
+        CassandraMetricsRegistry.Metrics.remove(dummy);
+        assertTrue(CassandraMetricsRegistry.Metrics.getMetrics().isEmpty());
+    }
+
+    @Test
+    public void testMetricAliasesRegistration()
+    {
+        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
+        MetricNameFactory factory = new DefaultNameFactory("Table", "MetricsTestScope");
+        MetricNameFactory aliasFactory = new DefaultNameFactory("Table", "MetricsTestAliasScope");
+
+        MetricName meter = factory.createMetricName("TestMeter");
+        MetricName meterAlias = aliasFactory.createMetricName("TestMeterAlias");
+        registry.meter(meter, meterAlias);
+
+        MetricName histogram = factory.createMetricName("TestHistogram");
+        MetricName histogramAlias = aliasFactory.createMetricName("TestHistogramAlias");
+        registry.histogram(histogram, histogramAlias, false);
+
+        MetricName counter = factory.createMetricName("TestCounter");
+        MetricName counterAlias = aliasFactory.createMetricName("TestCounterAlias");
+        registry.counter(counter, counterAlias);
+
+        MetricName timer = factory.createMetricName("TestTimer");
+        MetricName timerAlias = aliasFactory.createMetricName("TestTimerAlias");
+        registry.timer(timer, timerAlias);
+
+        Set<String> aliases = new HashSet<>(Arrays.asList(meter.getMetricName(), meterAlias.getMetricName(),
+                                                          histogram.getMetricName(), histogramAlias.getMetricName(),
+                                                          counter.getMetricName(), counterAlias.getMetricName(),
+                                                          timer.getMetricName(), timerAlias.getMetricName()));
+
+        assertTrue(registry.getMetrics().keySet().containsAll(aliases));
+        assertTrue(registry.getNames().containsAll(aliases));
+    }
+
     private boolean inRange(long anchor, long input, double range)
     {
         return input / ((double) anchor) < range;
diff --git a/test/unit/org/apache/cassandra/metrics/ClientRequestMetricsTest.java b/test/unit/org/apache/cassandra/metrics/ClientRequestMetricsTest.java
index b4e9b085d2..88060ffd27 100644
--- a/test/unit/org/apache/cassandra/metrics/ClientRequestMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/ClientRequestMetricsTest.java
@@ -36,11 +36,12 @@ import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.EmbeddedCassandraService;
 import org.apache.cassandra.service.reads.range.RangeCommandIterator;
 
+import static com.datastax.driver.core.Cluster.builder;
+import static org.apache.cassandra.cql3.CQLTester.assertRowsContains;
+import static org.apache.cassandra.cql3.CQLTester.row;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 
-import static com.datastax.driver.core.Cluster.builder;
-
 public class ClientRequestMetricsTest
 {
     private static Cluster cluster;
@@ -99,6 +100,8 @@ public class ClientRequestMetricsTest
 
         assertEquals(0, readMetricsContainer.compareLocalRequest());
         assertEquals(0, readMetricsContainer.compareRemoteRequest());
+
+        assertMetricsVirtualTable(writeMetricsContainer, readMetricsContainer);
     }
 
     @Test
@@ -114,6 +117,8 @@ public class ClientRequestMetricsTest
 
         assertEquals(1, writeMetricsContainer.compareLocalRequest());
         assertEquals(0, writeMetricsContainer.compareRemoteRequest());
+
+        assertMetricsVirtualTable(writeMetricsContainer, readMetricsContainer);
     }
 
     @Test
@@ -129,6 +134,8 @@ public class ClientRequestMetricsTest
 
         assertEquals(10, writeMetricsContainer.compareLocalRequest());
         assertEquals(0, writeMetricsContainer.compareRemoteRequest());
+
+        assertMetricsVirtualTable(writeMetricsContainer, readMetricsContainer);
     }
 
     @Test
@@ -146,6 +153,8 @@ public class ClientRequestMetricsTest
 
         assertEquals(0, writeMetricsContainer.compareLocalRequest());
         assertEquals(0, writeMetricsContainer.compareRemoteRequest());
+
+        assertMetricsVirtualTable(writeMetricsContainer, readMetricsContainer);
     }
 
     @Test
@@ -163,6 +172,8 @@ public class ClientRequestMetricsTest
 
         assertEquals(0, writeMetricsContainer.compareLocalRequest());
         assertEquals(0, writeMetricsContainer.compareRemoteRequest());
+
+        assertMetricsVirtualTable(writeMetricsContainer, readMetricsContainer);
     }
 
     @Test
@@ -178,6 +189,19 @@ public class ClientRequestMetricsTest
         assertThat(RangeCommandIterator.rangeMetrics.latency.getCount()).isEqualTo(latencyCount + 1);
     }
 
+    private void assertMetricsVirtualTable(ClientRequestMetricsContainer writeMetricsContainer, ClientRequestMetricsContainer readMetricsContainer)
+    {
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.client_request_group"),
+                row("org.apache.cassandra.metrics.ClientRequest.LocalRequests.Read", "Read", "meter",
+                        String.valueOf(readMetricsContainer.getMetrics().localRequests.getCount())),
+                row("org.apache.cassandra.metrics.ClientRequest.LocalRequests.Write", "Write", "meter",
+                        String.valueOf(writeMetricsContainer.getMetrics().localRequests.getCount())),
+                row("org.apache.cassandra.metrics.ClientRequest.RemoteRequests.Read", "Read", "meter",
+                        String.valueOf(readMetricsContainer.getMetrics().remoteRequests.getCount())),
+                row("org.apache.cassandra.metrics.ClientRequest.RemoteRequests.Write", "Write", "meter",
+                        String.valueOf(writeMetricsContainer.getMetrics().remoteRequests.getCount())));
+    }
+
     private void clearHistogram(Histogram histogram)
     {
         ((ClearableHistogram) histogram).clear();
@@ -206,6 +230,11 @@ public class ClientRequestMetricsTest
         {
             return metrics.remoteRequests.getCount() - remoteRequests;
         }
+
+        public ClientRequestMetrics getMetrics()
+        {
+            return metrics;
+        }
     }
 
     private void executeWrite(int id, int ord, String val)
diff --git a/test/unit/org/apache/cassandra/metrics/ClientRequestRowAndColumnMetricsTest.java b/test/unit/org/apache/cassandra/metrics/ClientRequestRowAndColumnMetricsTest.java
index 80a3d3e0ce..b94ed8db48 100644
--- a/test/unit/org/apache/cassandra/metrics/ClientRequestRowAndColumnMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/ClientRequestRowAndColumnMetricsTest.java
@@ -38,9 +38,9 @@ import org.apache.cassandra.transport.SimpleClient;
 import org.apache.cassandra.transport.messages.BatchMessage;
 import org.apache.cassandra.transport.messages.QueryMessage;
 
-import static org.junit.Assert.assertEquals;
-
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRIC_SCOPE_UNDEFINED;
 import static org.apache.cassandra.transport.ProtocolVersion.CURRENT;
+import static org.junit.Assert.assertEquals;
 
 public class ClientRequestRowAndColumnMetricsTest extends CQLTester
 {
@@ -48,6 +48,7 @@ public class ClientRequestRowAndColumnMetricsTest extends CQLTester
     public static void setup()
     {
         requireNetwork();
+        addMetricsKeyspace();
     }
 
     @Before
@@ -73,6 +74,9 @@ public class ClientRequestRowAndColumnMetricsTest extends CQLTester
         assertEquals(2, ClientRequestSizeMetrics.totalRowsRead.getCount());
         // The partition key is provided by the client in the request, so we don't consider those columns as read.
         assertEquals(4, ClientRequestSizeMetrics.totalColumnsRead.getCount());
+        assertRowsContains(executeNet("SELECT * FROM system_metrics.client_request_size_group"),
+                row("org.apache.cassandra.metrics.ClientRequestSize.RowsRead", METRIC_SCOPE_UNDEFINED, "counter", "2"),
+                row("org.apache.cassandra.metrics.ClientRequestSize.ColumnsRead", METRIC_SCOPE_UNDEFINED, "counter", "4"));
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/metrics/JmxVirtualTableMetricsTest.java b/test/unit/org/apache/cassandra/metrics/JmxVirtualTableMetricsTest.java
new file mode 100644
index 0000000000..6f8c8b9822
--- /dev/null
+++ b/test/unit/org/apache/cassandra/metrics/JmxVirtualTableMetricsTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.metrics;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import javax.management.JMX;
+import javax.management.ObjectName;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.virtual.CollectionVirtualTableAdapter;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.METRICS_GROUP_POSTFIX;
+import static org.apache.cassandra.schema.SchemaConstants.VIRTUAL_METRICS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Test compares JMX metrics to virtual table metrics values, basically all metric values must be equal and
+ * have the same representation in both places.
+ */
+public class JmxVirtualTableMetricsTest extends CQLTester
+{
+    private final Map<MetricType, Metric> metricToNameMap = new EnumMap<>(MetricType.class);
+    private final AtomicInteger gaugeValue = new AtomicInteger(123);
+
+    @BeforeClass
+    public static void setup() throws Exception
+    {
+        startJMXServer();
+        createMBeanServerConnection();
+        addMetricsKeyspace();
+    }
+
+    @Before
+    public void beforeTest()
+    {
+        metricToNameMap.clear();
+        MetricRegistry registry = new MetricRegistry();
+
+        metricToNameMap.put(MetricType.METER, registry.meter("meter"));
+        metricToNameMap.put(MetricType.COUNTER, registry.counter("counter"));
+        metricToNameMap.put(MetricType.HISTOGRAM, registry.histogram("histogram"));
+        metricToNameMap.put(MetricType.TIMER, registry.timer("timer"));
+        metricToNameMap.put(MetricType.GAUGE, registry.gauge("gauge", () -> gaugeValue::get));
+
+        CassandraMetricsRegistry.metricGroups.forEach(group -> {
+            MetricNameFactory factory = new DefaultNameFactory(group, "jmx.virtual");
+            CassandraMetricsRegistry.Metrics.register(factory.createMetricName(MetricType.METER.metricName),
+                                                      metricToNameMap.get(MetricType.METER));
+            CassandraMetricsRegistry.Metrics.register(factory.createMetricName(MetricType.COUNTER.metricName),
+                                                      metricToNameMap.get(MetricType.COUNTER));
+            CassandraMetricsRegistry.Metrics.register(factory.createMetricName(MetricType.HISTOGRAM.metricName),
+                                                      metricToNameMap.get(MetricType.HISTOGRAM));
+            CassandraMetricsRegistry.Metrics.register(factory.createMetricName(MetricType.TIMER.metricName),
+                                                      metricToNameMap.get(MetricType.TIMER));
+            CassandraMetricsRegistry.Metrics.register(factory.createMetricName(MetricType.GAUGE.metricName),
+                                                      metricToNameMap.get(MetricType.GAUGE));
+        });
+    }
+
+    @Test
+    public void testJmxEqualVirtualTableByMetricGroup() throws Exception
+    {
+        Map<String, List<ObjectName>> mbeanByMetricGroup = jmxConnection.queryNames(null, null)
+                                                                        .stream()
+                                                                        .filter(this::isLocalMetric)
+                                                                        .collect(Collectors.groupingBy(
+                                                                            on -> requireNonNull(
+                                                                                on.getKeyPropertyList().get("type"))));
+
+        for (Map.Entry<String, List<ObjectName>> e : mbeanByMetricGroup.entrySet())
+        {
+            assertRowsContains(executeNet(String.format("SELECT * FROM %s.%s", VIRTUAL_METRICS,
+                                                        METRICS_GROUP_POSTFIX.apply(
+                                                            CollectionVirtualTableAdapter.virtualTableNameStyle(
+                                                                e.getKey())))),
+                               e.getValue().stream().map(this::makeMetricRow).collect(Collectors.toList()));
+        }
+    }
+
+    @Test
+    public void testJmxEqualVirtualTableByMetricType() throws Exception
+    {
+        Map<MetricType, List<ObjectName>> mbeanByMetricGroup = jmxConnection.queryNames(null, null)
+                                                                            .stream()
+                                                                            .filter(this::isLocalMetric)
+                                                                            .collect(Collectors.groupingBy(
+                                                                                on -> MetricType.find(
+                                                                                                    on.getKeyPropertyList().get("name"))
+                                                                                                .orElseThrow()));
+
+        for (Map.Entry<MetricType, List<ObjectName>> e : mbeanByMetricGroup.entrySet())
+        {
+            switch (e.getKey())
+            {
+                case METER:
+                    assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_meter", VIRTUAL_METRICS)),
+                                       e.getValue().stream().map(this::makeMeterRow).collect(Collectors.toList()));
+                    break;
+                case COUNTER:
+                    assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_counter", VIRTUAL_METRICS)),
+                                       e.getValue().stream().map(this::makeCounterRow).collect(Collectors.toList()));
+                    break;
+                case HISTOGRAM:
+                    assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_histogram", VIRTUAL_METRICS)),
+                                       e.getValue().stream().map(this::makeHistogramRow).collect(Collectors.toList()));
+                    break;
+                case TIMER:
+                    assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_timer", VIRTUAL_METRICS)),
+                                       e.getValue().stream().map(this::makeTimerRow).collect(Collectors.toList()));
+                    break;
+                case GAUGE:
+                    assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_gauge", VIRTUAL_METRICS)),
+                                       e.getValue().stream().map(this::makeGaugeRow).collect(Collectors.toList()));
+                    break;
+            }
+        }
+    }
+
+    @Test
+    public void testAliasesWithJmxVirtualTables() throws Exception
+    {
+        CassandraMetricsRegistry registry = CassandraMetricsRegistry.Metrics;
+        MetricNameFactory factory = new DefaultNameFactory("Table", "AliasTestScopeFirst");
+        MetricNameFactory aliasFactory = new DefaultNameFactory("Index", "AliasTestScopeSecond");
+
+        CassandraMetricsRegistry.MetricName meter = factory.createMetricName("TestMeter");
+        CassandraMetricsRegistry.MetricName meterAlias = aliasFactory.createMetricName("TestMeterAlias");
+        registry.meter(meter, meterAlias);
+
+        assertRowsContains(executeNet(String.format("SELECT * FROM %s.type_meter", VIRTUAL_METRICS)),
+                           row(meter.getMetricName(), 0L, 0.0, 0.0, 0.0, 0.0, "AliasTestScopeFirst"),
+                           row(meterAlias.getMetricName(), 0L, 0.0, 0.0, 0.0, 0.0, "AliasTestScopeSecond"));
+
+        CassandraMetricsRegistry.JmxMeterMBean bean = JMX.newMBeanProxy(jmxConnection,
+                                                                        meter.getMBeanName(),
+                                                                        CassandraMetricsRegistry.JmxMeterMBean.class);
+
+        CassandraMetricsRegistry.JmxMeterMBean beanAlias = JMX.newMBeanProxy(jmxConnection,
+                                                                             meterAlias.getMBeanName(),
+                                                                             CassandraMetricsRegistry.JmxMeterMBean.class);
+
+        assertNotNull(bean);
+        assertNotNull(beanAlias);
+        assertEquals(bean.getCount(), beanAlias.getCount());
+        assertEquals(bean.getFifteenMinuteRate(), beanAlias.getFifteenMinuteRate(), 0.0);
+        assertEquals(bean.getFiveMinuteRate(), beanAlias.getFiveMinuteRate(), 0.0);
+        assertEquals(bean.getMeanRate(), beanAlias.getMeanRate(), 0.0);
+        assertEquals(bean.getOneMinuteRate(), beanAlias.getOneMinuteRate(), 0.0);
+    }
+
+    private Object[] makeMetricRow(ObjectName objectName)
+    {
+        MetricType type = MetricType.find(objectName.getKeyPropertyList().get("name")).orElseThrow();
+        String jmxValue;
+        switch (type)
+        {
+            case METER:
+                jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                            objectName,
+                                                            CassandraMetricsRegistry.JmxMeterMBean.class).getCount());
+                break;
+            case COUNTER:
+                jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                            objectName,
+                                                            CassandraMetricsRegistry.JmxCounterMBean.class).getCount());
+                break;
+            case HISTOGRAM:
+                jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                            objectName,
+                                                            CassandraMetricsRegistry.JmxHistogramMBean.class).get50thPercentile());
+                break;
+            case TIMER:
+                jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                            objectName,
+                                                            CassandraMetricsRegistry.JmxTimerMBean.class).getCount());
+                break;
+            case GAUGE:
+                jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                            objectName,
+                                                            CassandraMetricsRegistry.JmxGaugeMBean.class).getValue());
+                break;
+            default:
+                throw new RuntimeException("Unknown metric type: " + objectName.getKeyPropertyList().get("name"));
+        }
+        return CQLTester.row(getFullMetricName(objectName),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")),
+                             MetricType.find(objectName.getKeyPropertyList().get("name"))
+                                       .map(MetricType::name)
+                                       .map(String::toLowerCase)
+                                       .orElseThrow(),
+                             jmxValue);
+    }
+
+    private Object[] makeMeterRow(ObjectName objectName)
+    {
+        assertEquals(MetricType.METER.metricName, objectName.getKeyPropertyList().get("name"));
+        CassandraMetricsRegistry.JmxMeterMBean bean = JMX.newMBeanProxy(jmxConnection,
+                                                                        objectName,
+                                                                        CassandraMetricsRegistry.JmxMeterMBean.class);
+
+        return CQLTester.row(getFullMetricName(objectName),
+                             bean.getCount(),
+                             bean.getFifteenMinuteRate(),
+                             bean.getFiveMinuteRate(),
+                             bean.getMeanRate(),
+                             bean.getOneMinuteRate(),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")));
+    }
+
+    private Object[] makeCounterRow(ObjectName objectName)
+    {
+        assertEquals(MetricType.COUNTER.metricName, objectName.getKeyPropertyList().get("name"));
+        CassandraMetricsRegistry.JmxCounterMBean bean = JMX.newMBeanProxy(jmxConnection,
+                                                                          objectName,
+                                                                          CassandraMetricsRegistry.JmxCounterMBean.class);
+
+        return CQLTester.row(getFullMetricName(objectName),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")),
+                             bean.getCount());
+    }
+
+    private Object[] makeHistogramRow(ObjectName objectName)
+    {
+        assertEquals(MetricType.HISTOGRAM.metricName, objectName.getKeyPropertyList().get("name"));
+        CassandraMetricsRegistry.JmxHistogramMBean bean = JMX.newMBeanProxy(jmxConnection,
+                                                                            objectName,
+                                                                            CassandraMetricsRegistry.JmxHistogramMBean.class);
+
+        return CQLTester.row(getFullMetricName(objectName),
+                             bean.getMax(),
+                             bean.getMean(),
+                             bean.getMin(),
+                             bean.get75thPercentile(),
+                             bean.get95thPercentile(),
+                             bean.get98thPercentile(),
+                             bean.get999thPercentile(),
+                             bean.get99thPercentile(),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")));
+    }
+
+    private Object[] makeTimerRow(ObjectName objectName)
+    {
+        assertEquals(MetricType.TIMER.metricName, objectName.getKeyPropertyList().get("name"));
+        CassandraMetricsRegistry.JmxTimerMBean bean = JMX.newMBeanProxy(jmxConnection,
+                                                                        objectName,
+                                                                        CassandraMetricsRegistry.JmxTimerMBean.class);
+
+        return CQLTester.row(getFullMetricName(objectName),
+                             bean.getCount(),
+                             bean.getFifteenMinuteRate(),
+                             bean.getFiveMinuteRate(),
+                             bean.getMeanRate(),
+                             bean.getOneMinuteRate(),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")));
+    }
+
+    private Object[] makeGaugeRow(ObjectName objectName)
+    {
+        assertEquals(MetricType.GAUGE.metricName, objectName.getKeyPropertyList().get("name"));
+        String jmxValue = String.valueOf(JMX.newMBeanProxy(jmxConnection,
+                                                           objectName,
+                                                           CassandraMetricsRegistry.JmxGaugeMBean.class).getValue());
+
+        return CQLTester.row(getFullMetricName(objectName),
+                             requireNonNull(objectName.getKeyPropertyList().get("scope")),
+                             jmxValue);
+    }
+
+    private boolean isLocalMetric(ObjectName mBean)
+    {
+        MetricType type = MetricType.find(mBean.getKeyPropertyList().get("name")).orElse(null);
+        return mBean.toString().startsWith(DefaultNameFactory.GROUP_NAME) && metricToNameMap.containsKey(type);
+    }
+
+    private static String getFullMetricName(ObjectName objectName)
+    {
+        Map<String, String> props = objectName.getKeyPropertyList();
+        return new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME, requireNonNull(props.get("type")),
+                                                       requireNonNull(props.get("name")),
+                                                       requireNonNull(props.get("scope"))).getMetricName();
+    }
+
+    private enum MetricType
+    {
+        METER("MeterTestMetric"),
+        COUNTER("CounterTestMetric"),
+        HISTOGRAM("HistogramTestMetric"),
+        TIMER("TimerTestMetric"),
+        GAUGE("GaugeTestMetric");
+
+        private final String metricName;
+
+        MetricType(String metricName)
+        {
+            this.metricName = metricName;
+        }
+
+        public static Optional<MetricType> find(String metricName)
+        {
+            for (MetricType type : values())
+                if (type.metricName.equals(metricName))
+                    return Optional.of(type);
+
+            return Optional.empty();
+        }
+    }
+}
diff --git a/test/unit/org/apache/cassandra/metrics/KeyspaceMetricsTest.java b/test/unit/org/apache/cassandra/metrics/KeyspaceMetricsTest.java
index 7c00da5819..f30b9307c1 100644
--- a/test/unit/org/apache/cassandra/metrics/KeyspaceMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/KeyspaceMetricsTest.java
@@ -27,7 +27,10 @@ import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.codahale.metrics.Metric;
 import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
 import com.datastax.driver.core.Session;
 import org.apache.cassandra.ServerTestUtils;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -35,6 +38,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.EmbeddedCassandraService;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class KeyspaceMetricsTest
@@ -71,6 +75,36 @@ public class KeyspaceMetricsTest
         assertEquals(metrics.get().collect(Collectors.joining(",")), 0, metrics.get().count());
     }
 
+    @Test
+    public void testKeyspaceVirtualTable()
+    {
+        String keyspace = "uniquemetricskeyspace1";
+        session.execute(String.format(
+            "CREATE KEYSPACE %s WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };",
+            keyspace));
+
+        assertTrue(CassandraMetricsRegistry.Metrics.getNames().stream().anyMatch(m -> m.endsWith(keyspace)));
+        ResultSet resultSet = session.execute(
+            "SELECT * FROM system_metrics.keyspace_group WHERE scope = '" + keyspace + "';");
+
+        int count = 0;
+        for (Row row : resultSet)
+        {
+            String metricName = row.getString("name");
+            if (!metricName.endsWith(keyspace))
+                continue;
+
+            Metric metric = CassandraMetricsRegistry.Metrics.getMetrics().get(metricName);
+            assertEquals(CassandraMetricsRegistry.getValueAsString(metric), row.getString("value"));
+            count++;
+        }
+
+        assertTrue("Keyspace " + keyspace + " metrics was not found", count > 0);
+
+        session.execute(String.format("DROP KEYSPACE %s;", keyspace));
+        assertFalse(CassandraMetricsRegistry.Metrics.getNames().stream().anyMatch(m -> m.endsWith(keyspace)));
+    }
+
     @AfterClass
     public static void tearDown()
     {
diff --git a/test/unit/org/apache/cassandra/metrics/LatencyMetricsTest.java b/test/unit/org/apache/cassandra/metrics/LatencyMetricsTest.java
index 3a4dc8af98..20fc79c649 100644
--- a/test/unit/org/apache/cassandra/metrics/LatencyMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/LatencyMetricsTest.java
@@ -29,13 +29,13 @@ public class LatencyMetricsTest
 {
     private final MetricNameFactory factory = new TestMetricsNameFactory();
 
-    private class TestMetricsNameFactory implements MetricNameFactory
+    private static class TestMetricsNameFactory implements MetricNameFactory
     {
 
         @Override
         public CassandraMetricsRegistry.MetricName createMetricName(String metricName)
         {
-            return new CassandraMetricsRegistry.MetricName(TestMetricsNameFactory.class, metricName);
+            return new CassandraMetricsRegistry.MetricName(DefaultNameFactory.GROUP_NAME, ClientRequestMetrics.TYPE_NAME, metricName);
         }
     }
 
@@ -45,7 +45,7 @@ public class LatencyMetricsTest
     @Test
     public void testGetRecentLatency()
     {
-        final LatencyMetrics l = new LatencyMetrics("test", "test");
+        final LatencyMetrics l = new LatencyMetrics(ClientRequestMetrics.TYPE_NAME, "test");
         Runnable r = () -> {
             for (int i = 0; i < 10000; i++)
             {
@@ -67,7 +67,7 @@ public class LatencyMetricsTest
     @Test
     public void testReadMerging()
     {
-        final LatencyMetrics parent = new LatencyMetrics("testMerge", "testMerge");
+        final LatencyMetrics parent = new LatencyMetrics(ClientRequestMetrics.TYPE_NAME, "testMerge");
         final LatencyMetrics child = new LatencyMetrics(factory, "testChild", parent);
 
         for (int i = 0; i < 100; i++)
@@ -86,7 +86,7 @@ public class LatencyMetricsTest
     @Test
     public void testRelease()
     {
-        final LatencyMetrics parent = new LatencyMetrics("testRelease", "testRelease");
+        final LatencyMetrics parent = new LatencyMetrics(ClientRequestMetrics.TYPE_NAME, "testRelease");
         final LatencyMetrics child = new LatencyMetrics(factory, "testChildRelease", parent);
 
         for (int i = 0; i < 100; i++)
diff --git a/test/unit/org/apache/cassandra/metrics/TableMetricsTest.java b/test/unit/org/apache/cassandra/metrics/TableMetricsTest.java
index bbcced43db..a0691d8ccd 100644
--- a/test/unit/org/apache/cassandra/metrics/TableMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/TableMetricsTest.java
@@ -40,6 +40,8 @@ import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.service.EmbeddedCassandraService;
 import org.apache.cassandra.service.StorageService;
 
+import static org.apache.cassandra.cql3.CQLTester.assertRowsContains;
+import static org.apache.cassandra.cql3.CQLTester.row;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -140,6 +142,12 @@ public class TableMetricsTest
 
         assertEquals(10, cfs.metric.coordinatorWriteLatency.getCount());
         assertGreaterThan(cfs.metric.coordinatorWriteLatency.getMeanRate(), 0);
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.table_group"),
+                row("org.apache.cassandra.metrics.Table.CoordinatorWriteLatency.junit.tablemetricstest", "junit.tablemetricstest", "timer",
+                        String.valueOf(cfs.metric.coordinatorWriteLatency.getCount())));
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.column_family_group"),
+                row("org.apache.cassandra.metrics.ColumnFamily.CoordinatorWriteLatency.junit.tablemetricstest", "junit.tablemetricstest", "timer",
+                        String.valueOf(cfs.metric.coordinatorWriteLatency.getCount())));
     }
 
     @Test
@@ -156,6 +164,13 @@ public class TableMetricsTest
         StorageService.instance.forceKeyspaceFlush(KEYSPACE);
 
         assertGreaterThan(cfs.metric.maxSSTableSize.getValue().doubleValue(), 0);
+
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.table_group"),
+                row("org.apache.cassandra.metrics.Table.MaxSSTableSize.junit.tablemetricstest", "junit.tablemetricstest", "gauge",
+                        String.valueOf(cfs.metric.maxSSTableSize.getValue())));
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.column_family_group"),
+                row("org.apache.cassandra.metrics.ColumnFamily.MaxSSTableSize.junit.tablemetricstest", "junit.tablemetricstest", "gauge",
+                        String.valueOf(cfs.metric.maxSSTableSize.getValue())));
     }
 
     @Test
@@ -171,6 +186,12 @@ public class TableMetricsTest
         StorageService.instance.forceKeyspaceFlush(KEYSPACE);
 
         assertGreaterThan(cfs.metric.maxSSTableDuration.getValue().doubleValue(), 0);
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.table_group"),
+                row("org.apache.cassandra.metrics.Table.MaxSSTableDuration.junit.tablemetricstesttwcs", "junit.tablemetricstesttwcs", "gauge",
+                        String.valueOf(cfs.metric.maxSSTableDuration.getValue())));
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.column_family_group"),
+                row("org.apache.cassandra.metrics.ColumnFamily.MaxSSTableDuration.junit.tablemetricstesttwcs", "junit.tablemetricstesttwcs", "gauge",
+                        String.valueOf(cfs.metric.maxSSTableDuration.getValue())));
     }
 
     @Test
diff --git a/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java b/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java
index 24fdc8b475..f8f59d07b0 100644
--- a/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java
+++ b/test/unit/org/apache/cassandra/metrics/TrieMemtableMetricsTest.java
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
 import com.datastax.driver.core.Cluster;
 import com.datastax.driver.core.Session;
 import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.ServerTestUtils;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.OverrideConfigurationLoader;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -47,7 +48,9 @@ import org.jboss.byteman.contrib.bmunit.BMRules;
 import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
 
 import static org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_SHARD_COUNT;
-import static org.hamcrest.Matchers.*;
+import static org.apache.cassandra.cql3.CQLTester.assertRowsContains;
+import static org.apache.cassandra.cql3.CQLTester.row;
+import static org.hamcrest.Matchers.greaterThan;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
@@ -59,6 +62,8 @@ public class TrieMemtableMetricsTest extends SchemaLoader
 
     private static final Logger logger = LoggerFactory.getLogger(TrieMemtableMetricsTest.class);
     private static Session session;
+    private static Cluster cluster;
+    private static EmbeddedCassandraService cassandra;
 
     private static final String KEYSPACE = "triememtable";
     private static final String TABLE = "metricstest";
@@ -78,10 +83,9 @@ public class TrieMemtableMetricsTest extends SchemaLoader
         });
         MEMTABLE_SHARD_COUNT.setInt(NUM_SHARDS);
 
-        EmbeddedCassandraService cassandra = new EmbeddedCassandraService();
-        cassandra.start();
+        cassandra = ServerTestUtils.startEmbeddedCassandraService();
 
-        Cluster cluster = Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort()).build();
+        cluster = Cluster.builder().addContactPoint("127.0.0.1").withPort(DatabaseDescriptor.getNativeTransportPort()).build();
         session = cluster.connect();
 
         session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", KEYSPACE));
@@ -114,6 +118,11 @@ public class TrieMemtableMetricsTest extends SchemaLoader
 
         long allPuts = metrics.contendedPuts.getCount() + metrics.uncontendedPuts.getCount();
         assertEquals(10, allPuts);
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.trie_memtable_group"),
+                row("org.apache.cassandra.metrics.TrieMemtable.Contended memtable puts.triememtable.metricstest",
+                        "triememtable.metricstest", "counter", String.valueOf(metrics.contendedPuts.getCount())),
+                row("org.apache.cassandra.metrics.TrieMemtable.Uncontended memtable puts.triememtable.metricstest",
+                        "triememtable.metricstest", "counter", String.valueOf(metrics.uncontendedPuts.getCount())));
     }
 
     @Test
@@ -155,6 +164,9 @@ public class TrieMemtableMetricsTest extends SchemaLoader
         assertEquals(100, metrics.contendedPuts.getCount() + metrics.uncontendedPuts.getCount());
         assertThat(metrics.contendedPuts.getCount(), greaterThan(0L));
         assertThat(metrics.contentionTime.totalLatency.getCount(), greaterThan(0L));
+        assertRowsContains(cluster, session.execute("SELECT * FROM system_metrics.trie_memtable_group"),
+                row("org.apache.cassandra.metrics.TrieMemtable.Contention timeTotalLatency.triememtable.metricstest",
+                        "triememtable.metricstest", "counter", String.valueOf(metrics.contentionTime.totalLatency.getCount())));
     }
 
     @Test
@@ -203,6 +215,9 @@ public class TrieMemtableMetricsTest extends SchemaLoader
     @AfterClass
     public static void teardown()
     {
-        session.close();
+        if (cluster != null)
+            cluster.close();
+        if (cassandra != null)
+            cassandra.stop();
     }
 }
diff --git a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
index 17b59400e7..65dc279ec5 100644
--- a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
+++ b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java
@@ -251,7 +251,8 @@ public class FBUtilitiesTest
         AssertionError error = null;
         for (Pair<String, String> a : Arrays.asList(Pair.create("Testing", "testing"),
                                                     Pair.create("fooBarBaz", "foo_bar_baz"),
-                                                    Pair.create("foo_bar_baz", "foo_bar_baz")
+                                                    Pair.create("foo_bar_baz", "foo_bar_baz"),
+                                                    Pair.create("TCM", "tcm")
         ))
         {
             try


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org