diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/CalculateAlarm.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/CalculateAlarm.java index ae375ca62d8..4c36c23c022 100644 --- a/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/CalculateAlarm.java +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/CalculateAlarm.java @@ -63,13 +63,15 @@ public class CalculateAlarm { private final CommonDataQueue dataQueue; private final AlertDefineService alertDefineService; private final AlerterProperties alerterProperties; + private final SilenceAlarm silenceAlarm; private final ResourceBundle bundle; - public CalculateAlarm (AlerterWorkerPool workerPool, CommonDataQueue dataQueue, + public CalculateAlarm (AlerterWorkerPool workerPool, CommonDataQueue dataQueue, SilenceAlarm silenceAlarm, AlertDefineService alertDefineService, AlertMonitorDao monitorDao, AlerterProperties alerterProperties) { this.workerPool = workerPool; this.dataQueue = dataQueue; + this.silenceAlarm = silenceAlarm; this.alertDefineService = alertDefineService; this.alerterProperties = alerterProperties; this.bundle = ResourceBundleUtil.getBundle("alerter"); @@ -179,7 +181,7 @@ private void calculate(CollectRep.MetricsData metricsData) { int defineTimes = define.getTimes() == null ? 1 : define.getTimes(); if (times >= defineTimes) { triggeredAlertMap.remove(monitorAlertKey); - dataQueue.addAlertData(triggeredAlert); + silenceAlarm.filterSilenceAndSendData(triggeredAlert); } } else { fieldValueMap.put("app", app); @@ -203,7 +205,7 @@ private void calculate(CollectRep.MetricsData metricsData) { .build(); int defineTimes = define.getTimes() == null ? 1 : define.getTimes(); if (1 >= defineTimes) { - dataQueue.addAlertData(alert); + silenceAlarm.filterSilenceAndSendData(alert); } else { triggeredAlertMap.put(monitorAlertKey, alert); } @@ -269,7 +271,7 @@ private void handlerAvailableMetrics(long monitorId, String app, String metrics, .lastTriggerTime(currentTimeMilli) .times(1) .build(); - dataQueue.addAlertData(resumeAlert); + silenceAlarm.filterSilenceAndSendData(resumeAlert); } } } @@ -301,7 +303,7 @@ private void handlerMonitorAvailableAlert(long monitorId, String app, CollectRep .nextEvalInterval(alerterProperties.getAlertEvalIntervalBase()) .times(1); if (avaAlertDefine.getTimes() == null || avaAlertDefine.getTimes() <= 1) { - dataQueue.addAlertData(alertBuilder.build().clone()); + silenceAlarm.filterSilenceAndSendData(alertBuilder.build().clone()); } else { alertBuilder.status(CommonConstants.ALERT_STATUS_CODE_NOT_REACH); } @@ -318,7 +320,7 @@ private void handlerMonitorAvailableAlert(long monitorId, String app, CollectRep int defineTimes = avaAlertDefine.getTimes() == null ? 1 : avaAlertDefine.getTimes(); if (times >= defineTimes) { preAlert.setStatus(CommonConstants.ALERT_STATUS_CODE_PENDING); - dataQueue.addAlertData(preAlert); + silenceAlarm.filterSilenceAndSendData(preAlert); } else { preAlert.setStatus(CommonConstants.ALERT_STATUS_CODE_NOT_REACH); } diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/SilenceAlarm.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/SilenceAlarm.java new file mode 100644 index 00000000000..4712e932df5 --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/calculate/SilenceAlarm.java @@ -0,0 +1,99 @@ +package org.dromara.hertzbeat.alert.calculate; + +import lombok.RequiredArgsConstructor; +import org.dromara.hertzbeat.alert.dao.AlertSilenceDao; +import org.dromara.hertzbeat.common.cache.CacheFactory; +import org.dromara.hertzbeat.common.cache.ICacheService; +import org.dromara.hertzbeat.common.entity.alerter.Alert; +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.dromara.hertzbeat.common.entity.manager.TagItem; +import org.dromara.hertzbeat.common.queue.CommonDataQueue; +import org.dromara.hertzbeat.common.util.CommonConstants; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; + +/** + * silence alarm + * @author tom + * @date 2023/4/12 20:00 + */ +@Service +@RequiredArgsConstructor +public class SilenceAlarm { + + private final AlertSilenceDao alertSilenceDao; + + private final CommonDataQueue dataQueue; + + @SuppressWarnings("unchecked") + public void filterSilenceAndSendData(Alert alert) { + ICacheService silenceCache = CacheFactory.getAlertSilenceCache(); + List alertSilenceList = (List) silenceCache.get(CommonConstants.CACHE_ALERT_SILENCE); + if (alertSilenceList == null) { + alertSilenceList = alertSilenceDao.findAll(); + silenceCache.put(CommonConstants.CACHE_ALERT_SILENCE, alertSilenceList); + } + for (AlertSilence alertSilence : alertSilenceList) { + // if match the silence rule, return + boolean match = alertSilence.isMatchAll(); + if (!match) { + List tags = alertSilence.getTags(); + if (alert.getTags() != null && !alert.getTags().isEmpty()) { + Map alertTagMap = alert.getTags(); + match = tags.stream().anyMatch(item -> { + if (alertTagMap.containsKey(item.getName())) { + String tagValue = alertTagMap.get(item.getName()); + if (tagValue == null && item.getValue() == null) { + return true; + } else { + return tagValue != null && tagValue.equals(item.getValue()); + } + } else { + return false; + } + }); + } + if (match && alertSilence.getPriorities() != null && !alertSilence.getPriorities().isEmpty()) { + match = alertSilence.getPriorities().stream().anyMatch(item -> item != null && item == alert.getPriority()); + } + } + if (match) { + LocalDateTime nowDate = LocalDateTime.now(); + if (alertSilence.getType() == 0) { + // once time + if (alertSilence.getPeriodStart() != null && alertSilence.getPeriodEnd() != null) { + if (nowDate.isAfter(alertSilence.getPeriodStart().toLocalDateTime()) + && nowDate.isBefore(alertSilence.getPeriodEnd().toLocalDateTime())) { + int times = alertSilence.getTimes() == null ? 0 : alertSilence.getTimes(); + alertSilence.setTimes(times + 1); + alertSilenceDao.save(alertSilence); + return; + } + } + } else if (alertSilence.getType() == 1) { + // cyc time + int currentDayOfWeek = nowDate.toLocalDate().getDayOfWeek().getValue(); + if (alertSilence.getDays() != null && !alertSilence.getDays().isEmpty()) { + boolean dayMatch = alertSilence.getDays().stream().anyMatch(item -> item == currentDayOfWeek); + if (dayMatch && alertSilence.getPeriodStart() != null && alertSilence.getPeriodEnd() != null ) { + LocalTime nowTime = nowDate.toLocalTime(); + + if (nowTime.isAfter(alertSilence.getPeriodStart().toLocalTime()) + && nowTime.isBefore(alertSilence.getPeriodEnd().toLocalTime())) { + int times = alertSilence.getTimes() == null ? 0 : alertSilence.getTimes(); + alertSilence.setTimes(times + 1); + alertSilenceDao.save(alertSilence); + return; + } + } + } + } + } + } + dataQueue.addAlertData(alert); + } +} diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilenceController.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilenceController.java new file mode 100644 index 00000000000..432483f5939 --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilenceController.java @@ -0,0 +1,80 @@ +/* + * 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.dromara.hertzbeat.alert.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.dromara.hertzbeat.alert.service.AlertSilenceService; +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.dromara.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import static org.dromara.hertzbeat.common.util.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Alarm Silence management API + * 告警静默管理API + * @author tom + * @date 2023/04/12 22:32 + */ +@Tag(name = "Alert Silence API | 告警静默管理API") +@RestController +@RequestMapping(path = "/api/alert/silence", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilenceController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @PostMapping + @Operation(summary = "New Alarm Silence | 新增告警静默", description = "Added an alarm Silence | 新增一个告警静默") + public ResponseEntity> addNewAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, false); + alertSilenceService.addAlertSilence(alertSilence); + return ResponseEntity.ok(new Message<>("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Silence | 修改告警静默", description = "Modify an existing alarm Silence | 修改一个已存在告警静默") + public ResponseEntity> modifyAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, true); + alertSilenceService.modifyAlertSilence(alertSilence); + return ResponseEntity.ok(new Message<>("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Silence | 查询告警静默", + description = "You can obtain alarm Silence information based on the alarm Silence ID | 根据告警静默ID获取告警静默信息") + public ResponseEntity> getAlertSilence( + @Parameter(description = "Alarm Silence ID | 告警静默ID", example = "6565463543") @PathVariable("id") long id) { + AlertSilence alertSilence = alertSilenceService.getAlertSilence(id); + Message.MessageBuilder messageBuilder = Message.builder(); + if (alertSilence == null) { + messageBuilder.code(MONITOR_NOT_EXIST_CODE).msg("AlertSilence not exist."); + } else { + messageBuilder.data(alertSilence); + } + return ResponseEntity.ok(messageBuilder.build()); + } + +} diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilencesController.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilencesController.java new file mode 100644 index 00000000000..7f01ddfde99 --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/controller/AlertSilencesController.java @@ -0,0 +1,98 @@ +/* + * 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.dromara.hertzbeat.alert.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.dromara.hertzbeat.alert.service.AlertSilenceService; +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.dromara.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Silence the batch API for alarms + * 静默告警批量API + * @author tom + * @date 2023/04/12 22:03 + */ +@Tag(name = "Alert Silence Batch API | 告警静默管理API") +@RestController +@RequestMapping(path = "/api/alert/silences", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilencesController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @GetMapping + @Operation(summary = "Query the alarm silence list | 查询告警静默列表", + description = "You can obtain the list of alarm silence by querying filter items | 根据查询过滤项获取告警静默信息列表") + public ResponseEntity>> getAlertSilences( + @Parameter(description = "Alarm Silence ID | 告警静默ID", example = "6565463543") @RequestParam(required = false) List ids, + @Parameter(description = "Sort field, default id | 排序字段,默认id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending | 排序方式,asc:升序,desc:降序", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page | 列表当前分页", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages | 列表分页数量", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + if (ids != null && !ids.isEmpty()) { + CriteriaBuilder.In inPredicate= criteriaBuilder.in(root.get("id")); + for (long id : ids) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page alertSilencePage = alertSilenceService.getAlertSilences(specification,pageRequest); + Message> message = new Message<>(alertSilencePage); + return ResponseEntity.ok(message); + } + + @DeleteMapping + @Operation(summary = "Delete alarm silence in batches | 批量删除告警静默", + description = "Delete alarm silence in batches based on the alarm silence ID list | 根据告警静默ID列表批量删除告警静默") + public ResponseEntity> deleteAlertDefines( + @Parameter(description = "Alarm Silence IDs | 告警静默IDs", example = "6565463543") @RequestParam(required = false) List ids + ) { + if (ids != null && !ids.isEmpty()) { + alertSilenceService.deleteAlertSilences(new HashSet<>(ids)); + } + Message message = new Message<>(); + return ResponseEntity.ok(message); + } + +} diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/dao/AlertSilenceDao.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/dao/AlertSilenceDao.java new file mode 100644 index 00000000000..a0765be5f3a --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/dao/AlertSilenceDao.java @@ -0,0 +1,40 @@ +/* + * 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.dromara.hertzbeat.alert.dao; + +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +import java.util.Set; + +/** + * AlertSilence 数据库操作 + * @author tom + * @date 2023/04/12 22:03 + */ +public interface AlertSilenceDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete alarm silence based on the ID list + * @param silenceIds alert silence id list + */ + @Modifying + void deleteAlertSilencesByIdIn(Set silenceIds); +} diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/service/AlertSilenceService.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/service/AlertSilenceService.java new file mode 100644 index 00000000000..9ab07094ad8 --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/service/AlertSilenceService.java @@ -0,0 +1,63 @@ +package org.dromara.hertzbeat.alert.service; + +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +import java.util.Set; + +/** + * management interface service for alert silence + * @author tom + * @date 2023/4/12 13:15 + */ +public interface AlertSilenceService { + /** + * Verify the correctness of the request data parameters + * 校验请求数据参数正确性 + * @param alertSilence AlertSilence + * @param isModify 是否是修改配置 + * @throws IllegalArgumentException A checksum parameter error is thrown | 校验参数错误抛出 + */ + void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertSilence + * @param alertSilence AlertSilence Entity | 静默策略实体 + * @throws RuntimeException Added procedure exception throwing | 新增过程异常抛出 + */ + void addAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Modifying an AlertSilence | 修改静默策略 + * @param alertSilence Alarm definition Entity | 静默策略实体 + * @throws RuntimeException Exception thrown during modification | 修改过程中异常抛出 + */ + void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Obtain AlertSilence information + * @param silenceId AlertSilence ID + * @return AlertSilence + * @throws RuntimeException An exception was thrown during the query | 查询过程中异常抛出 + */ + AlertSilence getAlertSilence(long silenceId) throws RuntimeException; + + + /** + * Delete AlertSilence in batches | 批量删除静默策略 + * @param silenceIds AlertSilence IDs | 静默策略IDs + * @throws RuntimeException Exception thrown during deletion | 删除过程中异常抛出 + */ + void deleteAlertSilences(Set silenceIds) throws RuntimeException; + + /** + * Dynamic conditional query + * 动态条件查询 + * @param specification Query conditions | 查询条件 + * @param pageRequest Paging parameters | 分页参数 + * @return The query results | 查询结果 + */ + Page getAlertSilences(Specification specification, PageRequest pageRequest); +} diff --git a/alerter/src/main/java/org/dromara/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java b/alerter/src/main/java/org/dromara/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java new file mode 100644 index 00000000000..867de02399e --- /dev/null +++ b/alerter/src/main/java/org/dromara/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java @@ -0,0 +1,69 @@ +package org.dromara.hertzbeat.alert.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.alert.dao.AlertSilenceDao; +import org.dromara.hertzbeat.alert.service.AlertSilenceService; +import org.dromara.hertzbeat.common.cache.CacheFactory; +import org.dromara.hertzbeat.common.cache.ICacheService; +import org.dromara.hertzbeat.common.entity.alerter.AlertSilence; +import org.dromara.hertzbeat.common.util.CommonConstants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +/** + * management interface service implement for alert silence + * @author tom + * @date 2023/4/12 13:16 + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertSilenceServiceImpl implements AlertSilenceService { + + @Autowired + private AlertSilenceDao alertSilenceDao; + + @Override + public void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException { + // todo + } + + @Override + public void addAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public AlertSilence getAlertSilence(long silenceId) throws RuntimeException { + return alertSilenceDao.findById(silenceId).orElse(null); + } + + @Override + public void deleteAlertSilences(Set silenceIds) throws RuntimeException { + alertSilenceDao.deleteAlertSilencesByIdIn(silenceIds); + clearAlertSilencesCache(); + } + + @Override + public Page getAlertSilences(Specification specification, PageRequest pageRequest) { + return alertSilenceDao.findAll(specification, pageRequest); + } + + private void clearAlertSilencesCache() { + ICacheService silenceCache = CacheFactory.getAlertSilenceCache(); + silenceCache.remove(CommonConstants.CACHE_ALERT_SILENCE); + } +} diff --git a/common/src/main/java/org/dromara/hertzbeat/common/cache/CacheFactory.java b/common/src/main/java/org/dromara/hertzbeat/common/cache/CacheFactory.java index 2d52cb219d7..a325a664ad3 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/cache/CacheFactory.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/cache/CacheFactory.java @@ -27,8 +27,11 @@ public class CacheFactory { private CacheFactory() {} private static final ICacheService NOTICE_CACHE = - new CaffeineCacheServiceImpl<>(200, 1000, Duration.ofDays(1), false); - + new CaffeineCacheServiceImpl<>(10, 1000, Duration.ofDays(1), false); + + private static final ICacheService ALERT_SILENCE_CACHE = + new CaffeineCacheServiceImpl<>(10, 1000, Duration.ofDays(1), false); + /** * 获取notice模块的cache * @return caffeine cache @@ -36,4 +39,12 @@ private CacheFactory() {} public static ICacheService getNoticeCache() { return NOTICE_CACHE; } + + /** + * get alert silence cache + * @return caffeine cache + */ + public static ICacheService getAlertSilenceCache() { + return ALERT_SILENCE_CACHE; + } } diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/alerter/AlertSilence.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/alerter/AlertSilence.java new file mode 100644 index 00000000000..49a053ac74e --- /dev/null +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/alerter/AlertSilence.java @@ -0,0 +1,132 @@ +/* + * 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.dromara.hertzbeat.common.entity.alerter; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dromara.hertzbeat.common.entity.manager.JsonByteListAttributeConverter; +import org.dromara.hertzbeat.common.entity.manager.JsonTagListAttributeConverter; +import org.dromara.hertzbeat.common.entity.manager.TagItem; +import org.hibernate.validator.constraints.Length; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + +/** + * Alert Silence strategy entity + * 告警静默策略 + * @author tomsun28 + * @date 2023/04/12 22:19 + */ +@Entity +@Table(name = "hzb_alert_silence") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alert Silence Policy Entity | 告警静默策略实体") +@EntityListeners(AuditingEntityListener.class) +public class AlertSilence { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(title = "Alert Silence Policy Entity Primary Key Index ID", + description = "告警静默策略实体主键索引ID", + example = "87584674384", accessMode = READ_ONLY) + private Long id; + + @Schema(title = "Policy name", + description = "策略名称", + example = "silence-1", accessMode = READ_WRITE) + @Length(max = 100) + @NotNull + private String name; + + @Schema(title = "Whether to enable this policy", + description = "是否启用此策略", + example = "true", accessMode = READ_WRITE) + private boolean enable = true; + + @Schema(title = "Whether to match all", + description = "是否应用匹配所有", + example = "true", accessMode = READ_WRITE) + private boolean matchAll = true; + + @Schema(title = "Silence type 0: once, 1:cyc", + description = "静默类型 0:一次性静默 1:周期性静默", accessMode = READ_WRITE) + @NotNull + private Byte type; + + @Schema(title = "Silenced alerts num", + description = "已静默告警次数", accessMode = READ_WRITE) + private Integer times; + + @Schema(title = "匹配告警级别,空为全部告警级别 0:高-emergency-紧急告警-红色 1:中-critical-严重告警-橙色 2:低-warning-警告告警-黄色", + example = "[1]", accessMode = READ_WRITE) + @Convert(converter = JsonByteListAttributeConverter.class) + private List priorities; + + @Schema(description = "匹配告警信息标签(monitorId:xxx,monitorName:xxx)", example = "{name: key1, value: value1}", + accessMode = READ_WRITE) + @Convert(converter = JsonTagListAttributeConverter.class) + private List tags; + + @Schema(title = "周期性静默时有效 星期几,多选,全选或空则为每天 7:周日 1:周一 2:周二 3:周三 4:周四 5:周五 6:周六", example = "[0,1]", accessMode = READ_WRITE) + @Convert(converter = JsonByteListAttributeConverter.class) + private List days; + + @Schema(title = "限制时间段起始", example = "00:00:00", accessMode = READ_WRITE) + private ZonedDateTime periodStart; + + @Schema(title = "限制时间段截止", example = "23:59:59", accessMode = READ_WRITE) + private ZonedDateTime periodEnd; + + @Schema(title = "The creator of this record", description = "此条记录创建者", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", + description = "此条记录最新修改者", + example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)", + description = "记录创建时间", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", + description = "记录最新修改时间", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/JsonTagListAttributeConverter.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/JsonTagListAttributeConverter.java index e455edde3c8..b32be2d1f28 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/JsonTagListAttributeConverter.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/JsonTagListAttributeConverter.java @@ -30,22 +30,22 @@ * @author tom * @date 2021/12/4 07:54 */ -public class JsonTagListAttributeConverter implements AttributeConverter, String> { +public class JsonTagListAttributeConverter implements AttributeConverter, String> { @Override - public String convertToDatabaseColumn(List attribute) { + public String convertToDatabaseColumn(List attribute) { return JsonUtil.toJson(attribute); } @Override - public List convertToEntityAttribute(String dbData) { + public List convertToEntityAttribute(String dbData) { try { return JsonUtil.fromJson(dbData, new TypeReference<>() {}); } catch (Exception e) { // history data handler Map map = JsonUtil.fromJson(dbData, new TypeReference<>() {}); if (map != null) { - return map.entrySet().stream().map(entry -> new NoticeRule.TagItem(entry.getKey(), entry.getValue())).collect(Collectors.toList()); + return map.entrySet().stream().map(entry -> new TagItem(entry.getKey(), entry.getValue())).collect(Collectors.toList()); } else { return null; } diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/NoticeRule.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/NoticeRule.java index ba297846be9..e81ee7dcf96 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/NoticeRule.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/NoticeRule.java @@ -29,7 +29,6 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; -import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.time.LocalDateTime; import java.time.ZonedDateTime; @@ -131,18 +130,4 @@ public class NoticeRule { description = "记录最新修改时间", accessMode = READ_ONLY) @LastModifiedDate private LocalDateTime gmtUpdate; - - - @AllArgsConstructor - @NoArgsConstructor - @Data - public static class TagItem { - - @Schema(title = "Tag Name") - @NotBlank - private String name; - - @Schema(title = "Tag Value") - private String value; - } } diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/TagItem.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/TagItem.java new file mode 100644 index 00000000000..afe3c5bd140 --- /dev/null +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/TagItem.java @@ -0,0 +1,27 @@ +package org.dromara.hertzbeat.common.entity.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +/** + * tag item + * @author tom + * @date 2023/4/12 12:58 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class TagItem { + + @Schema(title = "Tag Name") + @NotBlank + private String name; + + @Schema(title = "Tag Value") + private String value; + +} diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/warehouse/History.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/warehouse/History.java index 6a96ef5fe98..6fea3e4db16 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/entity/warehouse/History.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/warehouse/History.java @@ -49,13 +49,14 @@ public class History { private String metric; @Schema(title = "实例") + @Column(length = 2048) private String instance; @Schema(title = "字段类型 0: 数值 1:字符串") private Byte metricType; @Schema(title = "字符值") - @Column(length = 1024) + @Column(length = 2048) private String str; @Schema(title = "数值") diff --git a/common/src/main/java/org/dromara/hertzbeat/common/util/CommonConstants.java b/common/src/main/java/org/dromara/hertzbeat/common/util/CommonConstants.java index bd220eb5e30..e640a96fb07 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/util/CommonConstants.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/util/CommonConstants.java @@ -270,4 +270,9 @@ public interface CommonConstants { * cache key notice_rule */ String CACHE_NOTICE_RULE = "notice_rule"; + + /** + * cache key alert silence + */ + String CACHE_ALERT_SILENCE = "alert_silence"; } diff --git a/manager/src/test/java/org/dromara/hertzbeat/manager/controller/NoticeConfigControllerTest.java b/manager/src/test/java/org/dromara/hertzbeat/manager/controller/NoticeConfigControllerTest.java index a23ed28b98a..d803dea83f4 100644 --- a/manager/src/test/java/org/dromara/hertzbeat/manager/controller/NoticeConfigControllerTest.java +++ b/manager/src/test/java/org/dromara/hertzbeat/manager/controller/NoticeConfigControllerTest.java @@ -2,6 +2,7 @@ import org.dromara.hertzbeat.common.entity.manager.NoticeReceiver; import org.dromara.hertzbeat.common.entity.manager.NoticeRule; +import org.dromara.hertzbeat.common.entity.manager.TagItem; import org.dromara.hertzbeat.common.util.CommonConstants; import org.dromara.hertzbeat.common.util.JsonUtil; import org.dromara.hertzbeat.manager.service.impl.NoticeConfigServiceImpl; @@ -39,8 +40,8 @@ class NoticeConfigControllerTest { public NoticeRule getNoticeRule(){ - List tags = new ArrayList<>(); - NoticeRule.TagItem tagItem = new NoticeRule.TagItem(); + List tags = new ArrayList<>(); + TagItem tagItem = new TagItem(); tagItem.setName("key1"); tagItem.setValue("value1"); tags.add(tagItem); diff --git a/manager/src/test/java/org/dromara/hertzbeat/manager/service/NoticeConfigServiceTest.java b/manager/src/test/java/org/dromara/hertzbeat/manager/service/NoticeConfigServiceTest.java index 39165748632..351663f81ea 100644 --- a/manager/src/test/java/org/dromara/hertzbeat/manager/service/NoticeConfigServiceTest.java +++ b/manager/src/test/java/org/dromara/hertzbeat/manager/service/NoticeConfigServiceTest.java @@ -5,6 +5,7 @@ import org.dromara.hertzbeat.common.entity.alerter.Alert; import org.dromara.hertzbeat.common.entity.manager.NoticeReceiver; import org.dromara.hertzbeat.common.entity.manager.NoticeRule; +import org.dromara.hertzbeat.common.entity.manager.TagItem; import org.dromara.hertzbeat.manager.component.alerter.DispatcherAlarm; import org.dromara.hertzbeat.manager.dao.NoticeReceiverDao; import org.dromara.hertzbeat.manager.dao.NoticeRuleDao; @@ -118,8 +119,8 @@ void getReceiverFilterRule() { final String tagValue = "tagValue"; final List priorities = Lists.newArrayList(priority); final List prioritiesFail = Lists.newArrayList(priorityFail); - final List tags = Lists.newArrayList(new NoticeRule.TagItem(tagName, tagValue)); - final List tagsFail = Lists.newArrayList(new NoticeRule.TagItem(tagNameFail, tagValue)); + final List tags = Lists.newArrayList(new TagItem(tagName, tagValue)); + final List tagsFail = Lists.newArrayList(new TagItem(tagNameFail, tagValue)); final Map tagsMap = Maps.newHashMap(tagName, tagValue); final NoticeRule rule1 = NoticeRule.builder() .id(1L) @@ -178,4 +179,4 @@ void sendTestMsg() { noticeConfigService.sendTestMsg(noticeReceiver); verify(dispatcherAlarm, times(1)).sendNoticeMsg(eq(noticeReceiver), any(Alert.class)); } -} \ No newline at end of file +} diff --git a/script/sql/schema.sql b/script/sql/schema.sql index 310a4601d0b..e0d58e6ff62 100644 --- a/script/sql/schema.sql +++ b/script/sql/schema.sql @@ -157,6 +157,30 @@ CREATE TABLE hzb_alert_define_monitor_bind index index_bind (alert_define_id, monitor_id) ) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; +-- ---------------------------- +-- Table structure for hzb_alert_silence +-- ---------------------------- +DROP TABLE IF EXISTS hzb_alert_silence ; +CREATE TABLE hzb_alert_silence +( + id bigint not null auto_increment comment '告警静默主键索引ID', + name varchar(100) not null comment '静默策略名称', + enable boolean not null default true comment '是否启用此策略', + match_all boolean not null default true comment '是否应用匹配所有', + priorities varchar(100) comment '匹配告警级别,空为全部告警级别', + tags varchar(4000) comment '匹配告警信息标签(monitorId:xxx,monitorName:xxx)', + times int not null default 0 comment '已静默告警次数', + type tinyint not null default 0 comment '静默类型 0:一次性静默 1:周期性静默', + days varchar(100) comment '周期性静默时有效 星期几,多选,全选或空则为每天 7:周日 1:周一 2:周二 3:周三 4:周四 5:周五 6:周六', + period_start timestamp comment '静默时间段起始:00:00:00', + period_end timestamp comment '静默时间段截止:23:59:59', + creator varchar(100) comment '创建者', + modifier varchar(100) comment '最新修改者', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (id) +) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; + -- ---------------------------- -- Table structure for alert -- ---------------------------- @@ -193,8 +217,8 @@ CREATE TABLE hzb_notice_rule receiver_name varchar(100) not null comment '消息接收人标识', enable boolean not null default true comment '是否启用此策略', filter_all boolean not null default true comment '是否转发所有', - priorities varchar(100) comment '过滤匹配告警级别,空为全部告警级别', - tags varchar(4000) comment '过滤匹配告警信息标签(monitorId:xxx,monitorName:xxx)', + priorities varchar(100) comment '匹配告警级别,空为全部告警级别', + tags varchar(4000) comment '匹配告警信息标签(monitorId:xxx,monitorName:xxx)', days varchar(100) comment '星期几,多选,全选或空则为每天 7:周日 1:周一 2:周二 3:周三 4:周四 5:周五 6:周六', period_start timestamp comment '限制时间段起始:00:00:00', period_end timestamp comment '限制时间段截止:23:59:59', @@ -234,4 +258,23 @@ CREATE TABLE hzb_notice_receiver primary key (id) ) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; +-- ---------------------------- +-- Table structure for hzb_history +-- ---------------------------- +DROP TABLE IF EXISTS hzb_history ; +CREATE TABLE hzb_history +( + id bigint not null auto_increment comment '通知策略主键索引ID', + monitor_id bigint not null comment '监控ID', + app varchar(100) not null comment '监控类型 mysql oracle db2', + metrics varchar(100) not null comment '指标集合名称 innodb disk cpu', + metric varchar(100) not null comment '指标名称 usage speed count', + instance varchar(1024) comment '实例', + metric_type tinyint not null comment '字段类型 0: 数值 1:字符串', + str varchar(1024) comment '字符值', + dou float comment '数值', + time bigint comment '采集时间戳', + primary key (id) +) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; + COMMIT; diff --git a/web-app/src/app/pojo/AlertSilence.ts b/web-app/src/app/pojo/AlertSilence.ts new file mode 100644 index 00000000000..d2a21603111 --- /dev/null +++ b/web-app/src/app/pojo/AlertSilence.ts @@ -0,0 +1,19 @@ +import { TagItem } from './NoticeRule'; + +export class AlertSilence { + id!: number; + name!: string; + enable: boolean = true; + matchAll: boolean = true; + type: number = 0; + times!: number; + priorities!: number[]; + tags!: TagItem[]; + days!: number[]; + periodStart: Date = new Date(); + periodEnd: Date = new Date(); + creator!: string; + modifier!: string; + gmtCreate!: number; + gmtUpdate!: number; +} diff --git a/web-app/src/app/routes/alert/alert-center/alert-center.component.html b/web-app/src/app/routes/alert/alert-center/alert-center.component.html index 061870fbdfc..afc3bb2ac57 100644 --- a/web-app/src/app/routes/alert/alert-center/alert-center.component.html +++ b/web-app/src/app/routes/alert/alert-center/alert-center.component.html @@ -111,7 +111,7 @@ - {{ data.target }} + {{ renderAlertTarget(data.target) }} diff --git a/web-app/src/app/routes/alert/alert-center/alert-center.component.ts b/web-app/src/app/routes/alert/alert-center/alert-center.component.ts index e620901688b..69e18098797 100644 --- a/web-app/src/app/routes/alert/alert-center/alert-center.component.ts +++ b/web-app/src/app/routes/alert/alert-center/alert-center.component.ts @@ -81,6 +81,20 @@ export class AlertCenterComponent implements OnInit { ); } + renderAlertTarget(target: string): string { + if (target == undefined || target === '') { + return target; + } + const targets = target.split('.'); + if (targets.length === 3) { + return `${this.i18nSvc.fanyi(`monitor.app.${targets[0]}`)} / ${targets[1]} / ${targets[2]}`; + } + if (target === 'availability') { + return this.i18nSvc.fanyi('monitor.availability'); + } + return target; + } + onDeleteAlerts() { if (this.checkedAlertIds == null || this.checkedAlertIds.size === 0) { this.notifySvc.warning(this.i18nSvc.fanyi('alert.center.notify.no-delete'), ''); diff --git a/web-app/src/app/routes/alert/alert-routing.module.ts b/web-app/src/app/routes/alert/alert-routing.module.ts index 4670289bbbc..4b3dcef0dcd 100644 --- a/web-app/src/app/routes/alert/alert-routing.module.ts +++ b/web-app/src/app/routes/alert/alert-routing.module.ts @@ -4,12 +4,14 @@ import { RouterModule, Routes } from '@angular/router'; import { AlertCenterComponent } from './alert-center/alert-center.component'; import { AlertNoticeComponent } from './alert-notice/alert-notice.component'; import { AlertSettingComponent } from './alert-setting/alert-setting.component'; +import { AlertSilenceComponent } from './alert-silence/alert-silence.component'; const routes: Routes = [ { path: '', component: AlertCenterComponent }, { path: 'center', component: AlertCenterComponent }, { path: 'setting', component: AlertSettingComponent }, { path: 'notice', component: AlertNoticeComponent }, + { path: 'silence', component: AlertSilenceComponent }, { path: '**', component: AlertCenterComponent } ]; diff --git a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html index 2911c29e1d7..bbbde377abe 100644 --- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html +++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html @@ -65,8 +65,10 @@ - {{ data.app + '.' + data.metric + '.' + data.field }} - {{ data.app + '.' + data.metric }} + {{ 'monitor.app.' + data.app | i18n }} / {{ data.metric + ' / ' + data.field }} + + {{ 'monitor.app.' + data.app | i18n }} / {{ 'monitor.availability' | i18n }} + {{ data.expr }} diff --git a/web-app/src/app/routes/alert/alert-silence/alert-silence.component.html b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.html new file mode 100644 index 00000000000..a490537644e --- /dev/null +++ b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.html @@ -0,0 +1,232 @@ + + + + + + {{ 'menu.dashboard' | i18n }} + + + + + {{ 'menu.alert.silence' | i18n }} + + + + + + + +
+ + + +
+ + + + + + {{ 'alert.silence.name' | i18n }} + {{ 'alert.silence.type' | i18n }} + {{ 'alert.silence.tags' | i18n }} + {{ 'alert.silence.times' | i18n }} + {{ 'alert.silence.enable' | i18n }} + {{ 'common.edit' | i18n }} + + + + + + + {{ data.name }} + + + + + {{ 'alert.silence.type.once' | i18n }} + + + + {{ 'alert.silence.type.cyc' | i18n }} + + + + + {{ sliceTagName(tag) }} + + + + + {{ 'alert.silence.match-all' | i18n }} + + + + + + {{ data.times == undefined ? 0 : data.times }} + + + + + + + + + + + + + + {{ 'common.total' | i18n }} {{ total }} + + + +
+
+ + {{ 'alert.silence.name' | i18n }} + + + + + + {{ 'alert.silence.match-all' | i18n }} + + + + + + {{ 'alert.silence.tags' | i18n }} + + + + + + + {{ 'alert.silence.priority' | i18n }} + + + + + + + + + + + {{ 'alert.silence.type' | i18n }} + + + + + + + + + {{ 'alert.silence.time' | i18n }} + + + + + + {{ 'alert.notice.rule.period-chose' | i18n }} + + + + + + {{ 'alert.notice.rule.time' | i18n }} + + + + + + + + {{ 'alert.notice.rule.enable' | i18n }} + + + + +
+
+
diff --git a/web-app/src/app/routes/alert/alert-silence/alert-silence.component.less b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web-app/src/app/routes/alert/alert-silence/alert-silence.component.spec.ts b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.spec.ts new file mode 100644 index 00000000000..a74c4d5cc29 --- /dev/null +++ b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AlertSilenceComponent } from './alert-silence.component'; + +describe('AlertSilenceComponent', () => { + let component: AlertSilenceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AlertSilenceComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(AlertSilenceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/routes/alert/alert-silence/alert-silence.component.ts b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.ts new file mode 100644 index 00000000000..0810b02e9a2 --- /dev/null +++ b/web-app/src/app/routes/alert/alert-silence/alert-silence.component.ts @@ -0,0 +1,390 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { I18NService } from '@core'; +import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme'; +import { NzModalService } from 'ng-zorro-antd/modal'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { NzTableQueryParams } from 'ng-zorro-antd/table'; +import { finalize } from 'rxjs/operators'; + +import { AlertSilence } from '../../../pojo/AlertSilence'; +import { TagItem } from '../../../pojo/NoticeRule'; +import { AlertSilenceService } from '../../../service/alert-silence.service'; +import { TagService } from '../../../service/tag.service'; + +@Component({ + selector: 'app-alert-silence', + templateUrl: './alert-silence.component.html', + styleUrls: ['./alert-silence.component.less'] +}) +export class AlertSilenceComponent implements OnInit { + constructor( + private modal: NzModalService, + private notifySvc: NzNotificationService, + private alertSilenceService: AlertSilenceService, + private settingsSvc: SettingsService, + private tagService: TagService, + @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService + ) {} + + pageIndex: number = 1; + pageSize: number = 8; + total: number = 0; + silences!: AlertSilence[]; + tableLoading: boolean = true; + checkedSilenceIds = new Set(); + + ngOnInit(): void { + this.loadAlertSilenceTable(); + } + + sync() { + this.loadAlertSilenceTable(); + } + + loadAlertSilenceTable() { + this.tableLoading = true; + let alertDefineInit$ = this.alertSilenceService.getAlertSilences(this.pageIndex - 1, this.pageSize).subscribe( + message => { + this.tableLoading = false; + this.checkedAll = false; + this.checkedSilenceIds.clear(); + if (message.code === 0) { + let page = message.data; + this.silences = page.content; + this.pageIndex = page.number + 1; + this.total = page.totalElements; + } else { + console.warn(message.msg); + } + alertDefineInit$.unsubscribe(); + }, + error => { + this.tableLoading = false; + alertDefineInit$.unsubscribe(); + } + ); + } + + updateAlertSilence(alertSilence: AlertSilence) { + this.tableLoading = true; + const updateDefine$ = this.alertSilenceService + .editAlertSilence(alertSilence) + .pipe( + finalize(() => { + updateDefine$.unsubscribe(); + this.tableLoading = false; + }) + ) + .subscribe( + message => { + if (message.code === 0) { + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.edit-success'), ''); + } else { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), message.msg); + } + this.loadAlertSilenceTable(); + this.tableLoading = false; + }, + error => { + this.tableLoading = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg); + } + ); + } + + onDeleteAlertSilences() { + if (this.checkedSilenceIds == null || this.checkedSilenceIds.size === 0) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); + return; + } + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.deleteAlertSilences(this.checkedSilenceIds) + }); + } + + onDeleteOneAlertSilence(id: number) { + let ids = new Set(); + ids.add(id); + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('common.confirm.delete'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.deleteAlertSilences(ids) + }); + } + + deleteAlertSilences(silenceIds: Set) { + if (silenceIds == null || silenceIds.size == 0) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); + return; + } + this.tableLoading = true; + const deleteDefines$ = this.alertSilenceService.deleteAlertSilences(silenceIds).subscribe( + message => { + deleteDefines$.unsubscribe(); + if (message.code === 0) { + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), ''); + this.loadAlertSilenceTable(); + } else { + this.tableLoading = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), message.msg); + } + }, + error => { + this.tableLoading = false; + deleteDefines$.unsubscribe(); + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), error.msg); + } + ); + } + + // begin: 列表多选分页逻辑 + checkedAll: boolean = false; + onAllChecked(checked: boolean) { + if (checked) { + this.silences.forEach(item => this.checkedSilenceIds.add(item.id)); + } else { + this.checkedSilenceIds.clear(); + } + } + onItemChecked(id: number, checked: boolean) { + if (checked) { + this.checkedSilenceIds.add(id); + } else { + this.checkedSilenceIds.delete(id); + } + } + /** + * 分页回调 + * + * @param params 页码信息 + */ + onTablePageChange(params: NzTableQueryParams) { + const { pageSize, pageIndex, sort, filter } = params; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.loadAlertSilenceTable(); + } + // end: 列表多选逻辑 + + // start 新增修改告警静默 model + isManageModalVisible = false; + isManageModalOkLoading = false; + isManageModalAdd = true; + silence: AlertSilence = new AlertSilence(); + searchTag!: string; + tagsOption: any[] = []; + matchTags: string[] = []; + silenceDates!: Date[]; + dayCheckOptions = [ + { label: this.i18nSvc.fanyi('common.week.7'), value: 7, checked: true }, + { label: this.i18nSvc.fanyi('common.week.1'), value: 1, checked: true }, + { label: this.i18nSvc.fanyi('common.week.2'), value: 2, checked: true }, + { label: this.i18nSvc.fanyi('common.week.3'), value: 3, checked: true }, + { label: this.i18nSvc.fanyi('common.week.4'), value: 4, checked: true }, + { label: this.i18nSvc.fanyi('common.week.5'), value: 5, checked: true }, + { label: this.i18nSvc.fanyi('common.week.6'), value: 6, checked: true } + ]; + + onNewAlertSilence() { + this.silence = new AlertSilence(); + let now = new Date(); + now.setHours(now.getHours() + 6); + this.silenceDates = [new Date(), now]; + this.isManageModalAdd = true; + this.isManageModalVisible = true; + this.isManageModalOkLoading = false; + } + onManageModalCancel() { + this.isManageModalVisible = false; + } + + onEditAlertSilence(silenceId: number) { + if (silenceId == null) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-edit'), ''); + return; + } + this.editAlertSilence(silenceId); + } + + editAlertSilence(silenceId: number) { + this.isManageModalAdd = false; + this.isManageModalVisible = true; + this.isManageModalOkLoading = false; + const getSilence$ = this.alertSilenceService + .getAlertSilence(silenceId) + .pipe( + finalize(() => { + getSilence$.unsubscribe(); + }) + ) + .subscribe( + message => { + if (message.code === 0) { + this.silence = message.data; + if (this.silence.type === 0) { + this.silenceDates = [this.silence.periodStart, this.silence.periodEnd]; + } + this.isManageModalVisible = true; + this.isManageModalAdd = false; + this.matchTags = []; + if (this.silence.tags != undefined) { + this.silence.tags.forEach(item => { + let tag = `${item.name}`; + if (item.value != undefined) { + tag = `${tag}:${item.value}`; + } + this.matchTags.push(tag); + this.tagsOption.push({ + value: tag, + label: tag + }); + }); + } + } else { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), message.msg); + } + }, + error => { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg); + } + ); + } + onManageModalOk() { + this.silence.tags = []; + this.matchTags.forEach(tag => { + let tmp: string[] = tag.split(':'); + let tagItem = new TagItem(); + if (tmp.length == 1) { + tagItem.name = tmp[0]; + this.silence.tags.push(tagItem); + } else if (tmp.length == 2) { + tagItem.name = tmp[0]; + tagItem.value = tmp[1]; + this.silence.tags.push(tagItem); + } + }); + if (this.silence.priorities != undefined) { + this.silence.priorities = this.silence.priorities.filter(item => item != null && item != 9); + } + if (this.silence.type === 0) { + this.silence.periodStart = this.silenceDates[0]; + this.silence.periodEnd = this.silenceDates[1]; + } + this.isManageModalOkLoading = true; + if (this.isManageModalAdd) { + const modalOk$ = this.alertSilenceService + .newAlertSilence(this.silence) + .pipe( + finalize(() => { + modalOk$.unsubscribe(); + this.isManageModalOkLoading = false; + }) + ) + .subscribe( + message => { + if (message.code === 0) { + this.isManageModalVisible = false; + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.new-success'), ''); + this.loadAlertSilenceTable(); + } else { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.new-fail'), message.msg); + } + }, + error => { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.new-fail'), error.msg); + } + ); + } else { + const modalOk$ = this.alertSilenceService + .editAlertSilence(this.silence) + .pipe( + finalize(() => { + modalOk$.unsubscribe(); + this.isManageModalOkLoading = false; + }) + ) + .subscribe( + message => { + if (message.code === 0) { + this.isManageModalVisible = false; + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.edit-success'), ''); + this.loadAlertSilenceTable(); + } else { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), message.msg); + } + }, + error => { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg); + } + ); + } + } + onSilenceDaysChange(value: any[]) { + this.silence.days = value + .filter(item => item.checked == true) + .map(item => item.value) + .concat(); + } + + onPrioritiesChange() { + if (this.silence.priorities != undefined) { + let isAll = false; + this.silence.priorities.forEach(item => { + if (item == 9) { + isAll = true; + } + }); + if (isAll) { + this.silence.priorities = [9, 0, 1, 2]; + } + } + } + + loadTagsOption() { + let tagsInit$ = this.tagService.loadTags(this.searchTag, undefined, 0, 1000).subscribe( + message => { + if (message.code === 0) { + let page = message.data; + this.tagsOption = []; + if (page.content != undefined) { + page.content.forEach(item => { + let tag = `${item.name}`; + if (item.value != undefined) { + tag = `${tag}:${item.value}`; + } + this.tagsOption.push({ + value: tag, + label: tag + }); + }); + } + } else { + console.warn(message.msg); + } + tagsInit$.unsubscribe(); + }, + error => { + tagsInit$.unsubscribe(); + console.error(error.msg); + } + ); + } + + sliceTagName(tag: any): string { + if (tag.value != undefined && tag.value.trim() != '') { + return `${tag.name}:${tag.value}`; + } else { + return tag.name; + } + } +} diff --git a/web-app/src/app/routes/alert/alert.module.ts b/web-app/src/app/routes/alert/alert.module.ts index 626284c137f..415ab966462 100644 --- a/web-app/src/app/routes/alert/alert.module.ts +++ b/web-app/src/app/routes/alert/alert.module.ts @@ -1,8 +1,10 @@ import { NgModule, Type } from '@angular/core'; import { SharedModule } from '@shared'; +import { NzBadgeModule } from 'ng-zorro-antd/badge'; import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb'; import { NzCascaderModule } from 'ng-zorro-antd/cascader'; import { NzCollapseModule } from 'ng-zorro-antd/collapse'; +import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; import { NzDividerModule } from 'ng-zorro-antd/divider'; import { NzListModule } from 'ng-zorro-antd/list'; import { NzRadioModule } from 'ng-zorro-antd/radio'; @@ -15,8 +17,9 @@ import { AlertCenterComponent } from './alert-center/alert-center.component'; import { AlertNoticeComponent } from './alert-notice/alert-notice.component'; import { AlertRoutingModule } from './alert-routing.module'; import { AlertSettingComponent } from './alert-setting/alert-setting.component'; +import { AlertSilenceComponent } from './alert-silence/alert-silence.component'; -const COMPONENTS: Array> = [AlertCenterComponent, AlertSettingComponent, AlertNoticeComponent]; +const COMPONENTS: Array> = [AlertCenterComponent, AlertSettingComponent, AlertNoticeComponent, AlertSilenceComponent]; @NgModule({ imports: [ @@ -31,7 +34,9 @@ const COMPONENTS: Array> = [AlertCenterComponent, AlertSettingCompone NzTransferModule, NzCollapseModule, NzListModule, - NzTimePickerModule + NzTimePickerModule, + NzDatePickerModule, + NzBadgeModule ], declarations: COMPONENTS }) diff --git a/web-app/src/app/service/alert-silence.service.spec.ts b/web-app/src/app/service/alert-silence.service.spec.ts new file mode 100644 index 00000000000..9deef67b4a1 --- /dev/null +++ b/web-app/src/app/service/alert-silence.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AlertSilenceService } from './alert-silence.service'; + +describe('AlertSilenceService', () => { + let service: AlertSilenceService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AlertSilenceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/service/alert-silence.service.ts b/web-app/src/app/service/alert-silence.service.ts new file mode 100644 index 00000000000..a3db21f7cf8 --- /dev/null +++ b/web-app/src/app/service/alert-silence.service.ts @@ -0,0 +1,55 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { AlertSilence } from '../pojo/AlertSilence'; +import { Message } from '../pojo/Message'; +import { Page } from '../pojo/Page'; + +const alert_silence_uri = '/alert/silence'; +const alert_silences_uri = '/alert/silences'; + +@Injectable({ + providedIn: 'root' +}) +export class AlertSilenceService { + constructor(private http: HttpClient) {} + + public newAlertSilence(body: AlertSilence): Observable> { + return this.http.post>(alert_silence_uri, body); + } + + public editAlertSilence(body: AlertSilence): Observable> { + return this.http.put>(alert_silence_uri, body); + } + + public getAlertSilence(silenceId: number): Observable> { + return this.http.get>(`${alert_silence_uri}/${silenceId}`); + } + + public deleteAlertSilences(silenceIds: Set): Observable> { + let httpParams = new HttpParams(); + silenceIds.forEach(silenceId => { + // 注意HttpParams是不可变对象 需要保存append后返回的对象为最新对象 + // append方法可以叠加同一key, set方法会把key之前的值覆盖只留一个key-value + httpParams = httpParams.append('ids', silenceId); + }); + const options = { params: httpParams }; + return this.http.delete>(alert_silences_uri, options); + } + + public getAlertSilences(pageIndex: number, pageSize: number): Observable>> { + pageIndex = pageIndex ? pageIndex : 0; + pageSize = pageSize ? pageSize : 8; + // 注意HttpParams是不可变对象 需要保存set后返回的对象为最新对象 + let httpParams = new HttpParams(); + httpParams = httpParams.appendAll({ + sort: 'id', + order: 'desc', + pageIndex: pageIndex, + pageSize: pageSize + }); + const options = { params: httpParams }; + return this.http.get>>(alert_silences_uri, options); + } +} diff --git a/web-app/src/assets/app-data.json b/web-app/src/assets/app-data.json index 354eda36a56..d2e501a9305 100644 --- a/web-app/src/assets/app-data.json +++ b/web-app/src/assets/app-data.json @@ -83,18 +83,24 @@ "group": true, "hideInBreadcrumb": true, "children": [ - { - "text": "告警中心", - "i18n": "menu.alert.center", - "icon": "anticon-alert", - "link": "/alert/center" - }, { "text": "告警配置", "i18n": "menu.alert.setting", "icon": "anticon-bulb", "link": "/alert/setting" }, + { + "text": "告警静默", + "i18n": "menu.alert.silence", + "icon": "anticon-audio-muted", + "link": "/alert/silence" + }, + { + "text": "告警中心", + "i18n": "menu.alert.center", + "icon": "anticon-alert", + "link": "/alert/center" + }, { "text": "告警通知", "i18n": "menu.alert.dispatch", diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index 4838838cf7a..b176f5cb5bf 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -30,6 +30,7 @@ "": "Alert", "center": "Alert Center", "setting": "Threshold Rule", + "silence": "Alert Silence", "dispatch": "Notification" }, "extras": { @@ -131,6 +132,19 @@ "alert.setting.target.other": "Other metric objects of the row", "alert.setting.target.instance": "Instance of the row", "alert.setting.operator": "Supported operator functions", + "alert.silence.new": "New Silence Rule", + "alert.silence.edit": "Edit Silence Rule", + "alert.silence.delete": "Delete Silence Rule", + "alert.silence.name": "Silence Rule Name", + "alert.silence.match-all": "Match All", + "alert.silence.priority": "Priority Match", + "alert.silence.type.once": "One Time Silence", + "alert.silence.type.cyc": "Periodic Silence", + "alert.silence.type": "Silence Type", + "alert.silence.tags": "Tag Match", + "alert.silence.time": "Silence Period", + "alert.silence.times": "Silenced Alerts Num", + "alert.silence.enable": "Enable Silence", "alert.center.delete": "Delete Alerts", "alert.center.clear": "Clear All", "alert.center.deal": "Mark Processed", diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index 5d7c9e3df0f..f49782d561d 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -29,6 +29,7 @@ "alert": { "": "告警", "center": "告警中心", + "silence": "告警静默", "setting": "阈值规则", "dispatch": "消息通知" }, @@ -131,6 +132,19 @@ "alert.setting.target.other": "所属行其它指标对象", "alert.setting.target.instance": "所属行实例", "alert.setting.operator": "支持操作符函数", + "alert.silence.new": "新增静默策略", + "alert.silence.edit": "编辑静默策略", + "alert.silence.delete": "删除静默策略", + "alert.silence.name": "策略名称", + "alert.silence.match-all": "应用所有", + "alert.silence.priority": "匹配级别", + "alert.silence.type.once": "一次性静默", + "alert.silence.type.cyc": "周期性静默", + "alert.silence.type": "静默类型", + "alert.silence.tags": "匹配标签", + "alert.silence.time": "静默时段", + "alert.silence.times": "已静默告警数", + "alert.silence.enable": "启用静默策略", "alert.center.delete": "删除告警", "alert.center.clear": "一键清空", "alert.center.deal": "标记已处理", diff --git a/web-app/src/assets/i18n/zh-TW.json b/web-app/src/assets/i18n/zh-TW.json index d79331cf35c..203221e197f 100644 --- a/web-app/src/assets/i18n/zh-TW.json +++ b/web-app/src/assets/i18n/zh-TW.json @@ -29,7 +29,8 @@ "alert": { "": "告警", "center": "告警中心", - "setting": "阈值规则", + "setting": "閾值規則", + "silence": "告警靜默", "dispatch": "消息通知" }, "extras": { @@ -131,6 +132,19 @@ "alert.setting.target.other": "所屬行其它指標對象", "alert.setting.target.instance": "所屬行實例", "alert.setting.operator": "支持操作符函數", + "alert.silence.new": "新增靜默策略", + "alert.silence.edit": "編輯靜默策略", + "alert.silence.delete": "刪除靜默策略", + "alert.silence.name": "策略名稱", + "alert.silence.match-all": "應用所有", + "alert.silence.priority": "匹配級別", + "alert.silence.type.once": "一次性靜默", + "alert.silence.type.cyc": "週期性靜默", + "alert.silence.type": "靜默類型", + "alert.silence.tags": "匹配標籤", + "alert.silence.time": "靜默時段", + "alert.silence.times": "已靜默告警數", + "alert.silence.enable": "啟用靜默策略", "alert.center.delete": "刪除告警", "alert.center.clear": "一鍵清空", "alert.center.deal": "標記已處理",