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 0ae28e02c fix(metrics): normalize range query steps before transport
(#7279)
0ae28e02c is described below
commit 0ae28e02c612ad64d2faaf2c9faa2bc9fb01725c
Author: pilichoumao <[email protected]>
AuthorDate: Mon Oct 26 16:37:37 2026 +0800
fix(metrics): normalize range query steps before transport (#7279)
* fix(metrics): normalize range query steps before transport
---
.../AbstractPrometheusCompatibleMetricsSource.java | 2 +-
.../cluster/metrics/MetricsStepTransportTest.java | 148 +++++++++++++++++++++
2 files changed, 149 insertions(+), 1 deletion(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
index f8de3e9c2..b22502cce 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
@@ -92,7 +92,7 @@ public abstract class
AbstractPrometheusCompatibleMetricsSource implements Metri
form.add("query", query.getMetric());
form.add("start", Long.toString(query.getStart()));
form.add("end", Long.toString(query.getEnd()));
- form.add("step", query.getStep());
+ form.add("step", query.getStep().strip());
log.debug("Querying {} range: start={}, end={}, step={}",
settings.getBackendType(), query.getStart(), query.getEnd(),
query.getStep());
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricsStepTransportTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricsStepTransportTest.java
new file mode 100644
index 000000000..65a66edd6
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MetricsStepTransportTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.cluster.metrics;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.rocketmq.studio.model.request.MetricsDataSourceQueryRequest;
+import org.apache.rocketmq.studio.settings.DataSourceVO;
+import org.apache.rocketmq.studio.settings.SettingsService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.web.client.RestClient;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MetricsStepTransportTest {
+
+ private HttpServer server;
+ private MetricsService service;
+ private final AtomicReference<String> receivedStep = new
AtomicReference<>();
+ private final AtomicInteger requests = new AtomicInteger();
+
+ @BeforeEach
+ void setUp() throws IOException {
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/api/v1/query_range", exchange -> {
+ requests.incrementAndGet();
+ String body = new String(exchange.getRequestBody().readAllBytes(),
StandardCharsets.UTF_8);
+ receivedStep.set(Arrays.stream(body.split("&"))
+ .filter(field -> field.startsWith("step="))
+ .map(field -> URLDecoder.decode(field.substring(5),
StandardCharsets.UTF_8))
+ .findFirst().orElse(null));
+ byte[] response = """
+
{"status":"success","data":{"resultType":"matrix","result":[]}}
+ """.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type",
"application/json");
+ exchange.sendResponseHeaders(200, response.length);
+ exchange.getResponseBody().write(response);
+ exchange.close();
+ });
+ server.start();
+ String baseUrl = "http://127.0.0.1:" + server.getAddress().getPort();
+ PrometheusMetricsSource source = new
PrometheusMetricsSource(RestClient.builder(), new ObjectMapper(),
+ MetricsSourceSettings.builder().baseUrl(baseUrl).build()) {
+ @Override
+ protected void validateQueryHost(String url) {
+ // Allow only this test fixture to use a loopback destination.
+ }
+ };
+ MetricsSourceFactory factory = mock(MetricsSourceFactory.class);
+ when(factory.create(any())).thenReturn(source);
+ SettingsService settings = mock(SettingsService.class);
+
when(settings.getDataSource("metrics")).thenReturn(DataSourceVO.builder()
+
.key("metrics").name("Metrics").type("prometheus").url(baseUrl).auth("none").build());
+ service = new MetricsService(source, mock(MetricProfileService.class),
factory, settings);
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("acceptedSteps")
+ void acceptedStepReachesHttpTransportWithoutPaddingTest(boolean
configuredSource, String input, String expected) {
+ MetricQueryDTO query = query(input);
+
+ MetricDataVO result = execute(configuredSource, query);
+
+ assertThat(receivedStep.get()).isEqualTo(expected);
+ assertThat(requests.get()).isEqualTo(1);
+ assertThat(result.getResultType()).isEqualTo("matrix");
+ assertThat(query.getStep()).isEqualTo(input);
+ }
+
+ @ParameterizedTest
+ @MethodSource("rejectedSteps")
+ void invalidOrExcessiveStepDoesNotReachHttpTransportTest(boolean
configuredSource, String step) {
+ assertThatThrownBy(() -> execute(configuredSource, query(step)))
+ .isInstanceOf(PrometheusException.class)
+ .satisfies(exception -> assertThat(((PrometheusException)
exception).getStatusCode()).isEqualTo(400));
+
+ assertThat(requests.get()).isZero();
+ assertThat(receivedStep.get()).isNull();
+ }
+
+ private MetricDataVO execute(boolean configuredSource, MetricQueryDTO
query) {
+ if (!configuredSource) {
+ return service.query(query);
+ }
+ MetricsDataSourceQueryRequest request = new
MetricsDataSourceQueryRequest();
+ request.setQuery(query);
+ return service.queryByDataSource("metrics", request);
+ }
+
+ private MetricQueryDTO query(String step) {
+ return
MetricQueryDTO.builder().metric("up").start(1_000_000L).end(1_000_300L).step(step).build();
+ }
+
+ private static Stream<Arguments> acceptedSteps() {
+ return Stream.of(false, true).flatMap(configuredSource -> Stream.of(
+ Arguments.of(configuredSource, "30s", "30s"),
+ Arguments.of(configuredSource, "30", "30"),
+ Arguments.of(configuredSource, "0.5", "0.5"),
+ Arguments.of(configuredSource, "1m30s", "1m30s"),
+ Arguments.of(configuredSource, " 30s ", "30s"),
+ Arguments.of(configuredSource, " 30 ", "30"),
+ Arguments.of(configuredSource, "\t0.5\n", "0.5"),
+ Arguments.of(configuredSource, "\u20031m30s\u2003", "1m30s")));
+ }
+
+ private static Stream<Arguments> rejectedSteps() {
+ return Stream.of(false, true).flatMap(configuredSource -> Stream.of("
\t ", " 30 s ", "0", "0.001")
+ .map(step -> Arguments.of(configuredSource, step)));
+ }
+}