This is an automated email from the ASF dual-hosted git repository.
jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new b9a94c0b1b feature: add metrics support for NamingServer (#7882)
b9a94c0b1b is described below
commit b9a94c0b1b008314db528ff6c685d19593b7afb9
Author: contrueCT <[email protected]>
AuthorDate: Mon Jan 19 09:47:20 2026 +0800
feature: add metrics support for NamingServer (#7882)
---
changes/en-us/2.x.md | 3 +-
changes/zh-cn/2.x.md | 2 +
namingserver/pom.xml | 9 +
.../namingserver/listener/ClusterChangeEvent.java | 28 ++-
...hangeEvent.java => ClusterChangePushEvent.java} | 46 ++--
.../manager/ClusterWatcherManager.java | 28 ++-
.../seata/namingserver/manager/NamingManager.java | 28 ++-
.../metrics/NamingServerMetricsManager.java | 63 ++++++
.../metrics/NamingServerTagsContributor.java | 64 ++++++
.../metrics/NoOpNamingMetricsManager.java | 53 +++++
.../metrics/PrometheusNamingMetricsManager.java | 181 ++++++++++++++++
namingserver/src/main/resources/application.yml | 16 +-
.../namingserver/ClusterWatcherManagerTest.java | 26 ++-
.../seata/namingserver/NamingControllerV2Test.java | 32 ++-
.../seata/namingserver/NamingManagerTest.java | 10 +-
.../NamingServerMetricsManagerTest.java | 233 +++++++++++++++++++++
namingserver/src/test/resources/application.yml | 14 ++
17 files changed, 781 insertions(+), 55 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 92e9d15aa4..a577c31d8a 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -19,7 +19,7 @@ Add changes here for all PR submitted to the 2.x branch.
<!-- Please add the `changes` to the following
location(feature/bugfix/optimize/test) based on the type of PR -->
### feature:
-
+- [[#7882](https://github.com/apache/incubator-seata/pull/7882)] add metrics
for NamingServer
- [[#7760](https://github.com/apache/incubator-seata/pull/7760)] unify
Jackson/fastjson serialization
@@ -53,6 +53,7 @@ Thanks to these contributors for their code commits. Please
report an unintended
<!-- Please make sure your Github ID is in the list below -->
- [slievrly](https://github.com/slievrly)
+- [contrueCT](https://github.com/contrueCT)
- [lokidundun](https://github.com/lokidundun)
- [LegendPei](https://github.com/LegendPei)
- [funky-eyes](https://github.com/funky-eyes)
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 609b123bb8..399ad9317d 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -20,6 +20,7 @@
### feature:
+- [[#7882](https://github.com/apache/incubator-seata/pull/7882)]
为NamingServer增加Metrics监控
- [[#7760](https://github.com/apache/incubator-seata/pull/7760)]
统一Jackson/fastjson序列化器
@@ -53,6 +54,7 @@
<!-- 请确保您的 GitHub ID 在以下列表中 -->
- [slievrly](https://github.com/slievrly)
+- [contrueCT](https://github.com/contrueCT)
- [lokidundun](https://github.com/lokidundun)
- [LegendPei](https://github.com/LegendPei)
- [funky-eyes](https://github.com/funky-eyes)
diff --git a/namingserver/pom.xml b/namingserver/pom.xml
index 0d2e9ff161..46268dc064 100644
--- a/namingserver/pom.xml
+++ b/namingserver/pom.xml
@@ -104,6 +104,15 @@
<artifactId>spring-boot-starter</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.micrometer</groupId>
+ <artifactId>micrometer-registry-prometheus</artifactId>
+ </dependency>
+
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
b/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
index 0564a9b057..7d81f5dc6a 100644
---
a/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
@@ -24,17 +24,25 @@ public class ClusterChangeEvent extends ApplicationEvent {
private String group;
+ private String namespace;
+
+ private String clusterName;
+
private long term;
- public ClusterChangeEvent(Object source, String group, long term) {
+ public ClusterChangeEvent(Object source, String group, String namespace,
String clusterName, long term) {
super(source);
this.group = group;
+ this.namespace = namespace;
+ this.clusterName = clusterName;
this.term = term;
}
- public ClusterChangeEvent(Object source, String group) {
+ public ClusterChangeEvent(Object source, String group, String namespace,
String clusterName) {
super(source);
this.group = group;
+ this.namespace = namespace;
+ this.clusterName = clusterName;
}
public ClusterChangeEvent(Object source, Clock clock) {
@@ -49,6 +57,22 @@ public class ClusterChangeEvent extends ApplicationEvent {
this.group = group;
}
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
public long getTerm() {
return term;
}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
b/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangePushEvent.java
similarity index 57%
copy from
namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
copy to
namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangePushEvent.java
index 0564a9b057..ed20689fe3 100644
---
a/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangeEvent.java
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/listener/ClusterChangePushEvent.java
@@ -18,42 +18,32 @@ package org.apache.seata.namingserver.listener;
import org.springframework.context.ApplicationEvent;
-import java.time.Clock;
-
-public class ClusterChangeEvent extends ApplicationEvent {
-
- private String group;
-
- private long term;
+/**
+ * Event published when cluster change notifications are pushed to watchers.
+ * Used for metrics tracking via Spring's event mechanism.
+ */
+public class ClusterChangePushEvent extends ApplicationEvent {
- public ClusterChangeEvent(Object source, String group, long term) {
- super(source);
- this.group = group;
- this.term = term;
- }
+ private final String namespace;
+ private final String clusterName;
+ private final String vgroup;
- public ClusterChangeEvent(Object source, String group) {
+ public ClusterChangePushEvent(Object source, String namespace, String
clusterName, String vgroup) {
super(source);
- this.group = group;
- }
-
- public ClusterChangeEvent(Object source, Clock clock) {
- super(source, clock);
- }
-
- public String getGroup() {
- return group;
+ this.namespace = namespace;
+ this.clusterName = clusterName;
+ this.vgroup = vgroup;
}
- public void setGroup(String group) {
- this.group = group;
+ public String getNamespace() {
+ return namespace;
}
- public long getTerm() {
- return term;
+ public String getClusterName() {
+ return clusterName;
}
- public void setTerm(long term) {
- this.term = term;
+ public String getVgroup() {
+ return vgroup;
}
}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/ClusterWatcherManager.java
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/ClusterWatcherManager.java
index 86d40b5faf..1f6f9e3094 100644
---
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/ClusterWatcherManager.java
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/ClusterWatcherManager.java
@@ -21,9 +21,13 @@ import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.seata.namingserver.listener.ClusterChangeEvent;
import org.apache.seata.namingserver.listener.ClusterChangeListener;
+import org.apache.seata.namingserver.listener.ClusterChangePushEvent;
import org.apache.seata.namingserver.listener.Watcher;
+import org.apache.seata.namingserver.metrics.NamingServerMetricsManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
@@ -54,8 +58,17 @@ public class ClusterWatcherManager implements
ClusterChangeListener {
private final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor =
new ScheduledThreadPoolExecutor(1, new
CustomizableThreadFactory("long-polling"));
+ @Autowired
+ private NamingServerMetricsManager metricsManager;
+
+ @Autowired
+ private ApplicationEventPublisher eventPublisher;
+
@PostConstruct
public void init() {
+ // Register metrics data supplier
+ metricsManager.setWatchersSupplier(() -> WATCHERS);
+
// Responds to monitors that time out
scheduledThreadPoolExecutor.scheduleAtFixedRate(
() -> {
@@ -85,8 +98,16 @@ public class ClusterWatcherManager implements
ClusterChangeListener {
GROUP_UPDATE_TERM.put(event.getGroup(), event.getTerm());
// Notifications are made of changes in cluster information
- Optional.ofNullable(WATCHERS.remove(event.getGroup()))
- .ifPresent(watchers ->
watchers.parallelStream().forEach(this::notify));
+
Optional.ofNullable(WATCHERS.remove(event.getGroup())).ifPresent(watchers -> {
+ watchers.parallelStream().forEach(this::notify);
+ // Publish event for metrics tracking
+ if (!watchers.isEmpty()) {
+ eventPublisher.publishEvent(new ClusterChangePushEvent(
+ this, event.getNamespace(),
event.getClusterName(), event.getGroup()));
+ // Refresh watcher count metrics after notification
+ metricsManager.refreshWatcherCountMetrics();
+ }
+ });
}
}
@@ -113,6 +134,9 @@ public class ClusterWatcherManager implements
ClusterChangeListener {
if (term == null || watcher.getTerm() >= term) {
WATCHERS.computeIfAbsent(group, value -> new
ConcurrentLinkedQueue<>())
.add(watcher);
+
+ // Immediately refresh watcher count metrics
+ metricsManager.refreshWatcherCountMetrics();
} else {
notify(watcher);
}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
index 13a6e42cdf..895b2a1e9d 100644
---
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
@@ -41,6 +41,7 @@ import org.apache.seata.namingserver.entity.pojo.ClusterData;
import org.apache.seata.namingserver.entity.vo.NamespaceVO;
import org.apache.seata.namingserver.entity.vo.monitor.ClusterVO;
import org.apache.seata.namingserver.listener.ClusterChangeEvent;
+import org.apache.seata.namingserver.metrics.NamingServerMetricsManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
@@ -92,6 +93,9 @@ public class NamingManager {
@Autowired
private ApplicationContext applicationContext;
+ @Autowired
+ private NamingServerMetricsManager metricsManager;
+
public NamingManager() {
this.instanceLiveTable = new ConcurrentHashMap<>();
this.namespaceClusterDataMap = new ConcurrentHashMap<>();
@@ -123,6 +127,9 @@ public class NamingManager {
heartbeatCheckTimePeriod,
heartbeatCheckTimePeriod,
TimeUnit.MILLISECONDS);
+
+ // Register metrics data supplier
+ metricsManager.setNamespaceClusterDataSupplier(() ->
namespaceClusterDataMap);
}
public List<ClusterVO> monitorCluster(String namespace) {
@@ -299,7 +306,7 @@ public class NamingManager {
.flatMap(map -> Optional.ofNullable(map.get(namespace))
.flatMap(namespaceBO ->
Optional.ofNullable(namespaceBO.getCluster(clusterName))))
.ifPresent(clusterBO -> {
- applicationContext.publishEvent(new
ClusterChangeEvent(this, vGroup, term));
+ applicationContext.publishEvent(new
ClusterChangeEvent(this, vGroup, namespace, clusterName, term));
});
}
@@ -327,12 +334,15 @@ public class NamingManager {
clusterName, (String)
node.getMetadata().get("cluster-type")));
boolean hasChanged = clusterData.registerInstance(node, unitName);
Object mappingObj = node.getMetadata().get(CONSTANT_GROUP);
- // if extended metadata includes vgroup mapping relationship, add
it in clusterData
+ // if extended metadata includes vgroup mapping relationship, add
it in
+ // clusterData
if (mappingObj instanceof Map) {
Map<String, String> vGroups = (Map<String, String>) mappingObj;
vGroups.forEach((k, v) -> {
- // In non-raft mode, a unit is one-to-one with a node, and
the unitName is stored on the node.
- // In raft mode, the unitName is equal to the raft-group,
so the node's unitName cannot be used.
+ // In non-raft mode, a unit is one-to-one with a node, and
the unitName is
+ // stored on the node.
+ // In raft mode, the unitName is equal to the raft-group,
so the node's unitName
+ // cannot be used.
boolean changed = addGroup(namespace, clusterName,
StringUtils.isBlank(v) ? unitName : v, k);
if (hasChanged || changed) {
notifyClusterChange(k, namespace, clusterName,
unitName, node.getTerm());
@@ -344,6 +354,9 @@ public class NamingManager {
node.getTransaction().getHost(),
node.getTransaction().getPort()),
System.currentTimeMillis());
+
+ // Immediately refresh cluster node count metrics
+ metricsManager.refreshClusterNodeCountMetrics();
} catch (Exception e) {
LOGGER.error("Instance registered failed:{}", e.getMessage(), e);
return false;
@@ -376,6 +389,9 @@ public class NamingManager {
node.getTransaction().getPort()));
}
}
+
+ // Immediately refresh cluster node count metrics
+ metricsManager.refreshClusterNodeCountMetrics();
} catch (Exception e) {
LOGGER.error("Instance unregistered failed:{}", e.getMessage(), e);
return false;
@@ -472,6 +488,10 @@ public class NamingManager {
instance.getTransaction().getHost() + ":"
+
instance.getTransaction().getPort());
}
+
+ // Immediately refresh cluster node count metrics
after removing offline
+ // instances
+ metricsManager.refreshClusterNodeCountMetrics();
}
}
}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerMetricsManager.java
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerMetricsManager.java
new file mode 100644
index 0000000000..119debdfa2
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerMetricsManager.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
+ *
+ * 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.seata.namingserver.metrics;
+
+import org.apache.seata.namingserver.entity.pojo.ClusterData;
+import org.apache.seata.namingserver.listener.Watcher;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
+
+/**
+ * Interface for NamingServer metrics management.
+ * Provides methods to track cluster nodes, watchers, and push notifications.
+ */
+public interface NamingServerMetricsManager {
+
+ // Metric names
+ String METRIC_CLUSTER_NODE_COUNT = "seata_namingserver_cluster_node_count";
+ String METRIC_WATCHER_COUNT = "seata_namingserver_watcher_count";
+ String METRIC_CLUSTER_CHANGE_PUSH_TOTAL =
"seata_namingserver_cluster_change_push_total";
+
+ // Tag names
+ String TAG_NAMESPACE = "namespace";
+ String TAG_CLUSTER = "cluster";
+ String TAG_UNIT = "unit";
+ String TAG_VGROUP = "vgroup";
+
+ /**
+ * Sets the supplier for namespace-cluster data used by cluster node count
metrics.
+ */
+ void setNamespaceClusterDataSupplier(Supplier<ConcurrentMap<String,
ConcurrentMap<String, ClusterData>>> supplier);
+
+ /**
+ * Sets the supplier for watchers data used by watcher count metrics.
+ */
+ void setWatchersSupplier(Supplier<Map<String, Queue<Watcher<?>>>>
supplier);
+
+ /**
+ * Refreshes the cluster node count metrics based on current registry data.
+ */
+ void refreshClusterNodeCountMetrics();
+
+ /**
+ * Refreshes the watcher count metrics based on current long-polling
connections.
+ */
+ void refreshWatcherCountMetrics();
+}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerTagsContributor.java
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerTagsContributor.java
new file mode 100644
index 0000000000..7231317f21
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NamingServerTagsContributor.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.seata.namingserver.metrics;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import
org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
+import
org.springframework.http.server.observation.ServerRequestObservationContext;
+import org.springframework.stereotype.Component;
+
+/**
+ * Custom tags contributor for HTTP server request metrics.
+ * Adds Seata business dimensions (namespace, cluster, vgroup) to
+ * http.server.requests metrics.
+ */
+@Component
+@ConditionalOnProperty(name = "seata.namingserver.metrics.enabled",
havingValue = "true")
+public class NamingServerTagsContributor extends
DefaultServerRequestObservationConvention {
+
+ private static final String TAG_NAMESPACE = "namespace";
+ private static final String TAG_CLUSTER = "cluster";
+ private static final String TAG_VGROUP = "vgroup";
+ private static final String UNKNOWN = "unknown";
+
+ @Override
+ public KeyValues
getLowCardinalityKeyValues(ServerRequestObservationContext context) {
+ KeyValues keyValues = super.getLowCardinalityKeyValues(context);
+
+ // Add namespace tag
+ String namespace = context.getCarrier().getParameter(TAG_NAMESPACE);
+ keyValues = keyValues.and(KeyValue.of(TAG_NAMESPACE, namespace != null
? namespace : UNKNOWN));
+
+ // Add cluster tag
+ String cluster = context.getCarrier().getParameter(TAG_CLUSTER);
+ if (cluster == null) {
+ cluster = context.getCarrier().getParameter("clusterName");
+ }
+ keyValues = keyValues.and(KeyValue.of(TAG_CLUSTER, cluster != null ?
cluster : UNKNOWN));
+
+ // Add vgroup tag
+ String vgroup = context.getCarrier().getParameter(TAG_VGROUP);
+ if (vgroup == null) {
+ vgroup = context.getCarrier().getParameter("group");
+ }
+ keyValues = keyValues.and(KeyValue.of(TAG_VGROUP, vgroup != null ?
vgroup : UNKNOWN));
+
+ return keyValues;
+ }
+}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NoOpNamingMetricsManager.java
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NoOpNamingMetricsManager.java
new file mode 100644
index 0000000000..69754ce549
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/NoOpNamingMetricsManager.java
@@ -0,0 +1,53 @@
+/*
+ * 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.seata.namingserver.metrics;
+
+import org.apache.seata.namingserver.entity.pojo.ClusterData;
+import org.apache.seata.namingserver.listener.Watcher;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
+
+@Component
+@ConditionalOnProperty(name = "seata.namingserver.metrics.enabled",
havingValue = "false", matchIfMissing = true)
+public class NoOpNamingMetricsManager implements NamingServerMetricsManager {
+
+ @Override
+ public void setNamespaceClusterDataSupplier(
+ Supplier<ConcurrentMap<String, ConcurrentMap<String,
ClusterData>>> supplier) {
+ // No-op
+ }
+
+ @Override
+ public void setWatchersSupplier(Supplier<Map<String, Queue<Watcher<?>>>>
supplier) {
+ // No-op
+ }
+
+ @Override
+ public void refreshClusterNodeCountMetrics() {
+ // No-op
+ }
+
+ @Override
+ public void refreshWatcherCountMetrics() {
+ // No-op
+ }
+}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/metrics/PrometheusNamingMetricsManager.java
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/PrometheusNamingMetricsManager.java
new file mode 100644
index 0000000000..4954f27c0a
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/metrics/PrometheusNamingMetricsManager.java
@@ -0,0 +1,181 @@
+/*
+ * 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.seata.namingserver.metrics;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.MultiGauge;
+import io.micrometer.core.instrument.Tags;
+import jakarta.annotation.PostConstruct;
+import org.apache.seata.common.metadata.namingserver.Unit;
+import org.apache.seata.namingserver.entity.pojo.ClusterData;
+import org.apache.seata.namingserver.listener.ClusterChangePushEvent;
+import org.apache.seata.namingserver.listener.Watcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
+
+/**
+ * Prometheus-based implementation of NamingServerMetricsManager.
+ * Uses Micrometer MultiGauge for dynamic tag management and automatic cleanup
of stale metrics.
+ */
+@Component
+@ConditionalOnProperty(name = "seata.namingserver.metrics.enabled",
havingValue = "true")
+public class PrometheusNamingMetricsManager implements
NamingServerMetricsManager {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(PrometheusNamingMetricsManager.class);
+
+ private final MeterRegistry meterRegistry;
+
+ private MultiGauge clusterNodeCountGauge;
+
+ private MultiGauge watcherCountGauge;
+
+ private final ConcurrentMap<String, Counter> clusterChangePushCounters =
new ConcurrentHashMap<>();
+
+ private Supplier<ConcurrentMap<String, ConcurrentMap<String,
ClusterData>>> namespaceClusterDataSupplier;
+ private Supplier<Map<String, Queue<Watcher<?>>>> watchersSupplier;
+
+ public PrometheusNamingMetricsManager(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
+ }
+
+ @PostConstruct
+ public void init() {
+ // Initialize MultiGauge for cluster node count
+ this.clusterNodeCountGauge =
MultiGauge.builder(METRIC_CLUSTER_NODE_COUNT)
+ .description("Number of alive Seata Server nodes in the
registry")
+ .register(meterRegistry);
+
+ // Initialize MultiGauge for watcher count
+ this.watcherCountGauge = MultiGauge.builder(METRIC_WATCHER_COUNT)
+ .description("Number of HTTP connections waiting for change
notifications (long polling)")
+ .register(meterRegistry);
+
+ LOGGER.info("NamingServer Prometheus metrics manager initialized with
event-driven refresh");
+ }
+
+ /**
+ * Listens for ClusterChangePushEvent and increments the push counter.
+ */
+ @EventListener
+ public void onClusterChangePush(ClusterChangePushEvent event) {
+ incrementClusterChangePushCount(event.getNamespace(),
event.getClusterName(), event.getVgroup());
+ }
+
+ @Override
+ public void setNamespaceClusterDataSupplier(
+ Supplier<ConcurrentMap<String, ConcurrentMap<String,
ClusterData>>> supplier) {
+ this.namespaceClusterDataSupplier = supplier;
+ }
+
+ @Override
+ public void setWatchersSupplier(Supplier<Map<String, Queue<Watcher<?>>>>
supplier) {
+ this.watchersSupplier = supplier;
+ }
+
+ @Override
+ public void refreshClusterNodeCountMetrics() {
+ if (namespaceClusterDataSupplier == null) {
+ return;
+ }
+
+ List<MultiGauge.Row<?>> rows = new ArrayList<>();
+ ConcurrentMap<String, ConcurrentMap<String, ClusterData>>
namespaceClusterDataMap =
+ namespaceClusterDataSupplier.get();
+
+ if (namespaceClusterDataMap != null) {
+ namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> {
+ if (clusterDataMap != null) {
+ clusterDataMap.forEach((clusterName, clusterData) -> {
+ if (clusterData != null && clusterData.getUnitData()
!= null) {
+ Map<String, Unit> unitData =
clusterData.getUnitData();
+ unitData.forEach((unitName, unit) -> {
+ int nodeCount = 0;
+ if (unit != null &&
unit.getNamingInstanceList() != null) {
+ nodeCount =
unit.getNamingInstanceList().size();
+ }
+ rows.add(MultiGauge.Row.of(
+ Tags.of(
+ TAG_NAMESPACE, namespace,
+ TAG_CLUSTER, clusterName,
+ TAG_UNIT, unitName),
+ nodeCount));
+ });
+ }
+ });
+ }
+ });
+ }
+
+ clusterNodeCountGauge.register(rows, true);
+ }
+
+ @Override
+ public void refreshWatcherCountMetrics() {
+ if (watchersSupplier == null) {
+ return;
+ }
+
+ List<MultiGauge.Row<?>> rows = new ArrayList<>();
+ Map<String, Queue<Watcher<?>>> watchers = watchersSupplier.get();
+
+ if (watchers != null) {
+ watchers.forEach((vgroup, watcherQueue) -> {
+ int count = 0;
+ if (watcherQueue != null) {
+ count = watcherQueue.size();
+ }
+ rows.add(MultiGauge.Row.of(Tags.of(TAG_VGROUP, vgroup),
count));
+ });
+ }
+
+ watcherCountGauge.register(rows, true);
+ }
+
+ private void incrementClusterChangePushCount(String namespace, String
cluster, String vgroup) {
+ String key = namespace + "|" + cluster + "|" + vgroup;
+ Counter counter =
+ clusterChangePushCounters.computeIfAbsent(key, k ->
Counter.builder(METRIC_CLUSTER_CHANGE_PUSH_TOTAL)
+ .description("Total number of cluster change push
notifications to watchers")
+ .tag(TAG_NAMESPACE, namespace)
+ .tag(TAG_CLUSTER, cluster)
+ .tag(TAG_VGROUP, vgroup)
+ .register(meterRegistry));
+ counter.increment();
+ }
+
+ /**
+ * Gets the current count of cluster change push notifications.
+ * Exposed for testing purposes.
+ */
+ public double getClusterChangePushCount(String namespace, String cluster,
String vgroup) {
+ String key = namespace + "|" + cluster + "|" + vgroup;
+ Counter counter = clusterChangePushCounters.get(key);
+ return counter != null ? counter.count() : 0;
+ }
+}
diff --git a/namingserver/src/main/resources/application.yml
b/namingserver/src/main/resources/application.yml
index e8ba1939c3..892bfe4b66 100644
--- a/namingserver/src/main/resources/application.yml
+++ b/namingserver/src/main/resources/application.yml
@@ -51,14 +51,28 @@ console:
# addr:
# - 127.0.0.1:8081
seata:
+ namingserver:
+ metrics:
+ enabled: true
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
csrf-ignore-urls: /naming/v1/**,/api/v1/naming/**
ignore:
urls:
/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/naming/v1/health,/error
+management:
+ endpoints:
+ web:
+ exposure:
+ include: "prometheus,health,info,metrics"
+ metrics:
+ tags:
+ application: ${spring.application.name}
+ distribution:
+ percentiles:
+ http.server.requests: 0.5, 0.99, 0.999
# MCP Server Configuration
mcp:
# Maximum query time interval, The unit is milliseconds, Default one day
query:
- max-query-duration: 86400000
\ No newline at end of file
+ max-query-duration: 86400000
diff --git
a/namingserver/src/test/java/org/apache/seata/namingserver/ClusterWatcherManagerTest.java
b/namingserver/src/test/java/org/apache/seata/namingserver/ClusterWatcherManagerTest.java
index 6a1addff7a..e32a3b47e5 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/ClusterWatcherManagerTest.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/ClusterWatcherManagerTest.java
@@ -22,11 +22,16 @@ import jakarta.servlet.http.HttpServletResponse;
import org.apache.seata.namingserver.listener.ClusterChangeEvent;
import org.apache.seata.namingserver.listener.Watcher;
import org.apache.seata.namingserver.manager.ClusterWatcherManager;
+import org.apache.seata.namingserver.metrics.NoOpNamingMetricsManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.test.util.ReflectionTestUtils;
@@ -42,7 +47,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-@SpringBootTest
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
public class ClusterWatcherManagerTest {
private ClusterWatcherManager clusterWatcherManager;
@@ -56,7 +62,12 @@ public class ClusterWatcherManagerTest {
@Mock
private HttpServletRequest request;
+ @Mock
+ private ApplicationEventPublisher eventPublisher;
+
private final String TEST_GROUP = "testGroup";
+ private final String TEST_NAMESPACE = "testNamespace";
+ private final String TEST_CLUSTER = "testCluster";
private final int TEST_TIMEOUT = 5000;
private final Long TEST_TERM = 1000L;
private final String TEST_CLIENT_ENDPOINT = "127.0.0.1";
@@ -64,6 +75,10 @@ public class ClusterWatcherManagerTest {
@BeforeEach
void setUp() {
clusterWatcherManager = new ClusterWatcherManager();
+ // Inject dependencies to avoid null pointer
+ ReflectionTestUtils.setField(clusterWatcherManager, "metricsManager",
new NoOpNamingMetricsManager());
+ ReflectionTestUtils.setField(clusterWatcherManager, "eventPublisher",
eventPublisher);
+
Mockito.when(asyncContext.getResponse()).thenReturn(response);
Mockito.when(asyncContext.getRequest()).thenReturn(request);
Mockito.when(request.getRemoteAddr()).thenReturn(TEST_CLIENT_ENDPOINT);
@@ -136,7 +151,7 @@ public class ClusterWatcherManagerTest {
assertNotNull(watchers);
assertNotNull(updateTime);
- ClusterChangeEvent zeroTermEvent = new ClusterChangeEvent(this,
TEST_GROUP, 0);
+ ClusterChangeEvent zeroTermEvent = new ClusterChangeEvent(this,
TEST_GROUP, TEST_NAMESPACE, TEST_CLUSTER, 0);
clusterWatcherManager.onChangeEvent(zeroTermEvent);
assertEquals(0, updateTime.size());
@@ -145,7 +160,8 @@ public class ClusterWatcherManagerTest {
assertNotNull(watchers.get(TEST_GROUP));
assertEquals(1, watchers.get(TEST_GROUP).size());
- ClusterChangeEvent event = new ClusterChangeEvent(this, TEST_GROUP,
TEST_TERM + 1);
+ ClusterChangeEvent event =
+ new ClusterChangeEvent(this, TEST_GROUP, TEST_NAMESPACE,
TEST_CLUSTER, TEST_TERM + 1);
clusterWatcherManager.onChangeEvent(event);
Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
@@ -246,7 +262,7 @@ public class ClusterWatcherManagerTest {
new Watcher<>(TEST_GROUP, asyncContext, TEST_TIMEOUT,
TEST_TERM, TEST_CLIENT_ENDPOINT);
clusterWatcherManager.registryWatcher(watcher);
- ClusterChangeEvent minus1TermEvent = new ClusterChangeEvent(this,
TEST_GROUP, -1);
+ ClusterChangeEvent minus1TermEvent = new ClusterChangeEvent(this,
TEST_GROUP, TEST_NAMESPACE, TEST_CLUSTER, -1);
clusterWatcherManager.onChangeEvent(minus1TermEvent);
Map<String, Long> updateTime =
diff --git
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
index 81b1985a8d..13c6f3faf1 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
@@ -22,11 +22,14 @@ import org.apache.seata.common.result.SingleResult;
import org.apache.seata.namingserver.controller.NamingControllerV2;
import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO;
import org.apache.seata.namingserver.manager.NamingManager;
+import org.apache.seata.namingserver.metrics.NoOpNamingMetricsManager;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.util.ReflectionTestUtils;
import java.util.Map;
import java.util.UUID;
@@ -36,15 +39,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-@RunWith(SpringRunner.class)
-@SpringBootTest
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
class NamingControllerV2Test {
- @Autowired
- NamingControllerV2 namingControllerV2;
+ private NamingControllerV2 namingControllerV2;
- @Autowired
- NamingManager namingManager;
+ private NamingManager namingManager;
+
+ @BeforeEach
+ void setUp() {
+ namingManager = new NamingManager();
+ ReflectionTestUtils.setField(namingManager, "metricsManager", new
NoOpNamingMetricsManager());
+ ReflectionTestUtils.setField(namingManager, "heartbeatTimeThreshold",
500000);
+ ReflectionTestUtils.setField(namingManager,
"heartbeatCheckTimePeriod", 10000000);
+ namingManager.init();
+ namingControllerV2 = new NamingControllerV2();
+ ReflectionTestUtils.setField(namingControllerV2, "namingManager",
namingManager);
+ }
@Test
void testNamespaces() {
diff --git
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
index 10169707b3..36eaccca81 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
@@ -31,13 +31,17 @@ import
org.apache.seata.namingserver.entity.vo.monitor.ClusterVO;
import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO;
import org.apache.seata.namingserver.listener.ClusterChangeEvent;
import org.apache.seata.namingserver.manager.NamingManager;
+import org.apache.seata.namingserver.metrics.NoOpNamingMetricsManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
-import org.springframework.boot.test.context.SpringBootTest;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
import org.springframework.context.ApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;
@@ -55,7 +59,8 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
-@SpringBootTest
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
class NamingManagerTest {
private NamingManager namingManager;
@@ -77,6 +82,7 @@ class NamingManagerTest {
ReflectionTestUtils.setField(namingManager, "applicationContext",
applicationContext);
ReflectionTestUtils.setField(namingManager, "heartbeatTimeThreshold",
500000);
ReflectionTestUtils.setField(namingManager,
"heartbeatCheckTimePeriod", 10000000);
+ ReflectionTestUtils.setField(namingManager, "metricsManager", new
NoOpNamingMetricsManager());
Mockito.when(httpResponse.code()).thenReturn(200);
Mockito.when(httpResponse.body()).thenReturn(responseBody);
diff --git
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingServerMetricsManagerTest.java
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingServerMetricsManagerTest.java
new file mode 100644
index 0000000000..d2e2bdd030
--- /dev/null
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingServerMetricsManagerTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.seata.namingserver;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.apache.seata.common.metadata.namingserver.NamingServerNode;
+import org.apache.seata.common.metadata.namingserver.Unit;
+import org.apache.seata.namingserver.entity.pojo.ClusterData;
+import org.apache.seata.namingserver.listener.ClusterChangePushEvent;
+import org.apache.seata.namingserver.listener.Watcher;
+import org.apache.seata.namingserver.metrics.PrometheusNamingMetricsManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static
org.apache.seata.namingserver.metrics.NamingServerMetricsManager.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for PrometheusNamingMetricsManager.
+ */
+class NamingServerMetricsManagerTest {
+
+ private MeterRegistry meterRegistry;
+ private PrometheusNamingMetricsManager metricsManager;
+
+ @BeforeEach
+ void setUp() {
+ meterRegistry = new SimpleMeterRegistry();
+ metricsManager = new PrometheusNamingMetricsManager(meterRegistry);
+ metricsManager.init();
+ }
+
+ @Test
+ void testClusterNodeCountMetrics() {
+ // Prepare test data
+ ConcurrentMap<String, ConcurrentMap<String, ClusterData>>
namespaceClusterDataMap = new ConcurrentHashMap<>();
+
+ // Create namespace -> cluster -> unit structure
+ String namespace = "test-namespace";
+ String clusterName = "test-cluster";
+ String unitName = "test-unit";
+
+ ClusterData clusterData = new ClusterData(clusterName, "default");
+ Unit unit = new Unit();
+ unit.setUnitName(unitName);
+ unit.setNamingInstanceList(new CopyOnWriteArrayList<>());
+
+ // Add some nodes
+ NamingServerNode node1 = new NamingServerNode();
+ NamingServerNode node2 = new NamingServerNode();
+ unit.getNamingInstanceList().add(node1);
+ unit.getNamingInstanceList().add(node2);
+
+ clusterData.getUnitData().put(unitName, unit);
+
+ ConcurrentMap<String, ClusterData> clusterDataMap = new
ConcurrentHashMap<>();
+ clusterDataMap.put(clusterName, clusterData);
+ namespaceClusterDataMap.put(namespace, clusterDataMap);
+
+ // Set the supplier
+ metricsManager.setNamespaceClusterDataSupplier(() ->
namespaceClusterDataMap);
+
+ // Manually trigger metrics refresh
+ metricsManager.refreshClusterNodeCountMetrics();
+
+ // Verify the metric is registered
+ Meter meter = meterRegistry
+ .find(METRIC_CLUSTER_NODE_COUNT)
+ .tag(TAG_NAMESPACE, namespace)
+ .tag(TAG_CLUSTER, clusterName)
+ .tag(TAG_UNIT, unitName)
+ .meter();
+
+ assertNotNull(meter, "Cluster node count metric should be registered");
+ }
+
+ @Test
+ void testWatcherCountMetrics() {
+ // Prepare test data
+ Map<String, Queue<Watcher<?>>> watchers = new ConcurrentHashMap<>();
+ String vgroup = "test-vgroup";
+
+ Queue<Watcher<?>> watcherQueue = new ConcurrentLinkedQueue<>();
+ watcherQueue.add(new Watcher<>(vgroup, null, 5000, 1L, "127.0.0.1"));
+ watcherQueue.add(new Watcher<>(vgroup, null, 5000, 1L, "127.0.0.2"));
+ watchers.put(vgroup, watcherQueue);
+
+ // Set the supplier
+ metricsManager.setWatchersSupplier(() -> watchers);
+
+ // Manually trigger metrics refresh
+ metricsManager.refreshWatcherCountMetrics();
+
+ // Verify the metric is registered
+ Meter meter =
+ meterRegistry.find(METRIC_WATCHER_COUNT).tag(TAG_VGROUP,
vgroup).meter();
+
+ assertNotNull(meter, "Watcher count metric should be registered");
+ }
+
+ @Test
+ void testClusterChangePushCounter() {
+ String namespace = "test-namespace";
+ String cluster = "test-cluster";
+ String vgroup1 = "vgroup1";
+ String vgroup2 = "vgroup2";
+
+ // Simulate events via event listener
+ metricsManager.onClusterChangePush(new ClusterChangePushEvent(this,
namespace, cluster, vgroup1));
+ metricsManager.onClusterChangePush(new ClusterChangePushEvent(this,
namespace, cluster, vgroup1));
+ metricsManager.onClusterChangePush(new ClusterChangePushEvent(this,
namespace, cluster, vgroup2));
+
+ // Verify counts
+ assertEquals(2.0, metricsManager.getClusterChangePushCount(namespace,
cluster, vgroup1));
+ assertEquals(1.0, metricsManager.getClusterChangePushCount(namespace,
cluster, vgroup2));
+
+ // Verify metrics are registered in registry
+ assertNotNull(meterRegistry
+ .find(METRIC_CLUSTER_CHANGE_PUSH_TOTAL)
+ .tag(TAG_NAMESPACE, namespace)
+ .tag(TAG_CLUSTER, cluster)
+ .tag(TAG_VGROUP, vgroup1)
+ .counter());
+ assertNotNull(meterRegistry
+ .find(METRIC_CLUSTER_CHANGE_PUSH_TOTAL)
+ .tag(TAG_NAMESPACE, namespace)
+ .tag(TAG_CLUSTER, cluster)
+ .tag(TAG_VGROUP, vgroup2)
+ .counter());
+ }
+
+ @Test
+ void testMultiGaugeCleanupStaleTags() {
+ // Prepare initial test data with two namespaces
+ ConcurrentMap<String, ConcurrentMap<String, ClusterData>>
namespaceClusterDataMap = new ConcurrentHashMap<>();
+
+ String namespace1 = "namespace1";
+ String namespace2 = "namespace2";
+ String clusterName = "cluster1";
+ String unitName = "unit1";
+
+ // Create two clusters in different namespaces
+ for (String namespace : new String[] {namespace1, namespace2}) {
+ ClusterData clusterData = new ClusterData(clusterName, "default");
+ Unit unit = new Unit();
+ unit.setUnitName(unitName);
+ unit.setNamingInstanceList(new CopyOnWriteArrayList<>());
+ unit.getNamingInstanceList().add(new NamingServerNode());
+ clusterData.getUnitData().put(unitName, unit);
+
+ ConcurrentMap<String, ClusterData> clusterDataMap = new
ConcurrentHashMap<>();
+ clusterDataMap.put(clusterName, clusterData);
+ namespaceClusterDataMap.put(namespace, clusterDataMap);
+ }
+
+ metricsManager.setNamespaceClusterDataSupplier(() ->
namespaceClusterDataMap);
+
+ // Manually trigger metrics refresh
+ metricsManager.refreshClusterNodeCountMetrics();
+
+ // Verify both metrics exist
+ assertNotNull(meterRegistry
+ .find(METRIC_CLUSTER_NODE_COUNT)
+ .tag(TAG_NAMESPACE, namespace1)
+ .meter());
+ assertNotNull(meterRegistry
+ .find(METRIC_CLUSTER_NODE_COUNT)
+ .tag(TAG_NAMESPACE, namespace2)
+ .meter());
+
+ // Remove namespace2
+ namespaceClusterDataMap.remove(namespace2);
+
+ // Manually trigger metrics refresh
+ metricsManager.refreshClusterNodeCountMetrics();
+
+ // Verify namespace1 still exists but namespace2 is cleaned up
+ assertNotNull(meterRegistry
+ .find(METRIC_CLUSTER_NODE_COUNT)
+ .tag(TAG_NAMESPACE, namespace1)
+ .meter());
+ // Note: MultiGauge with overwrite=true will remove stale entries
+ }
+
+ @Test
+ void testNullSupplierHandling() {
+ // Don't set any suppliers
+ // Manually trigger refresh - should not throw exception
+ metricsManager.refreshClusterNodeCountMetrics();
+ metricsManager.refreshWatcherCountMetrics();
+
+ // No exception means test passed
+ assertTrue(true);
+ }
+
+ @Test
+ void testEmptyDataHandling() {
+ // Set empty suppliers
+ metricsManager.setNamespaceClusterDataSupplier(ConcurrentHashMap::new);
+ metricsManager.setWatchersSupplier(ConcurrentHashMap::new);
+
+ // Manually trigger metrics refresh
+ metricsManager.refreshClusterNodeCountMetrics();
+ metricsManager.refreshWatcherCountMetrics();
+
+ // Verify no exception and no metrics registered
+ assertEquals(0,
meterRegistry.find(METRIC_CLUSTER_NODE_COUNT).meters().size());
+ assertEquals(0,
meterRegistry.find(METRIC_WATCHER_COUNT).meters().size());
+ }
+}
diff --git a/namingserver/src/test/resources/application.yml
b/namingserver/src/test/resources/application.yml
index 8ffcfe613b..3a333437b8 100644
--- a/namingserver/src/test/resources/application.yml
+++ b/namingserver/src/test/resources/application.yml
@@ -29,8 +29,22 @@ heartbeat:
threshold: 5000
period: 5000
seata:
+ namingserver:
+ metrics:
+ enabled: true
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls:
/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error,/naming/v1/**
+management:
+ endpoints:
+ web:
+ exposure:
+ include: "prometheus,health,info,metrics"
+ metrics:
+ tags:
+ application: ${spring.application.name}
+ distribution:
+ percentiles:
+ http.server.requests: 0.5, 0.99, 0.999
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]