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 0d83a567d fix(alert): preserve silence label conditions during
normalization (#6430)
0d83a567d is described below
commit 0d83a567d5f8299be3a6471929b23079f4757217
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 18:12:32 2026 +0800
fix(alert): preserve silence label conditions during normalization (#6430)
---
.../studio/ops/alert/AlertSilenceService.java | 3 ++
.../studio/ops/alert/AlertSilenceServiceTest.java | 26 ++++++++++++++
.../pages/ops/__tests__/SystemAlertsPage.test.tsx | 42 ++++++++++++++++++++++
web/src/pages/ops/systemAlerts.tsx | 10 +++---
4 files changed, 77 insertions(+), 4 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
index 726eccb9e..c09036ccf 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
@@ -198,6 +198,9 @@ public class AlertSilenceService {
|| normalizedValue.length() > 512) {
throw new BusinessException(400, "Silence labels must have
non-empty bounded keys and values");
}
+ if (normalized.containsKey(normalizedKey)) {
+ throw new BusinessException(400, "Silence label keys must be
unique after trimming");
+ }
normalized.put(normalizedKey, normalizedValue);
});
return Map.copyOf(normalized);
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
index 05c2676fb..c0b35af86 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
@@ -35,6 +35,8 @@ 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.when;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verifyNoInteractions;
@ExtendWith(MockitoExtension.class)
class AlertSilenceServiceTest {
@@ -262,6 +264,30 @@ class AlertSilenceServiceTest {
.hasMessage("Weekly silence windows must not exceed 7 days");
}
+ @Test
+ void rejectsLabelKeysThatCollideAfterTrimmingBeforePersistenceTest() {
+ AlertSilenceService service = new AlertSilenceService(repository,
operationAuditService);
+ CreateAlertSilenceDTO request =
recurringRequest(AlertSilenceRecurrence.DAILY);
+ request.setLabels(Map.of("brokerName", "broker-a", " brokerName ",
"broker-b"));
+ lenient().when(repository.save(any())).thenAnswer(invocation ->
invocation.getArgument(0));
+
+ assertThatThrownBy(() -> service.create(request))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Silence label keys must be unique after
trimming");
+ verifyNoInteractions(repository, operationAuditService);
+ }
+
+ @Test
+ void preservesDistinctLabelsAndNormalizesTheirWhitespaceTest() {
+ AlertSilenceService service = new AlertSilenceService(repository,
operationAuditService);
+ CreateAlertSilenceDTO request =
recurringRequest(AlertSilenceRecurrence.DAILY);
+ request.setLabels(Map.of(" brokerName ", " broker-a ", "__proto__",
"rack=one"));
+ when(repository.save(any())).thenAnswer(invocation ->
invocation.getArgument(0));
+
+ assertThat(service.create(request).getLabels())
+ .containsExactlyInAnyOrderEntriesOf(Map.of("brokerName",
"broker-a", "__proto__", "rack=one"));
+ }
+
private static CreateAlertSilenceDTO
recurringRequest(AlertSilenceRecurrence recurrence) {
CreateAlertSilenceDTO request = new CreateAlertSilenceDTO();
request.setStartsAt(java.time.OffsetDateTime.parse("2026-09-01T10:00:00Z"));
diff --git a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
index 3d5397d00..a75028db2 100644
--- a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
@@ -744,6 +744,48 @@ describe('SystemAlertsPage', () => {
expect(payload.labels).toBeUndefined();
});
+ it.each(['brokerName=a, brokerName=b', ' =broker-a'])(
+ 'rejectsSilenceLabelsWithoutDroppingConditionsTest: %s',
+ async (labels) => {
+ const user = userEvent.setup();
+ renderPage();
+ await user.click(await screen.findByRole('button', { name: '维护窗口' }));
+ fireEvent.change(screen.getByLabelText('标签范围'), { target: { value:
labels } });
+ fireEvent.change(screen.getByLabelText('开始时间'), {
+ target: { value: '2026-08-11T01:00' },
+ });
+ fireEvent.change(screen.getByLabelText('结束时间'), {
+ target: { value: '2026-08-11T02:00' },
+ });
+ await user.click(screen.getByRole('button', { name: /创\s*建/ }));
+ expect(await screen.findByText('标签格式应为
key=value,并以逗号分隔')).toBeInTheDocument();
+ expect(createAlertSilence).not.toHaveBeenCalled();
+ },
+ );
+
+ it('preservesPrototypeNamedSilenceLabelsInTheRequestTest', async () => {
+ vi.mocked(createAlertSilence).mockResolvedValue({
+ id: 10,
+ startsAt: '2026-08-11T01:00',
+ endsAt: '2026-08-11T02:00',
+ createdBy: 'admin',
+ });
+ const user = userEvent.setup();
+ renderPage();
+ await user.click(await screen.findByRole('button', { name: '维护窗口' }));
+ fireEvent.change(screen.getByLabelText('标签范围'), {
+ target: { value: ' __proto__
=broker-a,constructor=rack=one,toString=orders' },
+ });
+ fireEvent.change(screen.getByLabelText('开始时间'), { target: { value:
'2026-08-11T01:00' } });
+ fireEvent.change(screen.getByLabelText('结束时间'), { target: { value:
'2026-08-11T02:00' } });
+ await user.click(screen.getByRole('button', { name: /创\s*建/ }));
+ await waitFor(() => expect(createAlertSilence).toHaveBeenCalledTimes(1));
+ const labels = vi.mocked(createAlertSilence).mock.calls[0][0].labels;
+ expect(JSON.parse(JSON.stringify(labels))).toEqual(
+
JSON.parse('{"__proto__":"broker-a","constructor":"rack=one","toString":"orders"}'),
+ );
+ });
+
it('surfaces the label validation message when the silence labels are
invalid', async () => {
vi.mocked(listAlertSilencesPage).mockResolvedValue({
items: [],
diff --git a/web/src/pages/ops/systemAlerts.tsx
b/web/src/pages/ops/systemAlerts.tsx
index 5d826741c..db4480a20 100644
--- a/web/src/pages/ops/systemAlerts.tsx
+++ b/web/src/pages/ops/systemAlerts.tsx
@@ -96,15 +96,17 @@ const parseSilenceLabels = (
invalidMessage: string,
): Record<string, string> | undefined => {
if (!value?.trim()) return undefined;
- const labels: Record<string, string> = {};
+ const labels = new Map<string, string>();
for (const pair of value.split(',')) {
const separator = pair.indexOf('=');
- if (separator <= 0 || !pair.slice(separator + 1).trim()) {
+ const key = pair.slice(0, separator).trim();
+ const labelValue = pair.slice(separator + 1).trim();
+ if (separator <= 0 || !key || !labelValue || labels.has(key)) {
throw new Error(invalidMessage);
}
- labels[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
+ labels.set(key, labelValue);
}
- return labels;
+ return Object.fromEntries(labels);
};
const localDateTimeToUtc = (value: string) => new
Date(`${value}:00`).toISOString();