diff --git a/client/pom.xml b/client/pom.xml index 6884936967d..24f25cd2cbf 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -106,6 +106,7 @@ io.prometheus simpleclient + true diff --git a/client/src/main/java/com/alibaba/nacos/client/config/http/MetricsHttpAgent.java b/client/src/main/java/com/alibaba/nacos/client/config/http/MetricsHttpAgent.java index 6fdf2467a2b..bfb58ac69cd 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/http/MetricsHttpAgent.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/http/MetricsHttpAgent.java @@ -19,7 +19,6 @@ import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.client.monitor.MetricsMonitor; import com.alibaba.nacos.common.http.HttpRestResult; -import io.prometheus.client.Histogram; import java.util.Date; import java.util.Map; @@ -55,14 +54,15 @@ public HttpRestResult httpGet(String path, Map headers, Map paramValues, String encode, long readTimeoutMs) throws Exception { Date start = new Date(); - Histogram.Child histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, DEFAULT_CODE); + MetricsMonitor.MetricsTimer timer = MetricsMonitor.getConfigRequestMonitor( + GET, path, DEFAULT_CODE); HttpRestResult result; try { result = httpAgent.httpGet(path, headers, paramValues, encode, readTimeoutMs); - histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, + timer = MetricsMonitor.getConfigRequestMonitor(GET, path, String.valueOf(result.getCode())); } finally { - histogram.observe(System.currentTimeMillis() - start.getTime()); + timer.observe(System.currentTimeMillis() - start.getTime()); } return result; @@ -73,14 +73,15 @@ public HttpRestResult httpPost(String path, Map headers, Map paramValues, String encode, long readTimeoutMs) throws Exception { Date start = new Date(); - Histogram.Child histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, DEFAULT_CODE); + MetricsMonitor.MetricsTimer timer = MetricsMonitor.getConfigRequestMonitor( + POST, path, DEFAULT_CODE); HttpRestResult result; try { result = httpAgent.httpPost(path, headers, paramValues, encode, readTimeoutMs); - histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, + timer = MetricsMonitor.getConfigRequestMonitor(POST, path, String.valueOf(result.getCode())); } finally { - histogram.observe(System.currentTimeMillis() - start.getTime()); + timer.observe(System.currentTimeMillis() - start.getTime()); } return result; @@ -91,14 +92,15 @@ public HttpRestResult httpDelete(String path, Map header Map paramValues, String encode, long readTimeoutMs) throws Exception { Date start = new Date(); - Histogram.Child histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, DEFAULT_CODE); + MetricsMonitor.MetricsTimer timer = MetricsMonitor.getConfigRequestMonitor( + DELETE, path, DEFAULT_CODE); HttpRestResult result; try { result = httpAgent.httpDelete(path, headers, paramValues, encode, readTimeoutMs); - histogram = MetricsMonitor.getConfigRequestMonitor(GET, path, + timer = MetricsMonitor.getConfigRequestMonitor(DELETE, path, String.valueOf(result.getCode())); } finally { - histogram.observe(System.currentTimeMillis() - start.getTime()); + timer.observe(System.currentTimeMillis() - start.getTime()); } return result; diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java index b23df5ec78b..6f7d65a095b 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ClientWorker.java @@ -336,7 +336,7 @@ void removeCache(String dataId, String group, String tenant) { if (enableClientMetrics) { try { - MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size()); + MetricsMonitor.recordListenConfigCount(cacheMap.get().size()); } catch (Throwable t) { LOGGER.error("Failed to update metrics for listen config count", t); } @@ -421,7 +421,7 @@ public CacheData addCacheDataIfAbsent(String dataId, String group) { if (enableClientMetrics) { try { - MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size()); + MetricsMonitor.recordListenConfigCount(cacheMap.get().size()); } catch (Throwable t) { LOGGER.error("Failed to update metrics for listen config count", t); } @@ -477,7 +477,7 @@ public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant if (enableClientMetrics) { try { - MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size()); + MetricsMonitor.recordListenConfigCount(cacheMap.get().size()); } catch (Throwable t) { LOGGER.error("Failed to update metrics for listen config count", t); } diff --git a/client/src/main/java/com/alibaba/nacos/client/monitor/MetricsMonitor.java b/client/src/main/java/com/alibaba/nacos/client/monitor/MetricsMonitor.java index e68c0bba4dc..4548a74b2c2 100644 --- a/client/src/main/java/com/alibaba/nacos/client/monitor/MetricsMonitor.java +++ b/client/src/main/java/com/alibaba/nacos/client/monitor/MetricsMonitor.java @@ -16,51 +16,142 @@ package com.alibaba.nacos.client.monitor; -import io.prometheus.client.Counter; -import io.prometheus.client.Gauge; -import io.prometheus.client.Histogram; - /** * Metrics Monitor. * + *

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 mockedMetricsMonitor = Mockito.mockStatic(MetricsMonitor.class)) { - mockedMetricsMonitor.when(MetricsMonitor::getServiceInfoMapSizeMonitor) - .thenReturn(mockGaugeChild); holder.processServiceInfo(info); - verify(mockGaugeChild, times(1)).set(1); + mockedMetricsMonitor.verify(() -> MetricsMonitor.recordServiceInfoMapSize(1), times(1)); } } @@ -141,7 +135,7 @@ void testProcessServiceInfoEnableClientMetricsFalse() { Mockito.mockStatic(MetricsMonitor.class)) { holder.processServiceInfo(info); - mockedMetricsMonitor.verify(MetricsMonitor::getServiceInfoMapSizeMonitor, never()); + mockedMetricsMonitor.verify(() -> MetricsMonitor.recordServiceInfoMapSize(org.mockito.ArgumentMatchers.anyDouble()), never()); } } @@ -157,15 +151,12 @@ void testProcessServiceInfoEnableClientMetricsNotSet() { info.setHosts(hosts); - Gauge.Child mockGaugeChild = mock(Gauge.Child.class); try (MockedStatic mockedMetricsMonitor = Mockito.mockStatic(MetricsMonitor.class)) { - mockedMetricsMonitor.when(MetricsMonitor::getServiceInfoMapSizeMonitor) - .thenReturn(mockGaugeChild); holder.processServiceInfo(info); - verify(mockGaugeChild, times(1)).set(1); + mockedMetricsMonitor.verify(() -> MetricsMonitor.recordServiceInfoMapSize(1), times(1)); } } @@ -180,14 +171,11 @@ void testProcessServiceInfoSetThrowsException() { hosts.add(instance2); info.setHosts(hosts); - Gauge.Child mockGaugeChild = mock(Gauge.Child.class); RuntimeException exception = new RuntimeException("Mocked exception"); try (MockedStatic mockedMetricsMonitor = Mockito.mockStatic(MetricsMonitor.class)) { - mockedMetricsMonitor.when(MetricsMonitor::getServiceInfoMapSizeMonitor) - .thenReturn(mockGaugeChild); - doThrow(exception).when(mockGaugeChild).set(anyInt()); + doThrow(exception).when(() -> MetricsMonitor.recordServiceInfoMapSize(org.mockito.ArgumentMatchers.anyDouble())); ServiceInfo actual2 = holder.processServiceInfo(info); @@ -353,4 +341,4 @@ private FailoverReactor injectMockFailoverReactor() field.set(holder, mock); return mock; } -} +} \ No newline at end of file