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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 6c24d2ed3 fix(server): declare the cache manager so the data source 
API works (#4213)
6c24d2ed3 is described below

commit 6c24d2ed32b15fe7222dc7d0fb74ea837bbdb70a
Author: lizhimins <[email protected]>
AuthorDate: Thu Sep 10 17:21:34 2026 +0800

    fix(server): declare the cache manager so the data source API works (#4213)
    
    `@EnableCaching` on the application class and `@Cacheable("data-sources")` 
on
    `SettingsService.listDataSources` arrived together, with no `CacheManager` 
declared — the manager
    was left to Spring Boot's cache auto-configuration. Whatever that resolved 
to in this application
    could not serve the `data-sources` cache, so the caching interceptor threw
    `IllegalArgumentException: Cannot find cache named 'data-sources'` on every 
call and
    `GET /api/settings/datasources` answered 500. That broke the settings 
data-source tab and put an
    `Internal Server Error` alert on the dashboard, whose metrics panel loads 
the data source list.
    
    The manager is now declared explicitly, which removes the dependency on 
what the
    auto-configuration happens to pick. It carries no fixed cache-name list, so
    `ConcurrentMapCacheManager` creates caches on first use and a future 
`@Cacheable` with a new name
    cannot fail the same way.
    
    `SettingsServiceCachingTest` supplies its own manager, which is why it 
passed throughout while
    production failed on every request. `CacheConfigTest` mirrors the 
production wiring instead, and
    fails without this configuration.
    
    The rest of the change gives the metrics dashboard something to query, 
which the local stack did
    not previously have:
    
    - Both brokers export Prometheus metrics on 5557. `metricsPromExporterHost` 
must be set to
      0.0.0.0 explicitly; it defaults to `brokerIP1`, which leaves the endpoint 
unreachable from
      outside the container.
    - A `prometheus` service scrapes them by container name, so a container 
rebuild changing the IP
      does not need a config change. Port 9090 binds to the loopback interface 
only.
    - `.env.example` points `STUDIO_METRICS_PROMETHEUS_BASE_URL` at that 
container. It cannot be a
      loopback address: `UrlHostGuard` rejects loopback for metrics data 
sources, and the query then
      fails with "base URL is invalid".
---
 deploy/.env.example                                |  6 +-
 deploy/rocketmq/conf/broker-0.conf                 |  5 ++
 deploy/rocketmq/conf/broker-1.conf                 |  5 ++
 deploy/rocketmq/conf/prometheus.yml                | 25 ++++++++
 deploy/rocketmq/docker-compose.yml                 | 18 ++++++
 .../rocketmq/studio/common/config/CacheConfig.java | 47 +++++++++++++++
 .../studio/common/config/CacheConfigTest.java      | 67 ++++++++++++++++++++++
 7 files changed, 172 insertions(+), 1 deletion(-)

diff --git a/deploy/.env.example b/deploy/.env.example
index 3d7f82d1d..e7085087d 100644
--- a/deploy/.env.example
+++ b/deploy/.env.example
@@ -22,7 +22,11 @@ STUDIO_AUTH_ADMIN_USERNAME=admin
 STUDIO_AUTH_ADMIN_PASSWORD=change-me
 
 # Optional Prometheus-compatible data source for the built-in metrics proxy.
-STUDIO_METRICS_PROMETHEUS_BASE_URL=
+# `deploy/rocketmq/docker-compose.yml` ships a `rocketmq-prometheus` container 
on the
+# shared `rocketmq_net` network that scrapes both brokers, so the container 
name below
+# works as-is once that stack is up. Do not use localhost/127.0.0.1: 
UrlHostGuard blocks
+# loopback for metrics data sources and the query fails with "base URL is 
invalid".
+STUDIO_METRICS_PROMETHEUS_BASE_URL=http://rocketmq-prometheus:9090
 STUDIO_METRICS_PROMETHEUS_USERNAME=
 STUDIO_METRICS_PROMETHEUS_PASSWORD=
 STUDIO_METRICS_PROMETHEUS_BEARER_TOKEN=
diff --git a/deploy/rocketmq/conf/broker-0.conf 
b/deploy/rocketmq/conf/broker-0.conf
index 218ae6b5d..87ef67fbc 100644
--- a/deploy/rocketmq/conf/broker-0.conf
+++ b/deploy/rocketmq/conf/broker-0.conf
@@ -13,3 +13,8 @@ autoCreateSubscriptionGroup=true
 # 消息轨迹:开启并自动创建轨迹 topic RMQ_SYS_TRACE_TOPIC
 traceOn=true
 traceTopicEnable=true
+# Prometheus 指标导出(Studio 监控面板数据源)
+# Host 必须显式设为 0.0.0.0:默认取 brokerIP1,会导致容器外抓取不到 5557
+metricsExporterType=PROM
+metricsPromExporterPort=5557
+metricsPromExporterHost=0.0.0.0
diff --git a/deploy/rocketmq/conf/broker-1.conf 
b/deploy/rocketmq/conf/broker-1.conf
index b26d6b72a..293572fa1 100644
--- a/deploy/rocketmq/conf/broker-1.conf
+++ b/deploy/rocketmq/conf/broker-1.conf
@@ -15,3 +15,8 @@ autoCreateSubscriptionGroup=true
 # 消息轨迹:开启并自动创建轨迹 topic RMQ_SYS_TRACE_TOPIC
 traceOn=true
 traceTopicEnable=true
+# Prometheus 指标导出(Studio 监控面板数据源)
+# Host 必须显式设为 0.0.0.0:默认取 brokerIP1,会导致容器外抓取不到 5557
+metricsExporterType=PROM
+metricsPromExporterPort=5557
+metricsPromExporterHost=0.0.0.0
diff --git a/deploy/rocketmq/conf/prometheus.yml 
b/deploy/rocketmq/conf/prometheus.yml
new file mode 100644
index 000000000..b4b87372a
--- /dev/null
+++ b/deploy/rocketmq/conf/prometheus.yml
@@ -0,0 +1,25 @@
+# Prometheus 抓取配置 —— RocketMQ Studio 监控面板数据源
+#
+# broker 侧需在 broker-{0,1}.conf 打开 PROM 导出(metricsExporterType=PROM,
+# metricsPromExporterPort=5557,metricsPromExporterHost=0.0.0.0)。
+#
+# 按容器名抓取而非 IP:Prometheus 与 broker 同在 rocketmq_net,容器重建导致 IP
+# 漂移时无需改配置。
+
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+scrape_configs:
+  - job_name: rocketmq-broker
+    static_configs:
+      - targets:
+          - rmq-broker-0:5557
+          - rmq-broker-1:5557
+        labels:
+          cluster: rocketmq-studio
+
+  - job_name: prometheus
+    static_configs:
+      - targets:
+          - localhost:9090
diff --git a/deploy/rocketmq/docker-compose.yml 
b/deploy/rocketmq/docker-compose.yml
index 8b5e3c32b..3aa48e967 100644
--- a/deploy/rocketmq/docker-compose.yml
+++ b/deploy/rocketmq/docker-compose.yml
@@ -108,6 +108,23 @@ services:
       - broker-1
     restart: on-failure
 
+  prometheus:
+    image: prom/prometheus:v2.53.0
+    container_name: rocketmq-prometheus
+    command:
+      - --config.file=/etc/prometheus/prometheus.yml
+      - --storage.tsdb.retention.time=15d
+    volumes:
+      - ./conf/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+      - prometheus-data:/prometheus
+    # 仅绑定回环,避免公网暴露;Studio 走容器名 rocketmq-prometheus:9090 访问
+    ports:
+      - "127.0.0.1:9090:9090"
+    depends_on:
+      - broker-0
+      - broker-1
+    restart: unless-stopped
+
 networks:
   default:
     name: rocketmq_net
@@ -118,3 +135,4 @@ volumes:
   broker-0-logs:
   broker-1-store:
   broker-1-logs:
+  prometheus-data:
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
new file mode 100644
index 000000000..3fa534a07
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
@@ -0,0 +1,47 @@
+/*
+ * 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.rocketmq.studio.common.config;
+
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Declares the {@link CacheManager} backing the {@code @Cacheable} methods 
enabled by
+ * {@code @EnableCaching} on the application class.
+ * <p>
+ * {@code @EnableCaching} on its own leaves the manager to Spring Boot's cache
+ * auto-configuration. In this application that did not yield a manager able 
to serve the
+ * {@code data-sources} cache, so every call to {@code 
SettingsService.listDataSources} failed with
+ * {@code IllegalArgumentException: Cannot find cache named 'data-sources'} 
and surfaced as a 500 on
+ * {@code GET /api/settings/datasources} — breaking both the settings 
data-source tab and the
+ * metrics data-source picker on the dashboard. Declaring the manager here 
removes the dependency
+ * on whatever the auto-configuration happens to resolve to.
+ * <p>
+ * The manager is deliberately left without a fixed cache-name list: {@code 
ConcurrentMapCacheManager}
+ * then creates caches on first use, so adding a {@code @Cacheable} with a new 
name cannot
+ * reintroduce the same failure. Caches in use today: {@code data-sources}.
+ */
+@Configuration
+public class CacheConfig {
+
+    @Bean
+    public CacheManager cacheManager() {
+        return new ConcurrentMapCacheManager();
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/config/CacheConfigTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/config/CacheConfigTest.java
new file mode 100644
index 000000000..73902b129
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/config/CacheConfigTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.rocketmq.studio.common.config;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.CacheInterceptor;
+import 
org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Guards the wiring that broke {@code GET /api/settings/datasources} with a 
500: caching was
+ * enabled but the resolved {@link CacheManager} could not hand out the {@code 
data-sources} cache,
+ * so the {@code @Cacheable} interceptor threw {@code Cannot find cache named 
'data-sources'}.
+ * <p>
+ * The pre-existing {@code SettingsServiceCachingTest} supplies its own 
manager, which is why it
+ * kept passing while production failed on every request.
+ */
+class CacheConfigTest {
+
+    /** Mirrors the production wiring: {@code @EnableCaching} plus {@link 
CacheConfig}. */
+    @EnableCaching
+    static class CachingContext {
+    }
+
+    @Test
+    void resolvesEveryCacheNameUsedByCacheableMethodsTest() {
+        try (AnnotationConfigApplicationContext context = new 
AnnotationConfigApplicationContext()) {
+            context.register(CachingContext.class, CacheConfig.class);
+            context.refresh();
+
+            CacheManager cacheManager = context.getBean(CacheManager.class);
+
+            // The name SettingsService.listDataSources caches under.
+            assertThat(cacheManager.getCache("data-sources")).isNotNull();
+            // Names are created on demand, so a newly added @Cacheable cannot 
break the same way.
+            
assertThat(cacheManager.getCache("any-future-cache-name")).isNotNull();
+        }
+    }
+
+    @Test
+    void enablesTheCachingInterceptorAgainstThatManagerTest() {
+        try (AnnotationConfigApplicationContext context = new 
AnnotationConfigApplicationContext()) {
+            context.register(CachingContext.class, CacheConfig.class);
+            context.refresh();
+
+            // Without an interceptor @Cacheable would silently no-op instead 
of caching.
+            
assertThat(context.getBeansOfType(CacheInterceptor.class)).isNotEmpty();
+        }
+    }
+}

Reply via email to