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

tkhurana pushed a commit to branch PHOENIX-7562-feature-new
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/PHOENIX-7562-feature-new by 
this push:
     new 956c7518be PHOENIX-7562 Add HAGroupStore Metrics2 observability (#2576)
956c7518be is described below

commit 956c7518be5e69018e76b2af3f7dccbbc11813bd
Author: ritegarg <[email protected]>
AuthorDate: Fri Jul 24 14:49:39 2026 -0700

    PHOENIX-7562 Add HAGroupStore Metrics2 observability (#2576)
    
    Expose a focused per-group health, state, failure, and listener-latency 
contract without coupling metrics to HA decisions.
---
 .../apache/phoenix/jdbc/HAGroupStoreClient.java    |  98 ++++--
 .../apache/phoenix/jdbc/HAGroupStoreRecord.java    |  45 ++-
 .../jdbc/metrics/HAGroupStoreMetricValues.java     | 196 +++++++++++
 .../jdbc/metrics/HAGroupStoreMetricsSource.java    | 133 ++++++++
 .../metrics/HAGroupStoreMetricsSourceFactory.java  |  40 +++
 .../metrics/HAGroupStoreMetricsSourceImpl.java     | 171 ++++++++++
 .../apache/phoenix/jdbc/HAGroupStoreMetricsIT.java | 358 +++++++++++++++++++++
 .../phoenix/jdbc/HAGroupStoreRecordTest.java       |  19 ++
 .../metrics/HAGroupStoreMetricsSourceImplTest.java | 197 ++++++++++++
 9 files changed, 1222 insertions(+), 35 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreClient.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreClient.java
index ec00184969..82c8460f78 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreClient.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreClient.java
@@ -78,6 +78,8 @@ import 
org.apache.phoenix.exception.StaleHAGroupStoreRecordVersionException;
 import org.apache.phoenix.jdbc.ClusterRoleRecord.ClusterRole;
 import org.apache.phoenix.jdbc.ClusterRoleRecord.RegistryType;
 import org.apache.phoenix.jdbc.HAGroupStoreRecord.HAGroupState;
+import org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource;
+import org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSourceFactory;
 import org.apache.phoenix.query.QueryServices;
 import org.apache.phoenix.query.QueryServicesOptions;
 import org.apache.phoenix.util.JDBCUtil;
@@ -118,6 +120,7 @@ public class HAGroupStoreClient implements Closeable {
     new ConcurrentHashMap<>();
   // HAGroupName for this instance
   private final String haGroupName;
+  private final HAGroupStoreMetricsSource metricsSource;
   // PathChildrenCache for current cluster and HAGroupName
   private PathChildrenCache pathChildrenCache = null;
   // Watches the peer cluster's HA record; owns the peer cache/admin, retry, 
and visibility.
@@ -278,6 +281,8 @@ public class HAGroupStoreClient implements Closeable {
     final String zkUrl) {
     this.conf = conf;
     this.haGroupName = haGroupName;
+    this.metricsSource = 
HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(haGroupName);
+    resetMetricGauges();
     this.zkUrl = zkUrl;
     this.waitTimeForSyncModeInMs = (long) Math.ceil(
       conf.getLong(ZK_SESSION_TIMEOUT, DEFAULT_ZK_SESSION_TIMEOUT) * 
ZK_SESSION_TIMEOUT_MULTIPLIER);
@@ -299,7 +304,10 @@ public class HAGroupStoreClient implements Closeable {
       initializeZNodeIfNeeded();
       if (this.pathChildrenCache != null) {
         this.isHealthy = true;
+        metricsSource.setLocalCacheHealthy(true);
         HAGroupStoreRecord local = getHAGroupStoreRecord();
+        metricsSource.setCurrentLocalState(
+          local != null ? local.getHAGroupState() : 
HAGroupStoreRecord.HAGroupState.UNKNOWN);
         this.lastConfiguredPeerZKUrl = local != null ? local.getPeerZKUrl() : 
null;
         peerWatcher.reconfigure(this.lastConfiguredPeerZKUrl);
       } else {
@@ -317,6 +325,7 @@ public class HAGroupStoreClient implements Closeable {
 
     } catch (Exception e) {
       this.isHealthy = false;
+      resetMetricGauges();
       close();
       LOGGER.error("Unexpected error occurred while initializing 
HAGroupStoreClient, "
         + "marking cache as unhealthy", e);
@@ -686,8 +695,21 @@ public class HAGroupStoreClient implements Closeable {
 
   // Update the system table on best effort basis for HA group
   // In case of failure, we will log the error and continue.
-  private void updateSystemTableHAGroupRecordSilently(String haGroupName,
-    SystemTableHAGroupRecord record) throws SQLException {
+  private boolean updateSystemTableHAGroupRecordSilently(String haGroupName,
+    SystemTableHAGroupRecord record) {
+    try {
+      updateSystemTableHAGroupRecord(haGroupName, record);
+      return true;
+    } catch (Exception e) {
+      metricsSource.incrementSystemTableSyncFailedCount();
+      LOGGER.error("Failed to update system table on best effort basis for HA 
group {}", haGroupName,
+        e);
+      return false;
+    }
+  }
+
+  private void updateSystemTableHAGroupRecord(String haGroupName, 
SystemTableHAGroupRecord record)
+    throws SQLException {
     StringBuilder updateQuery = new StringBuilder("UPSERT INTO " + 
SYSTEM_HA_GROUP_NAME + " (");
     StringBuilder valuesClause = new StringBuilder(" VALUES (");
     List<Object> parameters = new ArrayList<>();
@@ -794,10 +816,6 @@ public class HAGroupStoreClient implements Closeable {
 
       pstmt.executeUpdate();
       conn.commit();
-    } catch (Exception e) {
-      LOGGER.error(
-        "Failed to update system table on best" + "effort basis for HA group 
{}, error: {}",
-        haGroupName, e);
     }
   }
 
@@ -832,7 +850,8 @@ public class HAGroupStoreClient implements Closeable {
    * Syncs data from ZooKeeper (source of truth) to the system table. This 
method is called
    * periodically to ensure consistency.
    */
-  private void syncZKToSystemTable() {
+  @VisibleForTesting
+  void syncZKToSystemTable() {
     if (!isHealthy) {
       LOGGER.debug("HAGroupStoreClient is not healthy," + "skipping sync for 
HA group {}",
         haGroupName);
@@ -875,16 +894,26 @@ public class HAGroupStoreClient implements Closeable {
       }
 
       // Update system table with ZK data
-      updateSystemTableHAGroupRecordSilently(haGroupName, 
newSystemTableRecord);
-      LOGGER.info("Successfully synced ZK data to system table for HA group 
{}", haGroupName);
-    } catch (IOException | SQLException e) {
+      if (updateSystemTableHAGroupRecordSilently(haGroupName, 
newSystemTableRecord)) {
+        LOGGER.info("Successfully synced ZK data to system table for HA group 
{}", haGroupName);
+      }
+    } catch (Exception e) {
+      metricsSource.incrementSystemTableSyncFailedCount();
       long syncIntervalSeconds = 
conf.getLong(HA_GROUP_STORE_SYNC_INTERVAL_SECONDS,
         DEFAULT_HA_GROUP_STORE_SYNC_INTERVAL_SECONDS);
       LOGGER.error("Failed to sync ZK data to system table for HA group on 
best effort basis {},"
-        + "retrying in {} seconds", haGroupName, syncIntervalSeconds);
+        + "retrying in {} seconds", haGroupName, syncIntervalSeconds, e);
     }
   }
 
+  private void resetMetricGauges() {
+    metricsSource.setLocalCacheHealthy(false);
+    metricsSource.setPeerVisible(false);
+    metricsSource.setDegradedStandbyActive(false);
+    metricsSource.setCurrentLocalState(HAGroupState.UNKNOWN);
+    metricsSource.setCurrentPeerState(HAGroupState.UNKNOWN);
+  }
+
   /**
    * Listener for the local cache: tracks local state transitions (suppressing 
STANDBY while
    * degraded), keeps the peer watcher pointed at the current peer ZK, drives 
the legacy CRR sync,
@@ -908,6 +937,9 @@ public class HAGroupStoreClient implements Closeable {
             // unreachable peer never blocks local event processing.
             String peerZKUrl = record.getPeerZKUrl();
             if (!Objects.equals(peerZKUrl, lastConfiguredPeerZKUrl)) {
+              // An in-flight callback from the prior peer can briefly 
overwrite UNKNOWN. The new
+              // watcher self-heals this best-effort gauge; exact generation 
fencing is unnecessary.
+              metricsSource.setCurrentPeerState(HAGroupState.UNKNOWN);
               lastConfiguredPeerZKUrl = peerZKUrl;
               peerWatcher.reconfigureAsync(peerZKUrl);
             }
@@ -916,12 +948,18 @@ public class HAGroupStoreClient implements Closeable {
           break;
         case CONNECTION_LOST:
         case CONNECTION_SUSPENDED:
+          boolean wasHealthy = isHealthy;
           isHealthy = false;
+          metricsSource.setLocalCacheHealthy(false);
+          if (wasHealthy) {
+            metricsSource.incrementLocalZkConnectionLostCount();
+          }
           LOGGER.warn("LOCAL HAGroupStoreClient cache connection 
lost/suspended for HA group {}",
             haGroupName);
           break;
         case CONNECTION_RECONNECTED:
           isHealthy = true;
+          metricsSource.setLocalCacheHealthy(true);
           LOGGER.info("LOCAL HAGroupStoreClient cache connection reconnected 
for HA group {}",
             haGroupName);
           break;
@@ -1007,6 +1045,7 @@ public class HAGroupStoreClient implements Closeable {
       HAGroupState fromState = lastKnownPeerState;
       HAGroupState toState = peerRecord.getHAGroupState();
       lastKnownPeerState = toState;
+      metricsSource.setCurrentPeerState(toState);
       LOGGER.info("Detected state transition for HA group {} from {} to {} on 
PEER cluster",
         haGroupName, fromState, toState);
       notifySubscribers(fromState, toState, stat != null ? stat.getMtime() : 
0L, ClusterType.PEER,
@@ -1017,11 +1056,15 @@ public class HAGroupStoreClient implements Closeable {
 
     @Override
     public void onPeerVisible() {
+      metricsSource.setPeerVisible(true);
       clearLocalDegradedStandbyIfStillStandby();
     }
 
     @Override
     public void onPeerBlind() {
+      metricsSource.setPeerVisible(false);
+      metricsSource.setCurrentPeerState(HAGroupState.UNKNOWN);
+      metricsSource.incrementPeerBlindCount();
       // Peer just went blind while local is STANDBY (present re-checks): 
subscribers last saw the
       // raw STANDBY, so that is the prior effective state.
       presentLocalDegradedStandbyIfStandby(HAGroupState.STANDBY);
@@ -1060,6 +1103,8 @@ public class HAGroupStoreClient implements Closeable {
           return false;
         }
         localDegradedStandbyActive = true;
+        metricsSource.setDegradedStandbyActive(true);
+        metricsSource.incrementDegradedStandbyPresentedCount();
       }
       LOGGER.warn("Peer not visible for HA group {}; presenting local 
DEGRADED_STANDBY "
         + "(reason=peer-blind)", haGroupName);
@@ -1086,6 +1131,7 @@ public class HAGroupStoreClient implements Closeable {
         }
         local = readLocalRecordQuietly();
         localDegradedStandbyActive = false;
+        metricsSource.setDegradedStandbyActive(false);
         recover = local != null && local.getHAGroupState() == 
HAGroupState.STANDBY;
       }
       if (recover) {
@@ -1122,7 +1168,8 @@ public class HAGroupStoreClient implements Closeable {
     instances.computeIfPresent(key, (k, v) -> v.isEmpty() ? null : v);
   }
 
-  private void shutdownSyncExecutor() {
+  @VisibleForTesting
+  void shutdownSyncExecutor() {
     if (syncExecutor != null) {
       MoreExecutors.shutdownAndAwaitTermination(syncExecutor, 5, 
TimeUnit.SECONDS);
       syncExecutor = null;
@@ -1194,6 +1241,7 @@ public class HAGroupStoreClient implements Closeable {
         waitTime = waitTimeForSyncModeInMs;
       }
     } else {
+      metricsSource.incrementInvalidTransitionRejectedCount();
       throw new InvalidClusterRoleTransitionException(
         "Cannot transition from " + currentHAGroupState + " to " + 
newHAGroupState);
     }
@@ -1439,6 +1487,9 @@ public class HAGroupStoreClient implements Closeable {
     synchronized (localTransitionLock) {
       HAGroupState oldState = lastKnownLocalState;
       lastKnownLocalState = newState;
+      metricsSource.setCurrentLocalState(newState);
+      metricsSource
+        .setDegradedStandbyActive(localDegradedStandbyActive && newState == 
HAGroupState.STANDBY);
       // Fail closed for a final STANDBY while the peer is not visible. 
STANDBY_TO_ACTIVE and
       // ABORT_TO_STANDBY pass through because replay listens for those 
failover signals.
       if (newState == HAGroupState.STANDBY) {
@@ -1496,18 +1547,23 @@ public class HAGroupStoreClient implements Closeable {
 
     // Notify all listeners with error isolation
     if (!listenersToNotify.isEmpty()) {
-      LOGGER.info("Notifying {} listeners of state transitionfor HA group {} 
from {} to {} on {} "
+      LOGGER.info("Notifying {} listeners of state transition for HA group {} 
from {} to {} on {} "
         + "cluster", listenersToNotify.size(), haGroupName, fromState, 
toState, clusterType);
 
-      for (HAGroupStateListener listener : listenersToNotify) {
-        try {
-          listener.onStateChange(haGroupName, fromState, toState, 
modifiedTime, clusterType,
-            lastSyncStateTimeInMs);
-        } catch (Exception e) {
-          LOGGER.error("Error notifying listener of state transition for HA 
group {} from {} to "
-            + "{} on {} cluster", haGroupName, fromState, toState, 
clusterType, e);
-          // Continue notifying other listeners
+      long startTimeNs = System.nanoTime();
+      try {
+        for (HAGroupStateListener listener : listenersToNotify) {
+          try {
+            listener.onStateChange(haGroupName, fromState, toState, 
modifiedTime, clusterType,
+              lastSyncStateTimeInMs);
+          } catch (Exception e) {
+            metricsSource.incrementNotificationListenerErrorCount();
+            LOGGER.error("Error notifying listener of state transition for HA 
group {} from {} to "
+              + "{} on {} cluster", haGroupName, fromState, toState, 
clusterType, e);
+          }
         }
+      } finally {
+        metricsSource.updateSubscriberNotifyTime(System.nanoTime() - 
startTimeNs);
       }
     }
   }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreRecord.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreRecord.java
index 0494ca9d34..be61a644dc 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreRecord.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/HAGroupStoreRecord.java
@@ -47,17 +47,21 @@ public class HAGroupStoreRecord {
 
   /**
    * Enum representing the HA group state with each state having a 
corresponding ClusterRole.
+   * <p>
+   * Metric codes are a stable external monitoring contract. They are 
independent of enum ordinal:
+   * never renumber or reuse an existing code, assign new states new codes, 
and keep code {@code 0}
+   * reserved for {@link #UNKNOWN}.
    */
   public enum HAGroupState {
-    ABORT_TO_ACTIVE_IN_SYNC,
-    ABORT_TO_ACTIVE_NOT_IN_SYNC,
-    ABORT_TO_STANDBY,
-    ACTIVE_IN_SYNC,
-    ACTIVE_NOT_IN_SYNC,
-    ACTIVE_NOT_IN_SYNC_TO_STANDBY,
-    ACTIVE_NOT_IN_SYNC_WITH_OFFLINE_PEER,
-    ACTIVE_IN_SYNC_TO_STANDBY,
-    ACTIVE_WITH_OFFLINE_PEER,
+    ABORT_TO_ACTIVE_IN_SYNC(1),
+    ABORT_TO_ACTIVE_NOT_IN_SYNC(2),
+    ABORT_TO_STANDBY(3),
+    ACTIVE_IN_SYNC(4),
+    ACTIVE_NOT_IN_SYNC(5),
+    ACTIVE_NOT_IN_SYNC_TO_STANDBY(6),
+    ACTIVE_NOT_IN_SYNC_WITH_OFFLINE_PEER(7),
+    ACTIVE_IN_SYNC_TO_STANDBY(8),
+    ACTIVE_WITH_OFFLINE_PEER(9),
     /**
      * Degraded standby. Used two ways: (1) a persisted state for a 
reader-degraded standby (see
      * {@link HAGroupStoreManager#setReaderToDegraded(String)}), governed by 
the allowedTransitions
@@ -66,13 +70,26 @@ public class HAGroupStoreRecord {
      * cannot see its peer - that overlay is never persisted to ZK and is 
cleared (not transitioned)
      * when the peer becomes visible again.
      */
-    DEGRADED_STANDBY,
-    OFFLINE,
-    STANDBY,
-    STANDBY_TO_ACTIVE,
-    UNKNOWN;
+    DEGRADED_STANDBY(10),
+    OFFLINE(11),
+    STANDBY(12),
+    STANDBY_TO_ACTIVE(13),
+    UNKNOWN(0);
 
     private Set<HAGroupState> allowedTransitions;
+    private final int metricCode;
+
+    HAGroupState(int metricCode) {
+      this.metricCode = metricCode;
+    }
+
+    /**
+     * Returns the stable, append-only code exported by HAGroupStore state 
gauges.
+     * @return non-ordinal monitoring code; {@code 0} is reserved for {@link 
#UNKNOWN}
+     */
+    public int getMetricCode() {
+      return metricCode;
+    }
 
     /**
      * Gets the corresponding ClusterRole for this HAGroupState.
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricValues.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricValues.java
new file mode 100644
index 0000000000..23f5787d3c
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricValues.java
@@ -0,0 +1,196 @@
+/*
+ * 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.phoenix.jdbc.metrics;
+
+import 
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
+
+/** Immutable snapshot of HAGroupStore metrics used by tests. */
+@VisibleForTesting
+public final class HAGroupStoreMetricValues {
+
+  private final long localCacheHealthStatus;
+  private final long peerVisibilityStatus;
+  private final long degradedStandbyActive;
+  private final long currentLocalState;
+  private final long currentPeerState;
+  private final long localZkConnectionLostCount;
+  private final long peerBlindCount;
+  private final long degradedStandbyPresentedCount;
+  private final long invalidTransitionRejectedCount;
+  private final long systemTableSyncFailedCount;
+  private final long notificationListenerErrorCount;
+  private final long subscriberNotifyTimeCount;
+  private final long subscriberNotifyTimeMaxMs;
+
+  private HAGroupStoreMetricValues(Builder builder) {
+    localCacheHealthStatus = builder.localCacheHealthStatus;
+    peerVisibilityStatus = builder.peerVisibilityStatus;
+    degradedStandbyActive = builder.degradedStandbyActive;
+    currentLocalState = builder.currentLocalState;
+    currentPeerState = builder.currentPeerState;
+    localZkConnectionLostCount = builder.localZkConnectionLostCount;
+    peerBlindCount = builder.peerBlindCount;
+    degradedStandbyPresentedCount = builder.degradedStandbyPresentedCount;
+    invalidTransitionRejectedCount = builder.invalidTransitionRejectedCount;
+    systemTableSyncFailedCount = builder.systemTableSyncFailedCount;
+    notificationListenerErrorCount = builder.notificationListenerErrorCount;
+    subscriberNotifyTimeCount = builder.subscriberNotifyTimeCount;
+    subscriberNotifyTimeMaxMs = builder.subscriberNotifyTimeMaxMs;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public long getLocalCacheHealthStatus() {
+    return localCacheHealthStatus;
+  }
+
+  public long getPeerVisibilityStatus() {
+    return peerVisibilityStatus;
+  }
+
+  public long getDegradedStandbyActive() {
+    return degradedStandbyActive;
+  }
+
+  public long getCurrentLocalState() {
+    return currentLocalState;
+  }
+
+  public long getCurrentPeerState() {
+    return currentPeerState;
+  }
+
+  public long getLocalZkConnectionLostCount() {
+    return localZkConnectionLostCount;
+  }
+
+  public long getPeerBlindCount() {
+    return peerBlindCount;
+  }
+
+  public long getDegradedStandbyPresentedCount() {
+    return degradedStandbyPresentedCount;
+  }
+
+  public long getInvalidTransitionRejectedCount() {
+    return invalidTransitionRejectedCount;
+  }
+
+  public long getSystemTableSyncFailedCount() {
+    return systemTableSyncFailedCount;
+  }
+
+  public long getNotificationListenerErrorCount() {
+    return notificationListenerErrorCount;
+  }
+
+  public long getSubscriberNotifyTimeCount() {
+    return subscriberNotifyTimeCount;
+  }
+
+  public long getSubscriberNotifyTimeMaxMs() {
+    return subscriberNotifyTimeMaxMs;
+  }
+
+  public static final class Builder {
+    private long localCacheHealthStatus;
+    private long peerVisibilityStatus;
+    private long degradedStandbyActive;
+    private long currentLocalState;
+    private long currentPeerState;
+    private long localZkConnectionLostCount;
+    private long peerBlindCount;
+    private long degradedStandbyPresentedCount;
+    private long invalidTransitionRejectedCount;
+    private long systemTableSyncFailedCount;
+    private long notificationListenerErrorCount;
+    private long subscriberNotifyTimeCount;
+    private long subscriberNotifyTimeMaxMs;
+
+    public Builder setLocalCacheHealthStatus(long value) {
+      localCacheHealthStatus = value;
+      return this;
+    }
+
+    public Builder setPeerVisibilityStatus(long value) {
+      peerVisibilityStatus = value;
+      return this;
+    }
+
+    public Builder setDegradedStandbyActive(long value) {
+      degradedStandbyActive = value;
+      return this;
+    }
+
+    public Builder setCurrentLocalState(long value) {
+      currentLocalState = value;
+      return this;
+    }
+
+    public Builder setCurrentPeerState(long value) {
+      currentPeerState = value;
+      return this;
+    }
+
+    public Builder setLocalZkConnectionLostCount(long value) {
+      localZkConnectionLostCount = value;
+      return this;
+    }
+
+    public Builder setPeerBlindCount(long value) {
+      peerBlindCount = value;
+      return this;
+    }
+
+    public Builder setDegradedStandbyPresentedCount(long value) {
+      degradedStandbyPresentedCount = value;
+      return this;
+    }
+
+    public Builder setInvalidTransitionRejectedCount(long value) {
+      invalidTransitionRejectedCount = value;
+      return this;
+    }
+
+    public Builder setSystemTableSyncFailedCount(long value) {
+      systemTableSyncFailedCount = value;
+      return this;
+    }
+
+    public Builder setNotificationListenerErrorCount(long value) {
+      notificationListenerErrorCount = value;
+      return this;
+    }
+
+    public Builder setSubscriberNotifyTimeCount(long value) {
+      subscriberNotifyTimeCount = value;
+      return this;
+    }
+
+    public Builder setSubscriberNotifyTimeMaxMs(long value) {
+      subscriberNotifyTimeMaxMs = value;
+      return this;
+    }
+
+    public HAGroupStoreMetricValues build() {
+      return new HAGroupStoreMetricValues(this);
+    }
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSource.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSource.java
new file mode 100644
index 0000000000..0af587c503
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSource.java
@@ -0,0 +1,133 @@
+/*
+ * 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.phoenix.jdbc.metrics;
+
+import org.apache.hadoop.hbase.metrics.BaseSource;
+import org.apache.phoenix.jdbc.HAGroupStoreRecord.HAGroupState;
+
+import 
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Metrics for one HAGroupStore group on a RegionServer.
+ * <p>
+ * Lifecycle gauges are best-effort and re-baselined when a client is 
constructed. Counters and the
+ * histogram operation count are cumulative for the JVM lifetime; histogram 
distributions cover the
+ * latest Metrics2 collection interval. Metrics are not transactionally 
coordinated across client
+ * replacement or RegionServers and never feed HA decisions.
+ * <p>
+ * {@code haGroup} is the only source-specific tag. Monitoring systems must 
supply cluster,
+ * environment, host, and RegionServer identity as external scrape-target 
labels; role is mutable
+ * and is represented by the state gauges rather than a tag.
+ * <p>
+ * State gauge code mapping:
+ * <ul>
+ * <li>0 = UNKNOWN</li>
+ * <li>1 = ABORT_TO_ACTIVE_IN_SYNC</li>
+ * <li>2 = ABORT_TO_ACTIVE_NOT_IN_SYNC</li>
+ * <li>3 = ABORT_TO_STANDBY</li>
+ * <li>4 = ACTIVE_IN_SYNC</li>
+ * <li>5 = ACTIVE_NOT_IN_SYNC</li>
+ * <li>6 = ACTIVE_NOT_IN_SYNC_TO_STANDBY</li>
+ * <li>7 = ACTIVE_NOT_IN_SYNC_WITH_OFFLINE_PEER</li>
+ * <li>8 = ACTIVE_IN_SYNC_TO_STANDBY</li>
+ * <li>9 = ACTIVE_WITH_OFFLINE_PEER</li>
+ * <li>10 = DEGRADED_STANDBY</li>
+ * <li>11 = OFFLINE</li>
+ * <li>12 = STANDBY</li>
+ * <li>13 = STANDBY_TO_ACTIVE</li>
+ * </ul>
+ */
+public interface HAGroupStoreMetricsSource extends BaseSource {
+
+  String METRICS_NAME = "HAGroupStore";
+  String METRICS_CONTEXT = "phoenix";
+  String METRICS_DESCRIPTION = "Metrics for HAGroupStore operations";
+  // Server-oriented JMX identity consistent with sibling HBase/Phoenix 
sources. Short-lived admin
+  // CLI invocations can create an incidental bean under the same context.
+  String METRICS_JMX_CONTEXT = "RegionServer,sub=" + METRICS_NAME;
+
+  String HA_GROUP_TAG_NAME = "haGroup";
+  String HA_GROUP_TAG_DESC = "HA group name";
+
+  String LOCAL_CACHE_HEALTH_STATUS = "haGroupStoreLocalCacheHealthStatus";
+  String LOCAL_CACHE_HEALTH_STATUS_DESC =
+    "Local HAGroupStore cache health status: 0 healthy, non-zero unhealthy";
+  String PEER_VISIBILITY_STATUS = "haGroupStorePeerVisibilityStatus";
+  String PEER_VISIBILITY_STATUS_DESC =
+    "Configured peer visibility status: 0 visible or unconfigured, non-zero 
blind";
+  String DEGRADED_STANDBY_ACTIVE = "haGroupStoreDegradedStandbyActive";
+  String DEGRADED_STANDBY_ACTIVE_DESC =
+    "Local in-memory peer-blind fail-closed overlay: 0 inactive, 1 active on 
raw STANDBY; "
+      + "never persisted to ZooKeeper and distinct from persisted 
DEGRADED_STANDBY";
+  String CURRENT_LOCAL_STATE = "haGroupStoreCurrentLocalState";
+  String CURRENT_LOCAL_STATE_DESC = "Current raw local HA group state code";
+  String CURRENT_PEER_STATE = "haGroupStoreCurrentPeerState";
+  String CURRENT_PEER_STATE_DESC = "Current peer HA group state code";
+
+  String LOCAL_ZK_CONNECTION_LOST_COUNT = 
"haGroupStoreLocalZkConnectionLostCount";
+  String LOCAL_ZK_CONNECTION_LOST_COUNT_DESC = "Number of local ZooKeeper 
connection losses";
+  String PEER_BLIND_COUNT = "haGroupStorePeerBlindCount";
+  String PEER_BLIND_COUNT_DESC = "Number of transitions to peer-blind";
+  String DEGRADED_STANDBY_PRESENTED_COUNT = 
"haGroupStoreDegradedStandbyPresentedCount";
+  String DEGRADED_STANDBY_PRESENTED_COUNT_DESC = "Number of degraded standby 
overlay presentations";
+  String INVALID_TRANSITION_REJECTED_COUNT = 
"haGroupStoreInvalidTransitionRejectedCount";
+  String INVALID_TRANSITION_REJECTED_COUNT_DESC =
+    "Number of invalid HA group state transitions rejected";
+  String SYSTEM_TABLE_SYNC_FAILED_COUNT = 
"haGroupStoreSystemTableSyncFailedCount";
+  String SYSTEM_TABLE_SYNC_FAILED_COUNT_DESC =
+    "Number of failed SYSTEM.HA_GROUP synchronization attempts";
+  String NOTIFICATION_LISTENER_ERROR_COUNT = 
"haGroupStoreNotificationListenerErrorCount";
+  String NOTIFICATION_LISTENER_ERROR_COUNT_DESC = "Number of HA group 
notification listener errors";
+
+  // MutableTimeHistogram capitalizes its exported basename. The logical 
registry key remains
+  // lowercase, while JMX/Metrics2 consumers use 
SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE plus the
+  // standard histogram suffixes.
+  String SUBSCRIBER_NOTIFY_TIME_MS = "haGroupStoreSubscriberNotifyTimeMs";
+  String SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE = 
"HaGroupStoreSubscriberNotifyTimeMs";
+  String SUBSCRIBER_NOTIFY_TIME_MS_DESC =
+    "Time spent synchronously notifying HA group subscribers in whole 
milliseconds; "
+      + "durations below one millisecond are recorded as zero-valued samples 
and still increment "
+      + "the operation count";
+
+  void setLocalCacheHealthy(boolean healthy);
+
+  void setPeerVisible(boolean visible);
+
+  void setDegradedStandbyActive(boolean active);
+
+  void setCurrentLocalState(HAGroupState state);
+
+  void setCurrentPeerState(HAGroupState state);
+
+  void incrementLocalZkConnectionLostCount();
+
+  void incrementPeerBlindCount();
+
+  void incrementDegradedStandbyPresentedCount();
+
+  void incrementInvalidTransitionRejectedCount();
+
+  void incrementSystemTableSyncFailedCount();
+
+  void incrementNotificationListenerErrorCount();
+
+  void updateSubscriberNotifyTime(long elapsedTimeNs);
+
+  @VisibleForTesting
+  HAGroupStoreMetricValues getCurrentMetricValues();
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceFactory.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceFactory.java
new file mode 100644
index 0000000000..2d61a42fd3
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.phoenix.jdbc.metrics;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Factory for process-lifetime HAGroupStore metric sources.
+ * <p>
+ * Creates one source lazily per HA group and intentionally retains it for the 
JVM lifetime so
+ * cumulative counters survive client replacement. HA-group names are expected 
to be a small,
+ * stable set within a process.
+ */
+public final class HAGroupStoreMetricsSourceFactory {
+
+  private static final ConcurrentHashMap<String, HAGroupStoreMetricsSource> 
SOURCES =
+    new ConcurrentHashMap<>();
+
+  private HAGroupStoreMetricsSourceFactory() {
+  }
+
+  public static HAGroupStoreMetricsSource getInstanceForHAGroup(String 
haGroupName) {
+    return SOURCES.computeIfAbsent(haGroupName, 
HAGroupStoreMetricsSourceImpl::new);
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImpl.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImpl.java
new file mode 100644
index 0000000000..f7d731fe76
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImpl.java
@@ -0,0 +1,171 @@
+/*
+ * 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.phoenix.jdbc.metrics;
+
+import java.util.concurrent.TimeUnit;
+import javax.management.ObjectName;
+import org.apache.hadoop.hbase.metrics.BaseSourceImpl;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.metrics2.lib.Interns;
+import org.apache.hadoop.metrics2.lib.MutableFastCounter;
+import org.apache.hadoop.metrics2.lib.MutableGaugeLong;
+import org.apache.hadoop.metrics2.lib.MutableTimeHistogram;
+import org.apache.phoenix.jdbc.HAGroupStoreRecord.HAGroupState;
+
+import 
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
+
+/** Hadoop Metrics2 implementation for one HAGroupStore group. */
+public class HAGroupStoreMetricsSourceImpl extends BaseSourceImpl
+  implements HAGroupStoreMetricsSource {
+
+  private final MutableGaugeLong localCacheHealthStatus;
+  private final MutableGaugeLong peerVisibilityStatus;
+  private final MutableGaugeLong degradedStandbyActive;
+  private final MutableGaugeLong currentLocalState;
+  private final MutableGaugeLong currentPeerState;
+
+  private final MutableFastCounter localZkConnectionLostCount;
+  private final MutableFastCounter peerBlindCount;
+  private final MutableFastCounter degradedStandbyPresentedCount;
+  private final MutableFastCounter invalidTransitionRejectedCount;
+  private final MutableFastCounter systemTableSyncFailedCount;
+  private final MutableFastCounter notificationListenerErrorCount;
+
+  private final MutableTimeHistogram subscriberNotifyTimeMs;
+
+  public HAGroupStoreMetricsSourceImpl(String haGroupName) {
+    this(METRICS_NAME, METRICS_DESCRIPTION, METRICS_CONTEXT, 
METRICS_JMX_CONTEXT, haGroupName);
+  }
+
+  HAGroupStoreMetricsSourceImpl(String metricsName, String metricsDescription,
+    String metricsContext, String metricsJmxContext, String haGroupName) {
+    super(metricsName, metricsDescription, metricsContext,
+      metricsJmxContext + ",haGroup=" + ObjectName.quote(haGroupName));
+    getMetricsRegistry().tag(Interns.info(HA_GROUP_TAG_NAME, 
HA_GROUP_TAG_DESC), haGroupName);
+
+    localCacheHealthStatus = 
getMetricsRegistry().newGauge(LOCAL_CACHE_HEALTH_STATUS,
+      LOCAL_CACHE_HEALTH_STATUS_DESC, 1L);
+    peerVisibilityStatus = 
getMetricsRegistry().newGauge(PEER_VISIBILITY_STATUS,
+      PEER_VISIBILITY_STATUS_DESC, 1L);
+    degradedStandbyActive =
+      getMetricsRegistry().newGauge(DEGRADED_STANDBY_ACTIVE, 
DEGRADED_STANDBY_ACTIVE_DESC, 0L);
+    currentLocalState = getMetricsRegistry().newGauge(CURRENT_LOCAL_STATE, 
CURRENT_LOCAL_STATE_DESC,
+      (long) HAGroupState.UNKNOWN.getMetricCode());
+    currentPeerState = getMetricsRegistry().newGauge(CURRENT_PEER_STATE, 
CURRENT_PEER_STATE_DESC,
+      (long) HAGroupState.UNKNOWN.getMetricCode());
+
+    localZkConnectionLostCount = 
getMetricsRegistry().newCounter(LOCAL_ZK_CONNECTION_LOST_COUNT,
+      LOCAL_ZK_CONNECTION_LOST_COUNT_DESC, 0L);
+    peerBlindCount = getMetricsRegistry().newCounter(PEER_BLIND_COUNT, 
PEER_BLIND_COUNT_DESC, 0L);
+    degradedStandbyPresentedCount = getMetricsRegistry()
+      .newCounter(DEGRADED_STANDBY_PRESENTED_COUNT, 
DEGRADED_STANDBY_PRESENTED_COUNT_DESC, 0L);
+    invalidTransitionRejectedCount = getMetricsRegistry()
+      .newCounter(INVALID_TRANSITION_REJECTED_COUNT, 
INVALID_TRANSITION_REJECTED_COUNT_DESC, 0L);
+    systemTableSyncFailedCount = 
getMetricsRegistry().newCounter(SYSTEM_TABLE_SYNC_FAILED_COUNT,
+      SYSTEM_TABLE_SYNC_FAILED_COUNT_DESC, 0L);
+    notificationListenerErrorCount = getMetricsRegistry()
+      .newCounter(NOTIFICATION_LISTENER_ERROR_COUNT, 
NOTIFICATION_LISTENER_ERROR_COUNT_DESC, 0L);
+
+    subscriberNotifyTimeMs = 
getMetricsRegistry().newTimeHistogram(SUBSCRIBER_NOTIFY_TIME_MS,
+      SUBSCRIBER_NOTIFY_TIME_MS_DESC);
+  }
+
+  @Override
+  public void setLocalCacheHealthy(boolean value) {
+    localCacheHealthStatus.set(value ? 0L : 1L);
+  }
+
+  @Override
+  public void setPeerVisible(boolean value) {
+    peerVisibilityStatus.set(value ? 0L : 1L);
+  }
+
+  @Override
+  public void setDegradedStandbyActive(boolean value) {
+    degradedStandbyActive.set(value ? 1L : 0L);
+  }
+
+  @Override
+  public void setCurrentLocalState(HAGroupState state) {
+    HAGroupState currentState = state != null ? state : HAGroupState.UNKNOWN;
+    currentLocalState.set(currentState.getMetricCode());
+  }
+
+  @Override
+  public void setCurrentPeerState(HAGroupState state) {
+    HAGroupState currentState = state != null ? state : HAGroupState.UNKNOWN;
+    currentPeerState.set(currentState.getMetricCode());
+  }
+
+  @Override
+  public void incrementLocalZkConnectionLostCount() {
+    localZkConnectionLostCount.incr();
+  }
+
+  @Override
+  public void incrementPeerBlindCount() {
+    peerBlindCount.incr();
+  }
+
+  @Override
+  public void incrementDegradedStandbyPresentedCount() {
+    degradedStandbyPresentedCount.incr();
+  }
+
+  @Override
+  public void incrementInvalidTransitionRejectedCount() {
+    invalidTransitionRejectedCount.incr();
+  }
+
+  @Override
+  public void incrementSystemTableSyncFailedCount() {
+    systemTableSyncFailedCount.incr();
+  }
+
+  @Override
+  public void incrementNotificationListenerErrorCount() {
+    notificationListenerErrorCount.incr();
+  }
+
+  @Override
+  public void updateSubscriberNotifyTime(long elapsedTimeNs) {
+    subscriberNotifyTimeMs.add(TimeUnit.NANOSECONDS.toMillis(elapsedTimeNs));
+  }
+
+  @Override
+  public HAGroupStoreMetricValues getCurrentMetricValues() {
+    return HAGroupStoreMetricValues.builder()
+      .setLocalCacheHealthStatus(localCacheHealthStatus.value())
+      .setPeerVisibilityStatus(peerVisibilityStatus.value())
+      .setDegradedStandbyActive(degradedStandbyActive.value())
+      
.setCurrentLocalState(currentLocalState.value()).setCurrentPeerState(currentPeerState.value())
+      .setLocalZkConnectionLostCount(localZkConnectionLostCount.value())
+      .setPeerBlindCount(peerBlindCount.value())
+      .setDegradedStandbyPresentedCount(degradedStandbyPresentedCount.value())
+      
.setInvalidTransitionRejectedCount(invalidTransitionRejectedCount.value())
+      .setSystemTableSyncFailedCount(systemTableSyncFailedCount.value())
+      
.setNotificationListenerErrorCount(notificationListenerErrorCount.value())
+      .setSubscriberNotifyTimeCount(subscriberNotifyTimeMs.getCount())
+      .setSubscriberNotifyTimeMaxMs(subscriberNotifyTimeMs.getMax()).build();
+  }
+
+  @VisibleForTesting
+  public void unregisterForTesting() {
+    DefaultMetricsSystem.instance().unregisterSource(metricsJmxContext);
+  }
+}
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupStoreMetricsIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupStoreMetricsIT.java
new file mode 100644
index 0000000000..80e1f7781a
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/jdbc/HAGroupStoreMetricsIT.java
@@ -0,0 +1,358 @@
+/*
+ * 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.phoenix.jdbc;
+
+import static 
org.apache.phoenix.jdbc.HAGroupStoreClient.ZK_CONSISTENT_HA_GROUP_RECORD_NAMESPACE;
+import static org.apache.phoenix.jdbc.PhoenixHAAdmin.getLocalZkUrl;
+import static org.apache.phoenix.jdbc.PhoenixHAAdmin.toPath;
+import static 
org.apache.phoenix.query.QueryServices.HA_GROUP_STORE_SYNC_INTERVAL_SECONDS;
+import static 
org.apache.phoenix.replication.reader.ReplicationLogReplayService.PHOENIX_REPLICATION_REPLAY_ENABLED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.phoenix.end2end.NeedsOwnMiniClusterTest;
+import org.apache.phoenix.exception.InvalidClusterRoleTransitionException;
+import org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricValues;
+import org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSourceFactory;
+import org.apache.phoenix.util.HAGroupStoreTestUtil;
+import org.apache.zookeeper.data.Stat;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TestName;
+
+@Category(NeedsOwnMiniClusterTest.class)
+public class HAGroupStoreMetricsIT extends HABaseIT {
+
+  private static final long EVENT_TIMEOUT_MS = 60000L;
+
+  @Rule
+  public final TestName testName = new TestName();
+
+  private PhoenixHAAdmin localAdmin;
+  private PhoenixHAAdmin peerAdmin;
+  private String zkUrl;
+  private String peerZkUrl;
+  private String masterUrl;
+  private String peerMasterUrl;
+  private HAGroupStoreClient clientToClose;
+
+  @BeforeClass
+  public static synchronized void setUpClass() throws Exception {
+    conf1.setBoolean(PHOENIX_REPLICATION_REPLAY_ENABLED, false);
+    conf2.setBoolean(PHOENIX_REPLICATION_REPLAY_ENABLED, false);
+    CLUSTERS.start();
+  }
+
+  @AfterClass
+  public static synchronized void tearDownClass() throws Exception {
+    CLUSTERS.close();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    zkUrl = getLocalZkUrl(CLUSTERS.getHBaseCluster1().getConfiguration());
+    peerZkUrl = getLocalZkUrl(CLUSTERS.getHBaseCluster2().getConfiguration());
+    masterUrl = CLUSTERS.getMasterAddress1();
+    peerMasterUrl = CLUSTERS.getMasterAddress2();
+    localAdmin = new 
PhoenixHAAdmin(CLUSTERS.getHBaseCluster1().getConfiguration(),
+      ZK_CONSISTENT_HA_GROUP_RECORD_NAMESPACE);
+    peerAdmin = new 
PhoenixHAAdmin(CLUSTERS.getHBaseCluster2().getConfiguration(),
+      ZK_CONSISTENT_HA_GROUP_RECORD_NAMESPACE);
+    deleteZNodes();
+    HAGroupStoreTestUtil.deleteHAGroupRecordInSystemTable(group(), zkUrl);
+    HAGroupStoreTestUtil.upsertHAGroupRecordInSystemTable(group(), zkUrl, 
peerZkUrl, masterUrl,
+      peerMasterUrl, ClusterRoleRecord.ClusterRole.ACTIVE, 
ClusterRoleRecord.ClusterRole.STANDBY,
+      null, CLUSTERS.getHdfsUrl1(), CLUSTERS.getHdfsUrl2());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (clientToClose != null) {
+      clientToClose.close();
+    }
+    deleteZNodes();
+    HAGroupStoreTestUtil.deleteHAGroupRecordInSystemTable(group(), zkUrl);
+    localAdmin.close();
+    peerAdmin.close();
+  }
+
+  @Test
+  public void testInitialAndLocalStateMetrics() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    writePeer(HAGroupStoreRecord.HAGroupState.STANDBY);
+    HAGroupStoreClient client = localClient();
+
+    awaitMetrics(values -> values.getLocalCacheHealthStatus() == 0
+      && values.getCurrentLocalState()
+          == HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC.getMetricCode()
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+
+    assertEquals(0L,
+      
client.setHAGroupStatusIfNeeded(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC_TO_STANDBY));
+    awaitMetrics(values -> values.getCurrentLocalState()
+        == 
HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC_TO_STANDBY.getMetricCode());
+
+    assertEquals(0L, 
client.setHAGroupStatusIfNeeded(HAGroupStoreRecord.HAGroupState.STANDBY));
+    awaitMetrics(values -> values.getCurrentLocalState()
+        == HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+  }
+
+  @Test
+  public void testLocalZkHealthMetrics() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    HAGroupStoreClient client = localClient();
+    awaitMetrics(values -> values.getLocalCacheHealthStatus() == 0);
+    HAGroupStoreMetricValues before = metrics();
+    int port = Integer.parseInt(
+      
CLUSTERS.getHBaseCluster1().getConfiguration().get("hbase.zookeeper.property.clientPort"));
+
+    CLUSTERS.getHBaseCluster1().shutdownMiniZKCluster();
+    try {
+      awaitMetrics(values -> values.getLocalCacheHealthStatus() == 1
+        && values.getLocalZkConnectionLostCount() == 
before.getLocalZkConnectionLostCount() + 1);
+      assertThrows(IOException.class, client::getHAGroupStoreRecord);
+    } finally {
+      CLUSTERS.getHBaseCluster1().startMiniZKCluster(1, port);
+    }
+    awaitMetrics(values -> values.getLocalCacheHealthStatus() == 0);
+  }
+
+  @Test
+  public void testLateInitializationFailureResetsGauges() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    Configuration invalidConf =
+      new Configuration(CLUSTERS.getHBaseCluster1().getConfiguration());
+    invalidConf.setLong(HA_GROUP_STORE_SYNC_INTERVAL_SECONDS, 0L);
+
+    assertNull(HAGroupStoreClient.getInstanceForZkUrl(invalidConf, group(), 
zkUrl));
+
+    HAGroupStoreMetricValues values = metrics();
+    assertEquals(1L, values.getLocalCacheHealthStatus());
+    assertEquals(1L, values.getPeerVisibilityStatus());
+    assertEquals(0L, values.getDegradedStandbyActive());
+    assertEquals(HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode(),
+      values.getCurrentLocalState());
+    assertEquals(HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode(),
+      values.getCurrentPeerState());
+  }
+
+  @Test
+  public void testPeerVisibilityAndDegradedMetrics() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.STANDBY);
+    writePeer(HAGroupStoreRecord.HAGroupState.STANDBY);
+    localClient();
+    awaitMetrics(values -> values.getPeerVisibilityStatus() == 0
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+    HAGroupStoreMetricValues before = metrics();
+    int port = Integer.parseInt(
+      
CLUSTERS.getHBaseCluster2().getConfiguration().get("hbase.zookeeper.property.clientPort"));
+
+    CLUSTERS.getHBaseCluster2().shutdownMiniZKCluster();
+    try {
+      awaitMetrics(values -> values.getPeerVisibilityStatus() == 1
+        && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode()
+        && values.getDegradedStandbyActive() == 1
+        && values.getPeerBlindCount() == before.getPeerBlindCount() + 1
+        && values.getDegradedStandbyPresentedCount()
+            == before.getDegradedStandbyPresentedCount() + 1);
+    } finally {
+      CLUSTERS.getHBaseCluster2().startMiniZKCluster(1, port);
+    }
+    awaitMetrics(values -> values.getPeerVisibilityStatus() == 0
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode()
+      && values.getDegradedStandbyActive() == 0);
+  }
+
+  @Test
+  public void testPeerReconfigurationMetrics() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    writePeer(HAGroupStoreRecord.HAGroupState.STANDBY);
+    localClient();
+    awaitMetrics(values -> values.getCurrentPeerState()
+        == HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC, null);
+    awaitMetrics(values -> values.getPeerVisibilityStatus() == 0
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode());
+
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    awaitMetrics(values -> values.getCurrentPeerState()
+        == HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC, "invalidURL");
+    awaitMetrics(values -> values.getPeerVisibilityStatus() == 1
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode());
+
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC, null);
+    awaitMetrics(values -> values.getPeerVisibilityStatus() == 0
+      && values.getCurrentPeerState() == 
HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode());
+  }
+
+  @Test
+  public void testInvalidTransitionMetric() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC);
+    HAGroupStoreClient client = localClient();
+    HAGroupStoreMetricValues before = metrics();
+
+    assertThrows(InvalidClusterRoleTransitionException.class,
+      () -> 
client.setHAGroupStatusIfNeeded(HAGroupStoreRecord.HAGroupState.STANDBY));
+    assertEquals(before.getInvalidTransitionRejectedCount() + 1,
+      metrics().getInvalidTransitionRejectedCount());
+  }
+
+  @Test
+  public void testSystemTableSyncFailureMetric() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.STANDBY);
+    HAGroupStoreClient client = localClient();
+    awaitMetrics(values -> values.getCurrentLocalState()
+        == HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+    client.shutdownSyncExecutor();
+    HAGroupStoreMetricValues before = metrics();
+    Admin admin = CLUSTERS.getHBaseCluster1().getAdmin();
+
+    
admin.disableTable(PhoenixDatabaseMetaData.SYSTEM_HA_GROUP_HBASE_TABLE_NAME);
+    try {
+      assertEquals(0L,
+        
client.setHAGroupStatusIfNeeded(HAGroupStoreRecord.HAGroupState.STANDBY_TO_ACTIVE));
+      assertEquals(before.getSystemTableSyncFailedCount() + 1,
+        metrics().getSystemTableSyncFailedCount());
+      Pair<HAGroupStoreRecord, Stat> updated = 
localAdmin.getHAGroupStoreRecordInZooKeeper(group());
+      assertEquals(HAGroupStoreRecord.HAGroupState.STANDBY_TO_ACTIVE,
+        updated.getLeft().getHAGroupState());
+    } finally {
+      
admin.enableTable(PhoenixDatabaseMetaData.SYSTEM_HA_GROUP_HBASE_TABLE_NAME);
+    }
+  }
+
+  @Test
+  public void testPeriodicSystemTableSyncFailureBeforeWriteMetric() throws 
Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    HAGroupStoreClient client = localClient();
+    awaitMetrics(values -> values.getLocalCacheHealthStatus() == 0);
+    client.shutdownSyncExecutor();
+
+    HAGroupStoreRecord invalidPolicyRecord =
+      new HAGroupStoreRecord(HAGroupStoreRecord.DEFAULT_PROTOCOL_VERSION, 
group(),
+        HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC, 0L, "INVALID_POLICY", 
peerZkUrl, masterUrl,
+        peerMasterUrl, CLUSTERS.getHdfsUrl1(), CLUSTERS.getHdfsUrl2(), 0L);
+    createOrUpdate(localAdmin, invalidPolicyRecord);
+    long before = metrics().getSystemTableSyncFailedCount();
+
+    client.syncZKToSystemTable();
+
+    assertEquals(before + 1, metrics().getSystemTableSyncFailedCount());
+  }
+
+  @Test
+  public void testListenerMetrics() throws Exception {
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC);
+    HAGroupStoreClient client = localClient();
+    
client.subscribeToTargetState(HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC,
+      ClusterType.LOCAL, (group, from, to, mtime, cluster, lastSync) -> {
+        throw new IllegalStateException("expected test failure");
+      });
+    AtomicInteger successfulListeners = new AtomicInteger();
+    
client.subscribeToTargetState(HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC,
+      ClusterType.LOCAL,
+      (group, from, to, mtime, cluster, lastSync) -> 
successfulListeners.incrementAndGet());
+    HAGroupStoreMetricValues before = metrics();
+
+    writeLocal(HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC);
+    awaitMetrics(values -> values.getNotificationListenerErrorCount()
+        == before.getNotificationListenerErrorCount() + 1
+      && values.getSubscriberNotifyTimeCount() == 
before.getSubscriberNotifyTimeCount() + 1
+      && successfulListeners.get() == 1);
+  }
+
+  private String group() {
+    return testName.getMethodName();
+  }
+
+  private HAGroupStoreClient localClient() {
+    HAGroupStoreClient client = HAGroupStoreClient
+      .getInstanceForZkUrl(CLUSTERS.getHBaseCluster1().getConfiguration(), 
group(), zkUrl);
+    if (client != null) {
+      clientToClose = client;
+    }
+    return client;
+  }
+
+  private HAGroupStoreMetricValues metrics() {
+    return 
HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(group()).getCurrentMetricValues();
+  }
+
+  private void awaitMetrics(Predicate<HAGroupStoreMetricValues> condition) 
throws Exception {
+    long deadline = System.currentTimeMillis() + EVENT_TIMEOUT_MS;
+    while (!condition.test(metrics()) && System.currentTimeMillis() < 
deadline) {
+      Thread.sleep(100L);
+    }
+    assertTrue("Metric condition not met for " + group(), 
condition.test(metrics()));
+  }
+
+  private HAGroupStoreRecord localRecord(HAGroupStoreRecord.HAGroupState 
state, String peerUrl) {
+    return new HAGroupStoreRecord(HAGroupStoreRecord.DEFAULT_PROTOCOL_VERSION, 
group(), state, 0L,
+      HighAvailabilityPolicy.FAILOVER.toString(), peerUrl, masterUrl, 
peerMasterUrl,
+      CLUSTERS.getHdfsUrl1(), CLUSTERS.getHdfsUrl2(), 0L);
+  }
+
+  private HAGroupStoreRecord peerRecord(HAGroupStoreRecord.HAGroupState state) 
{
+    return new HAGroupStoreRecord(HAGroupStoreRecord.DEFAULT_PROTOCOL_VERSION, 
group(), state, 0L,
+      HighAvailabilityPolicy.FAILOVER.toString(), zkUrl, peerMasterUrl, 
masterUrl,
+      CLUSTERS.getHdfsUrl2(), CLUSTERS.getHdfsUrl1(), 0L);
+  }
+
+  private void writeLocal(HAGroupStoreRecord.HAGroupState state) throws 
Exception {
+    writeLocal(state, peerZkUrl);
+  }
+
+  private void writeLocal(HAGroupStoreRecord.HAGroupState state, String 
peerUrl) throws Exception {
+    createOrUpdate(localAdmin, localRecord(state, peerUrl));
+  }
+
+  private void writePeer(HAGroupStoreRecord.HAGroupState state) throws 
Exception {
+    createOrUpdate(peerAdmin, peerRecord(state));
+  }
+
+  private void createOrUpdate(PhoenixHAAdmin admin, HAGroupStoreRecord record) 
throws Exception {
+    String path = toPath(group());
+    if (admin.getCurator().checkExists().forPath(path) == null) {
+      admin.createHAGroupStoreRecordInZooKeeper(record);
+      return;
+    }
+    Pair<HAGroupStoreRecord, Stat> current = 
admin.getHAGroupStoreRecordInZooKeeper(group());
+    admin.updateHAGroupStoreRecordInZooKeeper(group(), record, 
current.getRight().getVersion());
+  }
+
+  private void deleteZNodes() throws Exception {
+    localAdmin.getCurator().delete().quietly().forPath(toPath(group()));
+    peerAdmin.getCurator().delete().quietly().forPath(toPath(group()));
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HAGroupStoreRecordTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HAGroupStoreRecordTest.java
index 3fb77d4bd4..ee451d1207 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HAGroupStoreRecordTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/HAGroupStoreRecordTest.java
@@ -252,6 +252,25 @@ public class HAGroupStoreRecordTest {
   }
 
   // Tests for HAGroupState enum
+  @Test
+  public void testHAGroupStateMetricContract() {
+    assertEquals(1, 
HAGroupStoreRecord.HAGroupState.ABORT_TO_ACTIVE_IN_SYNC.getMetricCode());
+    assertEquals(2, 
HAGroupStoreRecord.HAGroupState.ABORT_TO_ACTIVE_NOT_IN_SYNC.getMetricCode());
+    assertEquals(3, 
HAGroupStoreRecord.HAGroupState.ABORT_TO_STANDBY.getMetricCode());
+    assertEquals(4, 
HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC.getMetricCode());
+    assertEquals(5, 
HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC.getMetricCode());
+    assertEquals(6, 
HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC_TO_STANDBY.getMetricCode());
+    assertEquals(7,
+      
HAGroupStoreRecord.HAGroupState.ACTIVE_NOT_IN_SYNC_WITH_OFFLINE_PEER.getMetricCode());
+    assertEquals(8, 
HAGroupStoreRecord.HAGroupState.ACTIVE_IN_SYNC_TO_STANDBY.getMetricCode());
+    assertEquals(9, 
HAGroupStoreRecord.HAGroupState.ACTIVE_WITH_OFFLINE_PEER.getMetricCode());
+    assertEquals(10, 
HAGroupStoreRecord.HAGroupState.DEGRADED_STANDBY.getMetricCode());
+    assertEquals(11, HAGroupStoreRecord.HAGroupState.OFFLINE.getMetricCode());
+    assertEquals(12, HAGroupStoreRecord.HAGroupState.STANDBY.getMetricCode());
+    assertEquals(13, 
HAGroupStoreRecord.HAGroupState.STANDBY_TO_ACTIVE.getMetricCode());
+    assertEquals(0, HAGroupStoreRecord.HAGroupState.UNKNOWN.getMetricCode());
+  }
+
   @Test
   public void testHAGroupStateGetClusterRole() {
     // Test that each HAGroupState maps to the correct ClusterRole
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImplTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImplTest.java
new file mode 100644
index 0000000000..9eabdd97b9
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/jdbc/metrics/HAGroupStoreMetricsSourceImplTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.phoenix.jdbc.metrics;
+
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.CURRENT_LOCAL_STATE;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.CURRENT_PEER_STATE;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.DEGRADED_STANDBY_ACTIVE;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.DEGRADED_STANDBY_PRESENTED_COUNT;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.HA_GROUP_TAG_NAME;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.INVALID_TRANSITION_REJECTED_COUNT;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.LOCAL_CACHE_HEALTH_STATUS;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.LOCAL_ZK_CONNECTION_LOST_COUNT;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.NOTIFICATION_LISTENER_ERROR_COUNT;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.PEER_BLIND_COUNT;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.PEER_VISIBILITY_STATUS;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.SUBSCRIBER_NOTIFY_TIME_MS;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE;
+import static 
org.apache.phoenix.jdbc.metrics.HAGroupStoreMetricsSource.SYSTEM_TABLE_SYNC_FAILED_COUNT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.management.ObjectName;
+import org.apache.hadoop.metrics2.AbstractMetric;
+import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl;
+import org.apache.hadoop.metrics2.impl.MetricsRecordImpl;
+import org.apache.hadoop.metrics2.lib.MutableFastCounter;
+import org.apache.hadoop.metrics2.lib.MutableGaugeLong;
+import org.apache.hadoop.metrics2.lib.MutableMetric;
+import org.apache.hadoop.metrics2.lib.MutableTimeHistogram;
+import org.apache.phoenix.jdbc.HAGroupStoreRecord.HAGroupState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HAGroupStoreMetricsSourceImplTest {
+
+  private HAGroupStoreMetricsSourceImpl source;
+
+  @Before
+  public void setUp() {
+    source = new HAGroupStoreMetricsSourceImpl("metrics-test-" + 
System.nanoTime());
+  }
+
+  @After
+  public void tearDown() {
+    source.unregisterForTesting();
+  }
+
+  @Test
+  public void testMetricNamesAreUniqueAndPrefixed() {
+    Set<String> metricNames = new 
HashSet<>(Arrays.asList(LOCAL_CACHE_HEALTH_STATUS,
+      PEER_VISIBILITY_STATUS, DEGRADED_STANDBY_ACTIVE, CURRENT_LOCAL_STATE, 
CURRENT_PEER_STATE,
+      LOCAL_ZK_CONNECTION_LOST_COUNT, PEER_BLIND_COUNT, 
DEGRADED_STANDBY_PRESENTED_COUNT,
+      INVALID_TRANSITION_REJECTED_COUNT, SYSTEM_TABLE_SYNC_FAILED_COUNT,
+      NOTIFICATION_LISTENER_ERROR_COUNT, SUBSCRIBER_NOTIFY_TIME_MS));
+
+    assertEquals(12, metricNames.size());
+    for (String metricName : metricNames) {
+      assertTrue(metricName, metricName.startsWith("haGroupStore"));
+    }
+  }
+
+  @Test
+  public void testMetricRegistryContract() {
+    Map<String, MutableMetric> metrics = 
source.getMetricsRegistry().getMetricsMap();
+    assertEquals(12, metrics.size());
+
+    for (String gauge : Arrays.asList(LOCAL_CACHE_HEALTH_STATUS, 
PEER_VISIBILITY_STATUS,
+      DEGRADED_STANDBY_ACTIVE, CURRENT_LOCAL_STATE, CURRENT_PEER_STATE)) {
+      assertTrue(gauge, metrics.get(gauge) instanceof MutableGaugeLong);
+    }
+    for (String counter : Arrays.asList(LOCAL_ZK_CONNECTION_LOST_COUNT, 
PEER_BLIND_COUNT,
+      DEGRADED_STANDBY_PRESENTED_COUNT, INVALID_TRANSITION_REJECTED_COUNT,
+      SYSTEM_TABLE_SYNC_FAILED_COUNT, NOTIFICATION_LISTENER_ERROR_COUNT)) {
+      assertTrue(counter, metrics.get(counter) instanceof MutableFastCounter);
+    }
+    assertTrue(metrics.get(SUBSCRIBER_NOTIFY_TIME_MS) instanceof 
MutableTimeHistogram);
+  }
+
+  @Test
+  public void testMetricsSnapshot() {
+    source.setLocalCacheHealthy(true);
+    source.setPeerVisible(true);
+    source.setDegradedStandbyActive(true);
+    source.setCurrentLocalState(HAGroupState.STANDBY_TO_ACTIVE);
+    source.setCurrentPeerState(HAGroupState.ACTIVE_IN_SYNC);
+    source.incrementLocalZkConnectionLostCount();
+    source.incrementPeerBlindCount();
+    source.incrementDegradedStandbyPresentedCount();
+    source.incrementInvalidTransitionRejectedCount();
+    source.incrementSystemTableSyncFailedCount();
+    source.incrementNotificationListenerErrorCount();
+    source.updateSubscriberNotifyTime(5_000_000L);
+
+    HAGroupStoreMetricValues values = source.getCurrentMetricValues();
+    assertEquals(0L, values.getLocalCacheHealthStatus());
+    assertEquals(0L, values.getPeerVisibilityStatus());
+    assertEquals(1L, values.getDegradedStandbyActive());
+    assertEquals(HAGroupState.STANDBY_TO_ACTIVE.getMetricCode(), 
values.getCurrentLocalState());
+    assertEquals(HAGroupState.ACTIVE_IN_SYNC.getMetricCode(), 
values.getCurrentPeerState());
+    assertEquals(1L, values.getLocalZkConnectionLostCount());
+    assertEquals(1L, values.getPeerBlindCount());
+    assertEquals(1L, values.getDegradedStandbyPresentedCount());
+    assertEquals(1L, values.getInvalidTransitionRejectedCount());
+    assertEquals(1L, values.getSystemTableSyncFailedCount());
+    assertEquals(1L, values.getNotificationListenerErrorCount());
+    assertEquals(1L, values.getSubscriberNotifyTimeCount());
+    assertEquals(5L, values.getSubscriberNotifyTimeMaxMs());
+  }
+
+  @Test
+  public void testSubMillisecondNotifyTimeRoundsDownToZero() {
+    source.updateSubscriberNotifyTime(999_999L);
+
+    HAGroupStoreMetricValues values = source.getCurrentMetricValues();
+    assertEquals(1L, values.getSubscriberNotifyTimeCount());
+    assertEquals(0L, values.getSubscriberNotifyTimeMaxMs());
+  }
+
+  @Test
+  public void testExportedHistogramNames() {
+    source.updateSubscriberNotifyTime(5_000_000L);
+    MetricsCollectorImpl collector = new MetricsCollectorImpl();
+    source.getMetrics(collector, true);
+
+    Set<String> exportedNames = new HashSet<>();
+    for (MetricsRecordImpl record : collector.getRecords()) {
+      for (AbstractMetric metric : record.metrics()) {
+        exportedNames.add(metric.name());
+      }
+    }
+
+    assertTrue(exportedNames.contains(SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE 
+ "_num_ops"));
+    assertTrue(exportedNames.contains(SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE 
+ "_max"));
+    assertTrue(
+      exportedNames.contains(SUBSCRIBER_NOTIFY_TIME_MS_EXPORTED_BASE + 
"_99th_percentile"));
+    assertFalse(exportedNames.contains(SUBSCRIBER_NOTIFY_TIME_MS + 
"_num_ops"));
+  }
+
+  @Test
+  public void testFactoryCachesSourceByGroup() {
+    String group = "factory-test-" + System.nanoTime();
+    assertSame(HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(group),
+      HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(group));
+  }
+
+  @Test
+  public void testFactoryIsolatesGroups() {
+    String prefix = "factory-isolation-" + System.nanoTime();
+    HAGroupStoreMetricsSource first =
+      HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(prefix + "-1");
+    HAGroupStoreMetricsSource second =
+      HAGroupStoreMetricsSourceFactory.getInstanceForHAGroup(prefix + "-2");
+
+    assertNotSame(first, second);
+    assertNotEquals(first.getMetricsJmxContext(), 
second.getMetricsJmxContext());
+    first.incrementInvalidTransitionRejectedCount();
+    assertEquals(1L, 
first.getCurrentMetricValues().getInvalidTransitionRejectedCount());
+    assertEquals(0L, 
second.getCurrentMetricValues().getInvalidTransitionRejectedCount());
+  }
+
+  @Test
+  public void testGroupIdentityIsTaggedAndQuoted() {
+    String group = "group,one=" + System.nanoTime();
+    HAGroupStoreMetricsSourceImpl taggedSource = new 
HAGroupStoreMetricsSourceImpl(group);
+    try {
+      assertEquals(group, 
taggedSource.getMetricsRegistry().getTag(HA_GROUP_TAG_NAME).value());
+      assertTrue(
+        taggedSource.getMetricsJmxContext().endsWith(",haGroup=" + 
ObjectName.quote(group)));
+    } finally {
+      taggedSource.unregisterForTesting();
+    }
+  }
+}

Reply via email to