Prometheus dependency is optional. If prometheus client is not on the
+ * classpath, all monitoring operations become no-ops.
+ *
* @author Nacos
*/
public class MetricsMonitor {
- private static final Gauge NACOS_MONITOR_GAUGE =
- Gauge.build().name("nacos_monitor").labelNames("module", "name")
- .help("nacos_monitor").register();
+ private static final boolean PROMETHEUS_AVAILABLE;
+
+ static {
+ boolean available;
+ try {
+ Class.forName("io.prometheus.client.Counter");
+ available = true;
+ } catch (ClassNotFoundException e) {
+ available = false;
+ }
+ PROMETHEUS_AVAILABLE = available;
+ }
+
+ private static volatile Object gauge;
+ private static volatile Object histogram;
+ private static volatile Object counter;
- private static final Histogram NACOS_CLIENT_REQUEST_HISTOGRAM = Histogram.build()
- .labelNames("module", "method", "url", "code").name("nacos_client_request")
- .help("nacos_client_request")
- .register();
+ private static Object getOrInitGauge() {
+ if (gauge == null) {
+ synchronized (MetricsMonitor.class) {
+ if (gauge == null) {
+ gauge = PrometheusMetricsHelper.createGauge("nacos_monitor",
+ "nacos_monitor", "module", "name");
+ }
+ }
+ }
+ return gauge;
+ }
- private static final Counter NACOS_CLIENT_NAMING_REQUEST_FAILED_TOTAL = Counter.build()
- .name("nacos_client_naming_request_failed_total")
- .help("nacos_client_naming_request_failed_total")
- .labelNames("module", "req_class", "res_status", "res_code", "err_class").register();
+ private static Object getOrInitHistogram() {
+ if (histogram == null) {
+ synchronized (MetricsMonitor.class) {
+ if (histogram == null) {
+ histogram = PrometheusMetricsHelper.createHistogram(
+ "nacos_client_request", "nacos_client_request",
+ "module", "method", "url", "code");
+ }
+ }
+ }
+ return histogram;
+ }
+
+ private static Object getOrInitCounter() {
+ if (counter == null) {
+ synchronized (MetricsMonitor.class) {
+ if (counter == null) {
+ counter = PrometheusMetricsHelper.createCounter(
+ "nacos_client_naming_request_failed_total",
+ "nacos_client_naming_request_failed_total",
+ "module", "req_class", "res_status", "res_code",
+ "err_class");
+ }
+ }
+ }
+ return counter;
+ }
+
+ public static MetricsTimer getConfigRequestMonitor(String method, String url,
+ String code) {
+ if (!PROMETHEUS_AVAILABLE) {
+ return MetricsTimer.NOOP;
+ }
+ return PrometheusMetricsHelper.getHistogramChild(getOrInitHistogram(),
+ "config", method, url, code);
+ }
- public static Gauge.Child getServiceInfoMapSizeMonitor() {
- return NACOS_MONITOR_GAUGE.labels("naming", "serviceInfoMapSize");
+ public static MetricsTimer getNamingRequestMonitor(String method, String url,
+ String code) {
+ if (!PROMETHEUS_AVAILABLE) {
+ return MetricsTimer.NOOP;
+ }
+ return PrometheusMetricsHelper.getHistogramChild(getOrInitHistogram(),
+ "naming", method, url, code);
}
- public static Gauge.Child getListenConfigCountMonitor() {
- return NACOS_MONITOR_GAUGE.labels("config", "listenConfigCount");
+ /**
+ * Record the size of service info map.
+ *
+ * @param size the size of service info map
+ */
+ public static void recordServiceInfoMapSize(double size) {
+ if (!PROMETHEUS_AVAILABLE) {
+ return;
+ }
+ PrometheusMetricsHelper.setGaugeChild(getOrInitGauge(), size,
+ "naming", "serviceInfoMapSize");
}
- public static Histogram.Child getConfigRequestMonitor(String method, String url, String code) {
- return NACOS_CLIENT_REQUEST_HISTOGRAM.labels("config", method, url, code);
+ /**
+ * Record the count of listened configs.
+ *
+ * @param count the count of listened configs
+ */
+ public static void recordListenConfigCount(double count) {
+ if (!PROMETHEUS_AVAILABLE) {
+ return;
+ }
+ PrometheusMetricsHelper.setGaugeChild(getOrInitGauge(), count,
+ "config", "listenConfigCount");
}
- public static Histogram.Child getNamingRequestMonitor(String method, String url, String code) {
- return NACOS_CLIENT_REQUEST_HISTOGRAM.labels("naming", method, url, code);
+ /**
+ * Record a failed naming request.
+ *
+ * @param reqClass the request class name
+ * @param resStatus the response status
+ * @param resCode the response code
+ * @param errClass the error class name
+ */
+ public static void recordNamingRequestFailed(String reqClass, String resStatus,
+ String resCode, String errClass) {
+ if (!PROMETHEUS_AVAILABLE) {
+ return;
+ }
+ PrometheusMetricsHelper.incCounterChild(getOrInitCounter(),
+ "naming", reqClass, resStatus, resCode, errClass);
}
- public static Counter.Child getNamingRequestFailedMonitor(String reqClass, String resStatus,
- String resCode,
- String errClass) {
- return NACOS_CLIENT_NAMING_REQUEST_FAILED_TOTAL.labels("naming", reqClass, resStatus,
- resCode, errClass);
+ /**
+ * Timer abstraction that wraps prometheus Histogram.Child observation.
+ */
+ public interface MetricsTimer {
+
+ MetricsTimer NOOP = duration -> {
+ };
+
+ void observe(double durationMs);
}
}
diff --git a/client/src/main/java/com/alibaba/nacos/client/monitor/PrometheusMetricsHelper.java b/client/src/main/java/com/alibaba/nacos/client/monitor/PrometheusMetricsHelper.java
new file mode 100644
index 00000000000..4e921368048
--- /dev/null
+++ b/client/src/main/java/com/alibaba/nacos/client/monitor/PrometheusMetricsHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 1999-2026 Alibaba Group Holding Ltd.
+ *
+ * Licensed 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 com.alibaba.nacos.client.monitor;
+
+import io.prometheus.client.Counter;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.Histogram;
+
+/**
+ * Prometheus metrics helper. This class is only loaded when prometheus client
+ * is on the classpath. It isolates all direct prometheus API calls so that
+ * {@link MetricsMonitor} can guard against ClassNotFoundException.
+ *
+ * @author Nacos
+ */
+final class PrometheusMetricsHelper {
+
+ private PrometheusMetricsHelper() {
+ }
+
+ static Object createGauge(String name, String help, String... labelNames) {
+ return Gauge.build().name(name).labelNames(labelNames).help(help).register();
+ }
+
+ static Object createHistogram(String name, String help, String... labelNames) {
+ return Histogram.build().name(name).labelNames(labelNames).help(help).register();
+ }
+
+ static Object createCounter(String name, String help, String... labelNames) {
+ return Counter.build().name(name).labelNames(labelNames).help(help).register();
+ }
+
+ static MetricsMonitor.MetricsTimer getHistogramChild(Object histogramObj,
+ String... labelValues) {
+ Histogram histogram = (Histogram) histogramObj;
+ Histogram.Child child = histogram.labels(labelValues);
+ return child::observe;
+ }
+
+ static void setGaugeChild(Object gaugeObj, double value, String... labelValues) {
+ Gauge gauge = (Gauge) gaugeObj;
+ gauge.labels(labelValues).set(value);
+ }
+
+ static void incCounterChild(Object counterObj, String... labelValues) {
+ Counter counter = (Counter) counterObj;
+ counter.labels(labelValues).inc();
+ }
+}
diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolder.java b/client/src/main/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolder.java
index 545d9c3e624..8261f4f5ee1 100644
--- a/client/src/main/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolder.java
+++ b/client/src/main/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolder.java
@@ -147,7 +147,7 @@ public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
if (enableClientMetrics) {
try {
- MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
+ MetricsMonitor.recordServiceInfoMapSize(serviceInfoMap.size());
} catch (Throwable t) {
NAMING_LOGGER.error("Failed to update metrics for service info map size", t);
}
diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java
index 5b4cf9a10ed..c27dfe38516 100644
--- a/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java
+++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/gprc/NamingGrpcClientProxy.java
@@ -562,14 +562,14 @@ private void recordRequestFailedMetrics(Request request, Exception exception,
try {
if (Objects.isNull(response)) {
- MetricsMonitor.getNamingRequestFailedMonitor(request.getClass().getSimpleName(),
+ MetricsMonitor.recordNamingRequestFailed(request.getClass().getSimpleName(),
MONITOR_LABEL_NONE,
- MONITOR_LABEL_NONE, exception.getClass().getSimpleName()).inc();
+ MONITOR_LABEL_NONE, exception.getClass().getSimpleName());
} else {
- MetricsMonitor.getNamingRequestFailedMonitor(request.getClass().getSimpleName(),
+ MetricsMonitor.recordNamingRequestFailed(request.getClass().getSimpleName(),
String.valueOf(response.getResultCode()),
String.valueOf(response.getErrorCode()),
- MONITOR_LABEL_NONE).inc();
+ MONITOR_LABEL_NONE);
}
} catch (Throwable t) {
NAMING_LOGGER.warn("Fail to record metrics for request {}",
diff --git a/client/src/test/java/com/alibaba/nacos/client/config/impl/ClientWorkerTest.java b/client/src/test/java/com/alibaba/nacos/client/config/impl/ClientWorkerTest.java
index b842083b8ae..4a6924cbd2d 100644
--- a/client/src/test/java/com/alibaba/nacos/client/config/impl/ClientWorkerTest.java
+++ b/client/src/test/java/com/alibaba/nacos/client/config/impl/ClientWorkerTest.java
@@ -41,10 +41,7 @@
import com.alibaba.nacos.common.remote.client.RpcClient;
import com.alibaba.nacos.common.remote.client.RpcClientFactory;
import com.alibaba.nacos.common.remote.client.grpc.GrpcClientConfig;
-import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.MD5Utils;
-import com.fasterxml.jackson.databind.JsonNode;
-import io.prometheus.client.Gauge;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -456,91 +453,6 @@ void testHandleClientMetricsReqeust() throws Exception {
((ClientWorker.ConfigRpcTransportClient) clientWorker.getAgent())
.handleClientMetricsRequest(
configMetricsRequest);
- JsonNode jsonNode = JacksonUtils.toObj(metricResponse.getMetrics().get(uuid).toString());
- String metricValues = jsonNode.get("metricValues")
- .get(ClientConfigMetricRequest.MetricsKey
- .build(ClientConfigMetricRequest.MetricsKey.CACHE_DATA,
- GroupKey.getKeyTenant(dataId, group, tenant))
- .toString())
- .textValue();
-
- int colonIndex = metricValues.lastIndexOf(":");
- assertEquals(content, metricValues.substring(0, colonIndex));
- assertEquals(md5, metricValues.substring(colonIndex + 1, metricValues.length()));
-
- }
-
- @Test
- void testGeConfigConfigNotFound() throws NacosException {
-
- Properties prop = new Properties();
- ConfigServerListManager agent = Mockito.mock(ConfigServerListManager.class);
- final NacosClientProperties nacosClientProperties =
- NacosClientProperties.PROTOTYPE.derive(prop);
- ClientWorker clientWorker = new ClientWorker(null, agent, nacosClientProperties);
-
- String dataId = "a";
- String group = "b";
- String tenant = "c";
- ConfigQueryResponse configQueryResponse = new ConfigQueryResponse();
- configQueryResponse.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config not found");
- Mockito.when(rpcClient.request(any(ConfigQueryRequest.class), anyLong()))
- .thenReturn(configQueryResponse);
-
- ConfigResponse configResponse =
- clientWorker.getServerConfig(dataId, group, tenant, 100, true);
- assertNull(configResponse.getContent());
- localConfigInfoProcessorMockedStatic.verify(
- () -> LocalConfigInfoProcessor.saveSnapshot(eq(clientWorker.getAgentName()),
- eq(dataId), eq(group),
- eq(tenant), eq(null)),
- times(1));
-
- }
-
- @Test
- void testGeConfigConfigConflict() throws NacosException {
-
- Properties prop = new Properties();
- ConfigServerListManager agent = Mockito.mock(ConfigServerListManager.class);
- final NacosClientProperties nacosClientProperties =
- NacosClientProperties.PROTOTYPE.derive(prop);
- ClientWorker clientWorker = new ClientWorker(null, agent, nacosClientProperties);
-
- String dataId = "a";
- String group = "b";
- String tenant = "c";
- ConfigQueryResponse configQueryResponse = new ConfigQueryResponse();
- configQueryResponse.setErrorInfo(ConfigQueryResponse.CONFIG_QUERY_CONFLICT,
- "config is being modified");
- Mockito.when(rpcClient.request(any(ConfigQueryRequest.class), anyLong()))
- .thenReturn(configQueryResponse);
-
- try {
- clientWorker.getServerConfig(dataId, group, tenant, 100, true);
- fail();
- } catch (NacosException e) {
- assertEquals(NacosException.CONFLICT, e.getErrCode());
- }
- }
-
- @Test
- void testShutdown() throws NacosException, NoSuchFieldException, IllegalAccessException {
- Properties prop = new Properties();
- ConfigFilterChainManager filter = new ConfigFilterChainManager(new Properties());
- ConfigServerListManager agent = Mockito.mock(ConfigServerListManager.class);
-
- final NacosClientProperties nacosClientProperties =
- NacosClientProperties.PROTOTYPE.derive(prop);
- ClientWorker clientWorker = new ClientWorker(filter, agent, nacosClientProperties);
- clientWorker.shutdown();
- Field agent1 = ClientWorker.class.getDeclaredField("agent");
- agent1.setAccessible(true);
- ConfigTransportClient o = (ConfigTransportClient) agent1.get(clientWorker);
- assertTrue(o.getExecutor().isShutdown());
- agent1.setAccessible(false);
-
- assertNull(clientWorker.getAgentName());
}
@Test
diff --git a/client/src/test/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolderTest.java b/client/src/test/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolderTest.java
index 3ebbc9d13d8..bdc3db99aa0 100644
--- a/client/src/test/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolderTest.java
+++ b/client/src/test/java/com/alibaba/nacos/client/naming/cache/ServiceInfoHolderTest.java
@@ -25,7 +25,6 @@
import com.alibaba.nacos.client.env.NacosClientProperties;
import com.alibaba.nacos.client.monitor.MetricsMonitor;
import com.alibaba.nacos.client.naming.backups.FailoverReactor;
-import io.prometheus.client.Gauge;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,12 +42,10 @@
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class ServiceInfoHolderTest {
@@ -114,15 +111,12 @@ void testProcessServiceInfoEnableClientMetricsTrue() {
hosts.add(instance2);
info.setHosts(hosts);
- Gauge.Child mockGaugeChild = mock(Gauge.Child.class);
try (MockedStatic