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 15167dc5d fix(studio): normalize the audit search term and require an
alert metric (#4682)
15167dc5d is described below
commit 15167dc5da4538a46ca1e207adc301dabda71ddb
Author: btlqql <[email protected]>
AuthorDate: Mon Sep 21 20:17:08 2026 +0800
fix(studio): normalize the audit search term and require an alert metric
(#4682)
Two service-input validation fixes from the same author, consolidated into
one change.
1. `AuditService` passed the raw `search` term to the repository, so a term
of only spaces became a LIKE pattern that matched nothing while the UI showed
an unfiltered-looking empty result. A new `normalizeSearch` trims the term and
maps blank to null across the list, summary and export entry points — the
behaviour `AlertService` and `AuthService` already had.
2. `NativeAlertRuleTestService.test` dereferenced the metric of a rule that
has none and answered 500; it now rejects the run with `BusinessException(400,
"metric is required")`.
Consolidates #4682 and #4711 (same author, both service-layer input
validation).
---
.../ops/alert/NativeAlertRuleTestService.java | 6 +++
.../rocketmq/studio/ops/audit/AuditService.java | 16 +++++++-
.../ops/alert/NativeAlertRuleTestServiceTest.java | 20 +++++++++
.../studio/ops/audit/AuditServiceTest.java | 47 ++++++++++++++++++++++
4 files changed, 87 insertions(+), 2 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestService.java
index 8c3095a41..dbe556b78 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestService.java
@@ -25,6 +25,7 @@ import
org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.instance.InstanceRepository;
import org.apache.rocketmq.studio.instance.InstanceVO;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +42,11 @@ public class NativeAlertRuleTestService {
public AlertRuleTestResultVO test(AlertRuleVO rule) {
normalizeRule(rule);
+ if (!StringUtils.hasText(rule.getMetric())) {
+ // A test run without a metric used to reach the sample filter
below and fail with a
+ // NullPointerException, which the request layer reports as an
unhandled server error.
+ throw new BusinessException(400, "metric is required");
+ }
NativeAlertRulePolicy.validate(rule);
InstanceVO instance =
instanceRepository.findByIdentifier(rule.getInstanceId())
.orElseThrow(() -> new BusinessException(404, "Instance not
found: " + rule.getInstanceId()));
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
index a514d4fe8..563bb98fa 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
@@ -23,6 +23,7 @@ import org.apache.rocketmq.studio.common.util.CsvUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -67,7 +68,8 @@ public class AuditService {
public AuditSummaryVO summarize(String search, String operationType,
String resourceType,
String clusterId, String startDate, String
endDate, String result) {
DateRange range = parseDateRange(startDate, endDate);
- return auditRepository.summarize(search, operationType, resourceType,
clusterId,
+ String normalizedSearch = normalizeSearch(search);
+ return auditRepository.summarize(normalizedSearch, operationType,
resourceType, clusterId,
range.start(), range.end(), result);
}
@@ -165,7 +167,8 @@ public class AuditService {
String startDate, String
endDate,
String result, int page, int
pageSize) {
DateRange range = parseDateRange(startDate, endDate);
- return auditRepository.findPage(search, operationType, resourceType,
target, clusterId,
+ String normalizedSearch = normalizeSearch(search);
+ return auditRepository.findPage(normalizedSearch, operationType,
resourceType, target, clusterId,
clusterIdMissing,
range.start(), range.end(), result, page, pageSize);
}
@@ -191,6 +194,15 @@ public class AuditService {
}
}
+ /**
+ * Trims the free-text search term so a term pasted with surrounding
whitespace still matches, and
+ * treats a whitespace-only term as no filter at all. Normalized once on
the way into the repository
+ * so the paged list, the summary aggregates and the CSV export all filter
on the same term.
+ */
+ private static String normalizeSearch(String search) {
+ return StringUtils.hasText(search) ? search.trim() : null;
+ }
+
private record DateRange(LocalDateTime start, LocalDateTime end) {
}
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestServiceTest.java
index 49a1f4559..12cf45bfc 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertRuleTestServiceTest.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.ops.alert;
import org.apache.rocketmq.studio.cluster.metrics.BusinessMetricsCollector;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.cluster.metrics.MetricAvailability;
import org.apache.rocketmq.studio.cluster.metrics.MetricSample;
import org.apache.rocketmq.studio.instance.InstanceRepository;
@@ -29,6 +30,7 @@ import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -123,6 +125,24 @@ class NativeAlertRuleTestServiceTest {
.satisfies(sample ->
assertThat(sample.currentValue()).isEqualTo(20));
}
+ @Test
+ void
rejectsARuleWithoutMetricInsteadOfFailingWithANullPointerExceptionTest() {
+ InstanceRepository instances = mock(InstanceRepository.class);
+ BusinessMetricsCollector collector =
mock(BusinessMetricsCollector.class);
+ InstanceVO instance = InstanceVO.builder().name("local").build();
+
when(instances.findByIdentifier("local")).thenReturn(Optional.of(instance));
+ when(collector.supports(instance)).thenReturn(true);
+ when(collector.collect(instance)).thenReturn(List.of(sample("orders",
20)));
+ AlertRuleVO rule =
AlertRuleVO.builder().domain(AlertDomain.BUSINESS).instanceId("local")
+ .operator(">").threshold(10).build();
+
+ assertThatThrownBy(() -> new NativeAlertRuleTestService(instances,
List.of(), List.of(collector),
+ new AlertRuleEvaluator()).test(rule))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("metric is required")
+ .satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(400));
+ }
+
private static MetricSample sample(String group, double value) {
return sample("consumer.lag.total", group, value);
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
index cfbeaaa59..02ac20a9f 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
@@ -34,6 +34,8 @@ import java.util.TimeZone;
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.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
@@ -265,4 +267,49 @@ class AuditServiceTest {
assertThat(deleted).isEqualTo(500);
verify(auditRepository).deleteBefore(any(LocalDateTime.class),
eq(500), eq(20));
}
+
+ @Test
+ void queryLogsShouldTrimSearchTermBeforeDelegating() {
+ auditService.queryLogs(1, 10, " ops ", null, null, null, null, false,
+ null, null, null);
+
+ ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
+ verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(),
+ isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(10));
+ assertThat(search.getValue()).isEqualTo("ops");
+ }
+
+ @Test
+ void queryLogsShouldTreatWhitespaceOnlySearchAsAbsent() {
+ auditService.queryLogs(1, 10, " ", null, null, null, null, false,
+ null, null, null);
+
+ ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
+ verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(),
+ isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(10));
+ assertThat(search.getValue()).isNull();
+ }
+
+ @Test
+ void exportLogsShouldTrimSearchTermBeforeDelegating() {
+ when(auditRepository.findPage(any(), any(), any(), any(), any(),
anyBoolean(),
+ any(), any(), any(), anyInt(),
anyInt())).thenReturn(PageResult.empty(1, 10));
+
+ auditService.exportLogs(" 50% off ", null, null, null, null, false,
null, null, null);
+
+ ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
+ verify(auditRepository).findPage(search.capture(), any(), any(),
any(), any(), anyBoolean(),
+ any(), any(), any(), anyInt(), anyInt());
+ assertThat(search.getValue()).isEqualTo("50% off");
+ }
+
+ @Test
+ void summarizeShouldTrimSearchTermBeforeDelegating() {
+ auditService.summarize(" ops ", null, null, null, null, null, null);
+
+ ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
+ verify(auditRepository).summarize(search.capture(), isNull(),
isNull(), isNull(),
+ isNull(), isNull(), isNull());
+ assertThat(search.getValue()).isEqualTo("ops");
+ }
}