diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java index b8ca5648e72..e74f12d659f 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertGroupConvergeDao.java @@ -41,5 +41,12 @@ public interface AlertGroupConvergeDao extends JpaRepository findAlertGroupConvergesByEnableIsTrue(); - + + /** + * Query group alarm converge list based on the name + * @param name alert converge name + * @return group alarm converge list + */ + List findAlertGroupConvergesByName(String name); + } diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java index 7fbdcd4837b..c09df759d94 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImpl.java @@ -21,12 +21,14 @@ import jakarta.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; +import java.util.ResourceBundle; import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce; import org.apache.hertzbeat.alert.service.AlertGroupConvergeService; import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -43,41 +45,52 @@ @Transactional(rollbackFor = Exception.class) @Slf4j public class AlertGroupConvergeServiceImpl implements AlertGroupConvergeService { - + @Autowired private AlertGroupConvergeDao alertGroupConvergeDao; - + @Autowired private AlarmGroupReduce alarmGroupReduce; - + + protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); + @Override public void validate(AlertGroupConverge alertGroupConverge, boolean isModify) throws IllegalArgumentException { - // todo + if (alertGroupConverge == null || !StringUtils.hasText(alertGroupConverge.getName())) { + throw new IllegalArgumentException(bundle.getString("alerter.converge.empty.name")); + } + List sameNameConverges = alertGroupConvergeDao.findAlertGroupConvergesByName(alertGroupConverge.getName()); + if (sameNameConverges != null && !sameNameConverges.isEmpty()) { + boolean isDuplicate = sameNameConverges.stream().anyMatch(it -> !it.getId().equals(alertGroupConverge.getId())); + if (isDuplicate) { + throw new IllegalArgumentException(bundle.getString("alerter.converge.duplicate.name")); + } + } } - + @Override public void addAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException { alertGroupConvergeDao.save(alertGroupConverge); refreshAlertGroupConvergesCache(); } - + @Override public void modifyAlertGroupConverge(AlertGroupConverge alertGroupConverge) throws RuntimeException { alertGroupConvergeDao.save(alertGroupConverge); refreshAlertGroupConvergesCache(); } - + @Override public AlertGroupConverge getAlertGroupConverge(long convergeId) throws RuntimeException { return alertGroupConvergeDao.findById(convergeId).orElse(null); } - + @Override public void deleteAlertGroupConverges(Set convergeIds) throws RuntimeException { alertGroupConvergeDao.deleteAlertGroupConvergesByIdIn(convergeIds); refreshAlertGroupConvergesCache(); } - + @Override public Page getAlertGroupConverges(List convergeIds, String search, String sort, String order, int pageIndex, int pageSize) { Specification specification = (root, query, criteriaBuilder) -> { @@ -105,7 +118,7 @@ public Page getAlertGroupConverges(List convergeIds, S PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); return alertGroupConvergeDao.findAll(specification, pageRequest); } - + private void refreshAlertGroupConvergesCache() { List alertGroupConverges = alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue(); alarmGroupReduce.refreshGroupDefines(alertGroupConverges); diff --git a/hertzbeat-alerter/src/main/resources/alerter_en_US.properties b/hertzbeat-alerter/src/main/resources/alerter_en_US.properties index bd7851d06e8..6c30e1f26b4 100644 --- a/hertzbeat-alerter/src/main/resources/alerter_en_US.properties +++ b/hertzbeat-alerter/src/main/resources/alerter_en_US.properties @@ -34,3 +34,5 @@ alerter.priority.1 = Critical Alert alerter.priority.2 = Warning Alert alerter.calculate.parse.error = Expression is not fully parsed, may have syntax errors or incomplete inputs alerter.datasource.executor.not.found = No query executor found +alerter.converge.duplicate.name = Group convergence strategy name already exists +alerter.converge.empty.name = Group convergence strategy name cannot be empty diff --git a/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties b/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties index bc4398b918c..3e307a33340 100644 --- a/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties +++ b/hertzbeat-alerter/src/main/resources/alerter_zh_CN.properties @@ -34,3 +34,5 @@ alerter.priority.1 = 严重告警 alerter.priority.2 = 警告告警 alerter.calculate.parse.error = 表达式未完全解析,可能存在语法错误或输入不完整 alerter.datasource.executor.not.found = 未找到查询执行器 +alerter.converge.duplicate.name = 分组收敛策略名称已存在 +alerter.converge.empty.name = 分组收敛策略名称不能为空 diff --git a/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties b/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties index 6878bf06f66..8e765eb655b 100644 --- a/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties +++ b/hertzbeat-alerter/src/main/resources/alerter_zh_TW.properties @@ -34,3 +34,5 @@ alerter.priority.1 = 嚴重警報 alerter.priority.2 = 警告警報 alerter.calculate.parse.error = 表達式未完全解析,可能存在語法錯誤或輸入不完整 alerter.datasource.executor.not.found = 未找到查詢執行器 +alerter.converge.duplicate.name = 分組收斂策略名稱已存在 +alerter.converge.empty.name = 分組收斂策略名稱不能為空 diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java new file mode 100644 index 00000000000..a0739e225a1 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/AlertGroupConvergeServiceImplTest.java @@ -0,0 +1,115 @@ +/* + * 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.hertzbeat.alert.service.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.List; +import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; +import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce; +import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test case for {@link AlertGroupConvergeServiceImpl} + */ +@ExtendWith(MockitoExtension.class) +public class AlertGroupConvergeServiceImplTest { + + @Mock + private AlertGroupConvergeDao alertGroupConvergeDao; + + @Mock + private AlarmGroupReduce alarmGroupReduce; + + @InjectMocks + private AlertGroupConvergeServiceImpl alertGroupConvergeService; + + private AlertGroupConverge converge; + + @BeforeEach + void setUp() { + converge = AlertGroupConverge.builder() + .id(1L) + .name("test-converge") + .build(); + } + + @Test + void testValidateNewSuccess() { + converge.setId(null); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(Collections.emptyList()); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateNewDuplicateName() { + converge.setId(null); + AlertGroupConverge existing = AlertGroupConverge.builder() + .id(2L) + .name("test-converge") + .build(); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(List.of(existing)); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateNewEmptyName() { + converge.setName(""); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + + converge.setName(null); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, false)); + } + + @Test + void testValidateModifySuccessNameUnchanged() { + when(alertGroupConvergeDao.findAlertGroupConvergesByName("test-converge")) + .thenReturn(List.of(converge)); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, true)); + } + + @Test + void testValidateModifySuccessNameChanged() { + converge.setName("new-name"); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("new-name")) + .thenReturn(Collections.emptyList()); + assertDoesNotThrow(() -> alertGroupConvergeService.validate(converge, true)); + } + + @Test + void testValidateModifyDuplicateName() { + converge.setName("existing-name"); + AlertGroupConverge existing = AlertGroupConverge.builder() + .id(2L) + .name("existing-name") + .build(); + when(alertGroupConvergeDao.findAlertGroupConvergesByName("existing-name")) + .thenReturn(List.of(existing)); + assertThrows(IllegalArgumentException.class, () -> alertGroupConvergeService.validate(converge, true)); + } +} diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java index 423ba43692b..0debf14968e 100644 --- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/alerter/AlertGroupConverge.java @@ -25,6 +25,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -45,7 +46,9 @@ * Alert group converge strategy entity */ @Entity -@Table(name = "hzb_alert_group_converge") +@Table(name = "hzb_alert_group_converge", indexes = { + @Index(name = "idx_name", columnList = "name") +}) @Data @Builder @AllArgsConstructor @@ -62,21 +65,22 @@ public class AlertGroupConverge { @Schema(title = "Policy name", example = "group-converge-1") @Size(max = 100) @NotNull + @Column(name = "name") private String name; - + @Schema(title = "Labels to group by", example = "[\"instance\"]") @Convert(converter = JsonStringListAttributeConverter.class) @Column(name = "group_labels", length = 1024) private List groupLabels; - + @Schema(title = "Initial wait time before sending first group alert (s)", example = "30") @Column(name = "group_wait") private Long groupWait; - + @Schema(title = "Interval between group alert sends (s)", example = "300") @Column(name = "group_interval") private Long groupInterval; - + @Schema(title = "Interval for repeating firing alerts (s), set to 0 to disable repeating", example = "9000") @Column(name = "repeat_interval") private Long repeatInterval; diff --git a/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.html b/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.html index c1e59c89463..98cc59f4988 100644 --- a/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.html +++ b/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.html @@ -98,7 +98,9 @@ {{ data.name }} - {{ label }} + + {{ label.length > 15 ? (label | slice : 0 : 15) + '...' : label }} + {{ data.groupWait }}{{ 'alert.group-converge.seconds' | i18n }} {{ data.groupInterval }}{{ 'alert.group-converge.seconds' | i18n }} diff --git a/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.ts b/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.ts index 8d67bdbe057..b6b6eed9b71 100644 --- a/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.ts +++ b/web-app/src/app/routes/alert/alert-group/alert-group-converge.component.ts @@ -210,7 +210,7 @@ export class AlertGroupConvergeComponent implements OnInit { onNewGroupConverge() { this.groupConverge = new AlertGroupConverge(); - this.groupConverge.groupLabels = ['']; + this.groupConverge.groupLabels = []; this.isManageModalAdd = true; this.isManageModalVisible = true; this.isManageModalOkLoading = false; @@ -234,7 +234,7 @@ export class AlertGroupConvergeComponent implements OnInit { if (message.code === 0) { this.groupConverge = message.data; if (!Array.isArray(this.groupConverge.groupLabels) || this.groupConverge.groupLabels.length === 0) { - this.groupConverge.groupLabels = ['']; + this.groupConverge.groupLabels = []; } this.isManageModalVisible = true; } else { diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index 56a715772de..15990e38ed2 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -83,7 +83,7 @@ "alert.inhibit.equal_labels.common": "Common Labels", "alert.inhibit.equal_labels.custom": "Custom Labels", "alert.inhibit.equal_labels.more": "{{count}} more labels", - "alert.inhibit.equal_labels.placeholder": "Press Enter after entering label name, or select from dropdown", + "alert.inhibit.equal_labels.placeholder": "Input or select labels", "alert.inhibit.equal_labels.tip": "Source and target alerts must have equal values for these label keys, common keys like alertname, instance, severity etc", "alert.inhibit.name": "Inhibit Rule Name", "alert.inhibit.name.tip": "Name to identify this inhibit rule, must be unique", diff --git a/web-app/src/assets/i18n/ja-JP.json b/web-app/src/assets/i18n/ja-JP.json index ed2648d9ffc..66ec1addfe1 100644 --- a/web-app/src/assets/i18n/ja-JP.json +++ b/web-app/src/assets/i18n/ja-JP.json @@ -83,7 +83,7 @@ "alert.inhibit.equal_labels.common": "共通ラベル", "alert.inhibit.equal_labels.custom": "カスタムラベル", "alert.inhibit.equal_labels.more": "{{count}} 個のラベルが追加", - "alert.inhibit.equal_labels.placeholder": "ラベル名を入力後、Enterキーを押すか、ドロップダウンから選択してください", + "alert.inhibit.equal_labels.placeholder": "ラベルを入力または選択", "alert.inhibit.equal_labels.tip": "ソースおよびターゲットのアラートは、これらのラベルキーに対して等しい値を持つ必要があります。共通キーとしてalertname、instance、severityなどがあります", "alert.inhibit.name": "抑制ルール名", "alert.inhibit.name.tip": "この抑制ルールを識別するための名前。ユニークでなければなりません", diff --git a/web-app/src/assets/i18n/pt-BR.json b/web-app/src/assets/i18n/pt-BR.json index 8b255db6f24..dd460551b48 100644 --- a/web-app/src/assets/i18n/pt-BR.json +++ b/web-app/src/assets/i18n/pt-BR.json @@ -305,7 +305,7 @@ "alert.inhibit.equal_labels.common": "Tags comuns", "alert.inhibit.equal_labels.custom": "Tags personalizadas", "alert.inhibit.equal_labels.more": "Existem também {{count}} tags", - "alert.inhibit.equal_labels.placeholder": "Digite o nome da tag e pressione Enter ou selecione na lista suspensa", + "alert.inhibit.equal_labels.placeholder": "Insira ou selecione tags", "alert.inhibit.equal_labels.tip": "As chaves de tag e os valores correspondentes dos alarmes de origem e dos alarmes de destino devem ser iguais. As chaves de tag comuns incluem alertname, instância, gravidade, etc.", "alert.inhibit.name": "Suprimir nome da regra", "alert.inhibit.name.tip": "Um nome que identifique esta regra de supressão precisa ser exclusivo", diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index 4636cb055ef..5f6e4bcb7c8 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -83,7 +83,7 @@ "alert.inhibit.equal_labels.common": "常用标签", "alert.inhibit.equal_labels.custom": "自定义标签", "alert.inhibit.equal_labels.more": "还有 {{count}} 个标签", - "alert.inhibit.equal_labels.placeholder": "输入标签名称后按回车,或从下拉列表选择", + "alert.inhibit.equal_labels.placeholder": "输入或选择标签", "alert.inhibit.equal_labels.tip": "源告警和目标告警这些标签Key和对应值必须相等,常用标签Key如 alertname、instance、severity 等", "alert.inhibit.name": "抑制规则名称", "alert.inhibit.name.tip": "标识此抑制规则的名称,需要唯一", diff --git a/web-app/src/assets/i18n/zh-TW.json b/web-app/src/assets/i18n/zh-TW.json index 211027a20e2..2b4fbc57cbf 100644 --- a/web-app/src/assets/i18n/zh-TW.json +++ b/web-app/src/assets/i18n/zh-TW.json @@ -83,7 +83,7 @@ "alert.inhibit.equal_labels.common": "常用標籤", "alert.inhibit.equal_labels.custom": "自定義標籤", "alert.inhibit.equal_labels.more": "還有 {{count}} 個標籤", - "alert.inhibit.equal_labels.placeholder": "輸入標籤名稱後按回車,或從下拉列表選擇", + "alert.inhibit.equal_labels.placeholder": "輸入或選擇標籤", "alert.inhibit.equal_labels.tip": "源告警和目標告警這些標籤Key和對應值必須相等,常用標籤Key如 alertname、instance、severity 等", "alert.inhibit.name": "抑制規則名稱", "alert.inhibit.name.tip": "標識此抑制規則的名稱,需要唯一",