mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
11
Commits
pushgateway
...
alert-sse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dcb4c3566 | ||
|
|
206f256e96 | ||
|
|
1c7ea61b59 | ||
|
|
5932645422 | ||
|
|
6b7b9e0476 | ||
|
|
9e482e985d | ||
|
|
b49353a567 | ||
|
|
2111280283 | ||
|
|
bb729dd33e | ||
|
|
95ee0d272f | ||
|
|
5f1bf5ea95 |
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.config;
|
||||
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* SSE manager for alert
|
||||
*/
|
||||
@Component
|
||||
public class AlertSseManager {
|
||||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
||||
|
||||
public SseEmitter createEmitter(Long clientId) {
|
||||
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
|
||||
emitter.onCompletion(() -> removeEmitter(clientId));
|
||||
emitter.onTimeout(() -> removeEmitter(clientId));
|
||||
emitters.put(clientId, emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void broadcast(String data) {
|
||||
emitters.forEach((clientId, emitter) -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.id(String.valueOf(System.currentTimeMillis()))
|
||||
.name("ALERT_EVENT")
|
||||
.data(data));
|
||||
} catch (IOException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.controller;
|
||||
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
import org.apache.hertzbeat.alert.config.AlertSseManager;
|
||||
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* SSE controller for alert
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/alert/sse", produces = {TEXT_EVENT_STREAM_VALUE})
|
||||
public class AlertSseController {
|
||||
|
||||
private final AlertSseManager emitterManager;
|
||||
|
||||
public AlertSseController(AlertSseManager emitterManager) {
|
||||
this.emitterManager = emitterManager;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/subscribe")
|
||||
public SseEmitter subscribe() {
|
||||
Long clientId = SnowFlakeIdGenerator.generateId();
|
||||
return emitterManager.createEmitter(clientId);
|
||||
}
|
||||
}
|
||||
+20
-16
@@ -23,11 +23,13 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||
import org.apache.hertzbeat.alert.config.AlertSseManager;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.alert.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.plugin.PostAlertPlugin;
|
||||
import org.apache.hertzbeat.plugin.Plugin;
|
||||
import org.apache.hertzbeat.plugin.runner.PluginRunner;
|
||||
@@ -45,16 +47,18 @@ public class AlertNoticeDispatch {
|
||||
private final AlertStoreHandler alertStoreHandler;
|
||||
private final Map<Byte, AlertNotifyHandler> alertNotifyHandlerMap;
|
||||
private final PluginRunner pluginRunner;
|
||||
private final AlertSseManager emitterManager;
|
||||
|
||||
public AlertNoticeDispatch(AlerterWorkerPool workerPool,
|
||||
NoticeConfigService noticeConfigService,
|
||||
AlertStoreHandler alertStoreHandler,
|
||||
List<AlertNotifyHandler> alertNotifyHandlerList, PluginRunner pluginRunner) {
|
||||
List<AlertNotifyHandler> alertNotifyHandlerList, PluginRunner pluginRunner, AlertSseManager emitterManager) {
|
||||
this.workerPool = workerPool;
|
||||
this.noticeConfigService = noticeConfigService;
|
||||
this.alertStoreHandler = alertStoreHandler;
|
||||
this.pluginRunner = pluginRunner;
|
||||
alertNotifyHandlerMap = Maps.newHashMapWithExpectedSize(alertNotifyHandlerList.size());
|
||||
this.emitterManager = emitterManager;
|
||||
alertNotifyHandlerList.forEach(r -> alertNotifyHandlerMap.put(r.type(), r));
|
||||
}
|
||||
|
||||
@@ -104,27 +108,27 @@ public class AlertNoticeDispatch {
|
||||
public void dispatchAlarm(GroupAlert groupAlert) {
|
||||
if (groupAlert != null) {
|
||||
// Determining alarm type storage
|
||||
alertStoreHandler.store(groupAlert);
|
||||
GroupAlert storedGroupAlert = alertStoreHandler.store(groupAlert);
|
||||
// Notice distribution
|
||||
sendNotify(groupAlert);
|
||||
sendNotify(storedGroupAlert);
|
||||
// Execute the plugin if enable (Compatible with old version plugins, will be removed in later versions)
|
||||
pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(groupAlert));
|
||||
pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(storedGroupAlert));
|
||||
// Execute the plugin if enable with params
|
||||
pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(groupAlert, pluginContext));
|
||||
pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(storedGroupAlert, pluginContext));
|
||||
// Send alert to the sse client
|
||||
emitterManager.broadcast(JsonUtil.toJson(storedGroupAlert));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendNotify(GroupAlert alert) {
|
||||
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> {
|
||||
workerPool.executeNotify(() -> rule.getReceiverId()
|
||||
.forEach(receiverId -> {
|
||||
try {
|
||||
sendNoticeMsg(getOneReceiverById(receiverId),
|
||||
getOneTemplateById(rule.getTemplateId()), alert);
|
||||
} catch (AlertNoticeException e) {
|
||||
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
|
||||
}
|
||||
}));
|
||||
}));
|
||||
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> workerPool.executeNotify(() -> rule.getReceiverId()
|
||||
.forEach(receiverId -> {
|
||||
try {
|
||||
sendNoticeMsg(getOneReceiverById(receiverId),
|
||||
getOneTemplateById(rule.getTemplateId()), alert);
|
||||
} catch (AlertNoticeException e) {
|
||||
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
|
||||
}
|
||||
}))));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@ public interface AlertStoreHandler {
|
||||
* Persistent alarm records
|
||||
* It is necessary to associate and assign values
|
||||
* to the alert tag information tags while persisting.
|
||||
*
|
||||
* @param alert alarm information
|
||||
* @return groupAlert
|
||||
*/
|
||||
void store(GroupAlert alert);
|
||||
GroupAlert store(GroupAlert alert);
|
||||
}
|
||||
|
||||
+26
-14
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.notice.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,44 +47,53 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
|
||||
private final SingleAlertDao singleAlertDao;
|
||||
|
||||
@Override
|
||||
public void store(GroupAlert groupAlert) {
|
||||
public GroupAlert store(GroupAlert groupAlert) {
|
||||
if (groupAlert == null || groupAlert.getAlerts() == null || groupAlert.getAlerts().isEmpty()) {
|
||||
log.error("The Group Alerts is empty, ignore store");
|
||||
return;
|
||||
return groupAlert;
|
||||
}
|
||||
// 1. Find existing alert group
|
||||
GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey());
|
||||
|
||||
// 2. Process individual alerts
|
||||
Set<String> alertFingerprints = new HashSet<>(8);
|
||||
groupAlert.getAlerts().forEach(singleAlert -> {
|
||||
|
||||
List<SingleAlert> originalAlerts = groupAlert.getAlerts();
|
||||
List<SingleAlert> newAlerts = new ArrayList<>();
|
||||
|
||||
|
||||
for (SingleAlert singleAlert : originalAlerts) {
|
||||
SingleAlert existAlert = singleAlertDao.findByFingerprint(singleAlert.getFingerprint());
|
||||
|
||||
if (existAlert != null) {
|
||||
// Update existing alert
|
||||
// Update the existing alert with the ID and creation time from the database
|
||||
singleAlert.setId(existAlert.getId());
|
||||
singleAlert.setGmtCreate(existAlert.getGmtCreate());
|
||||
|
||||
|
||||
// Status transition logic
|
||||
if (CommonConstants.ALERT_STATUS_FIRING.equals(singleAlert.getStatus())) {
|
||||
// If the alert is firing and the existing alert is not resolved, update the start time and trigger times
|
||||
if (!CommonConstants.ALERT_STATUS_RESOLVED.equals(existAlert.getStatus())) {
|
||||
singleAlert.setStartAt(existAlert.getStartAt());
|
||||
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1) + Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
|
||||
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1)
|
||||
+ Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
|
||||
singleAlert.setTriggerTimes(triggerTimes);
|
||||
}
|
||||
}
|
||||
} else if (CommonConstants.ALERT_STATUS_RESOLVED.equals(singleAlert.getStatus())) {
|
||||
// Transition to resolved state
|
||||
// If the alert is resolved, set the end time (if not already set) and copy other fields from the existing alert
|
||||
if (singleAlert.getEndAt() == null) {
|
||||
singleAlert.setEndAt(System.currentTimeMillis());
|
||||
singleAlert.setEndAt(System.currentTimeMillis());
|
||||
}
|
||||
singleAlert.setStartAt(existAlert.getStartAt());
|
||||
singleAlert.setActiveAt(existAlert.getActiveAt());
|
||||
singleAlert.setTriggerTimes(existAlert.getTriggerTimes());
|
||||
}
|
||||
}
|
||||
alertFingerprints.add(singleAlert.getFingerprint());
|
||||
singleAlertDao.save(singleAlert);
|
||||
});
|
||||
|
||||
SingleAlert savedSingleAlert = singleAlertDao.save(singleAlert);
|
||||
newAlerts.add(savedSingleAlert);
|
||||
alertFingerprints.add(savedSingleAlert.getFingerprint());
|
||||
}
|
||||
groupAlert.setAlerts(newAlerts);
|
||||
// 3. Process resolved alerts
|
||||
if (existGroupAlert != null) {
|
||||
List<String> existFingerprints = existGroupAlert.getAlertFingerprints();
|
||||
@@ -120,6 +130,8 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
|
||||
|
||||
// 4. Save alert group
|
||||
groupAlert.setAlertFingerprints(alertFingerprints.stream().toList());
|
||||
groupAlertDao.save(groupAlert);
|
||||
GroupAlert savedGroupAlert = groupAlertDao.save(groupAlert);
|
||||
savedGroupAlert.setAlerts(groupAlert.getAlerts());
|
||||
return savedGroupAlert;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||
import org.apache.hertzbeat.alert.config.AlertSseManager;
|
||||
import org.apache.hertzbeat.alert.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
@@ -60,6 +61,9 @@ class AlertNoticeDispatchTest {
|
||||
@Mock
|
||||
private AlertNotifyHandler alertNotifyHandler;
|
||||
|
||||
@Mock
|
||||
private AlertSseManager emitterManager;
|
||||
|
||||
private AlertNoticeDispatch alertNoticeDispatch;
|
||||
|
||||
private static final int DISPATCH_THREADS = 3;
|
||||
@@ -77,7 +81,8 @@ class AlertNoticeDispatchTest {
|
||||
noticeConfigService,
|
||||
alertStoreHandler,
|
||||
alertNotifyHandlerList,
|
||||
pluginRunner
|
||||
pluginRunner,
|
||||
emitterManager
|
||||
);
|
||||
|
||||
receiver = NoticeReceiver.builder()
|
||||
|
||||
+19
-8
@@ -73,27 +73,34 @@ class DbAlertStoreHandlerImplTest {
|
||||
public void testStoreNewAlert() {
|
||||
String groupKey = "test-group";
|
||||
groupAlert.setGroupKey(groupKey);
|
||||
|
||||
|
||||
when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(null);
|
||||
|
||||
|
||||
SingleAlert savedSingleAlert = new SingleAlert();
|
||||
when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(savedSingleAlert);
|
||||
|
||||
GroupAlert savedGroupAlert = new GroupAlert();
|
||||
when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(savedGroupAlert);
|
||||
|
||||
dbAlertStoreHandler.store(groupAlert);
|
||||
|
||||
|
||||
verify(singleAlertDao).save(any(SingleAlert.class));
|
||||
verify(groupAlertDao).save(groupAlert);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testStoreExistingAlert() {
|
||||
String groupKey = "test-group";
|
||||
String fingerprint = "test-fingerprint";
|
||||
|
||||
|
||||
groupAlert.setGroupKey(groupKey);
|
||||
singleAlert.setFingerprint(fingerprint);
|
||||
|
||||
|
||||
GroupAlert existingGroup = new GroupAlert();
|
||||
existingGroup.setId(1L);
|
||||
when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(existingGroup);
|
||||
|
||||
|
||||
SingleAlert existingAlert = new SingleAlert();
|
||||
existingAlert.setId(1L);
|
||||
existingAlert.setStatus("firing");
|
||||
@@ -101,11 +108,15 @@ class DbAlertStoreHandlerImplTest {
|
||||
existingAlert.setActiveAt(2000L);
|
||||
existingAlert.setTriggerTimes(1);
|
||||
when(singleAlertDao.findByFingerprint(fingerprint)).thenReturn(existingAlert);
|
||||
|
||||
|
||||
when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(existingAlert);
|
||||
when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(existingGroup);
|
||||
|
||||
dbAlertStoreHandler.store(groupAlert);
|
||||
|
||||
|
||||
verify(singleAlertDao).save(any(SingleAlert.class));
|
||||
verify(groupAlertDao).save(groupAlert);
|
||||
assertEquals(1L, groupAlert.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -18,6 +18,7 @@
|
||||
package org.apache.hertzbeat.common.entity.alerter;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
@@ -97,10 +98,12 @@ public class GroupAlert {
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)")
|
||||
@CreatedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
|
||||
@LastModifiedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime gmtUpdate;
|
||||
|
||||
@Transient
|
||||
|
||||
+3
@@ -18,6 +18,7 @@
|
||||
package org.apache.hertzbeat.common.entity.alerter;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
@@ -100,10 +101,12 @@ public class SingleAlert {
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)")
|
||||
@CreatedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
|
||||
@LastModifiedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime gmtUpdate;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* start up class.
|
||||
@@ -39,6 +40,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
@ComponentScan(basePackages = {"org.apache.hertzbeat"})
|
||||
@ConfigurationPropertiesScan(basePackages = {"org.apache.hertzbeat"})
|
||||
@ImportRuntimeHints(HertzbeatRuntimeHintsRegistrar.class)
|
||||
@EnableAsync
|
||||
public class Manager {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Manager.class, args);
|
||||
|
||||
@@ -72,6 +72,7 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alerts/report/**===*
|
||||
- /api/alert/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NzNotificationService } from 'ng-zorro-antd/notification';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
import { Mute } from '../../../pojo/Mute';
|
||||
import { SingleAlert } from '../../../pojo/SingleAlert';
|
||||
import { AlertSoundService } from '../../../service/alert-sound.service';
|
||||
import { AlertService } from '../../../service/alert.service';
|
||||
import { GeneralConfigService } from '../../../service/general-config.service';
|
||||
@@ -122,6 +123,7 @@ export class HeaderNotifyComponent implements OnInit, OnDestroy {
|
||||
private previousCount = 0;
|
||||
// default to mute status
|
||||
mute: Mute = { mute: true };
|
||||
private eventSource!: EventSource;
|
||||
constructor(
|
||||
private router: Router,
|
||||
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
|
||||
@@ -154,9 +156,7 @@ export class HeaderNotifyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
this.loadData();
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadData();
|
||||
}, 10000); // every 10 seconds refresh the tabs
|
||||
this.initSSEConnection();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -217,11 +217,6 @@ export class HeaderNotifyComponent implements OnInit, OnDestroy {
|
||||
list.push(item);
|
||||
});
|
||||
this.data = this.updateNoticeData(list);
|
||||
|
||||
if (page.totalElements > this.previousCount && !this.mute.mute) {
|
||||
this.alertSound.playAlertSound(this.i18nSvc.currentLang);
|
||||
}
|
||||
this.previousCount = page.totalElements;
|
||||
this.count = page.totalElements;
|
||||
} else {
|
||||
console.warn(message.msg);
|
||||
@@ -291,4 +286,36 @@ export class HeaderNotifyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initSSEConnection(): void {
|
||||
const sseUrl = '/api/alert/sse/subscribe';
|
||||
|
||||
this.eventSource = new EventSource(sseUrl);
|
||||
|
||||
this.eventSource.addEventListener('ALERT_EVENT', (evt: MessageEvent) => {
|
||||
let list: any[] = [];
|
||||
let alert: SingleAlert = JSON.parse(evt.data);
|
||||
let item = {
|
||||
id: alert.id,
|
||||
avatar: '/assets/img/notification.svg',
|
||||
// title: `${alert.tags?.monitorName}--${this.i18nSvc.fanyi(`alert.severity.${alert.severity}`)}`,
|
||||
title: alert.content,
|
||||
datetime: new Date(alert.activeAt).toLocaleString(),
|
||||
color: 'blue',
|
||||
status: alert.status,
|
||||
type: this.i18nSvc.fanyi('dashboard.alerts.title-no')
|
||||
};
|
||||
list.push(item);
|
||||
|
||||
this.data = this.updateNoticeData(list);
|
||||
if (!this.mute.mute) {
|
||||
this.alertSound.playAlertSound(this.i18nSvc.currentLang);
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
this.eventSource.onerror = error => {
|
||||
console.error('SSE connection error:', error);
|
||||
setTimeout(() => this.initSSEConnection(), 3000);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
<app-toolbar>
|
||||
<ng-template #center>
|
||||
<div class="center-content">
|
||||
<button nz-button (click)="sync()" nz-tooltip [nzTooltipTitle]="'common.refresh' | i18n">
|
||||
<i nz-icon nzType="sync" nzTheme="outline"></i>
|
||||
</button>
|
||||
|
||||
<div class="search-wrapper">
|
||||
<nz-input-group [nzPrefix]="prefixTemplate" class="search-input">
|
||||
<input
|
||||
@@ -63,7 +59,13 @@
|
||||
</app-toolbar>
|
||||
|
||||
<div class="alert-cards">
|
||||
<nz-card *ngFor="let group of groupAlerts" class="alert-card" [class]="'status-' + group.status" [nzBordered]="false">
|
||||
<nz-card
|
||||
*ngFor="let group of groupAlerts"
|
||||
class="alert-card"
|
||||
[class.new-alert]="group.isNew"
|
||||
[class]="'status-' + group.status"
|
||||
[nzBordered]="false"
|
||||
>
|
||||
<!-- Alert Group Header -->
|
||||
<div class="alert-header">
|
||||
<div class="alert-info">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* 调整工具栏布局 */
|
||||
:host ::ng-deep app-toolbar {
|
||||
.center-content {
|
||||
display: flex;
|
||||
@@ -282,3 +281,23 @@
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
position: relative;
|
||||
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
|
||||
|
||||
&.new-alert {
|
||||
animation: slideInFromRight 0.5s ease-out forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { I18NService } from '@core';
|
||||
import { ALAIN_I18N_TOKEN } from '@delon/theme';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
@@ -26,12 +26,15 @@ import { NzNotificationService } from 'ng-zorro-antd/notification';
|
||||
import { GroupAlert } from '../../../pojo/GroupAlert';
|
||||
import { AlertService } from '../../../service/alert.service';
|
||||
|
||||
interface ExtendedGroupAlert extends GroupAlert {
|
||||
isNew?: boolean;
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-alert-center',
|
||||
templateUrl: './alert-center.component.html',
|
||||
styleUrl: './alert-center.component.less'
|
||||
})
|
||||
export class AlertCenterComponent implements OnInit {
|
||||
export class AlertCenterComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private notifySvc: NzNotificationService,
|
||||
private modal: NzModalService,
|
||||
@@ -42,18 +45,109 @@ export class AlertCenterComponent implements OnInit {
|
||||
pageIndex: number = 1;
|
||||
pageSize: number = 8;
|
||||
total: number = 0;
|
||||
groupAlerts!: GroupAlert[];
|
||||
groupAlerts: ExtendedGroupAlert[] = [];
|
||||
tableLoading: boolean = false;
|
||||
checkedAlertIds = new Set<number>();
|
||||
filterStatus!: string;
|
||||
filterContent: string | undefined;
|
||||
private eventSource!: EventSource;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadAlertsTable();
|
||||
this.initSSESubscription();
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.loadAlertsTable();
|
||||
ngOnDestroy(): void {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize SSE subscription for real-time alerts
|
||||
private initSSESubscription(): void {
|
||||
this.eventSource = new EventSource('/api/alert/sse/subscribe');
|
||||
this.eventSource.addEventListener('ALERT_EVENT', (evt: MessageEvent) => {
|
||||
try {
|
||||
const newAlert: GroupAlert = JSON.parse(evt.data);
|
||||
this.updateAlertList(newAlert);
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle SSE errors
|
||||
this.eventSource.onerror = error => {
|
||||
console.error('SSE connection error:', error);
|
||||
this.eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
private updateAlertList(newAlert: GroupAlert): void {
|
||||
const extendedAlert: ExtendedGroupAlert = {
|
||||
...newAlert,
|
||||
isNew: true
|
||||
};
|
||||
|
||||
if (!extendedAlert.alerts) {
|
||||
extendedAlert.alerts = [];
|
||||
}
|
||||
|
||||
const matchesFilter = this.checkAlertMatchesFilter(extendedAlert);
|
||||
if (!matchesFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIndex = this.groupAlerts.findIndex(a => a.id === extendedAlert.id);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
this.groupAlerts = [extendedAlert, ...this.groupAlerts];
|
||||
this.total += 1;
|
||||
|
||||
setTimeout(() => {
|
||||
const index = this.groupAlerts.findIndex(a => a.id === extendedAlert.id);
|
||||
if (index !== -1) {
|
||||
this.groupAlerts[index].isNew = false;
|
||||
// 触发变更检测
|
||||
this.groupAlerts = [...this.groupAlerts];
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
this.groupAlerts[existingIndex] = {
|
||||
...extendedAlert,
|
||||
isNew: true
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.groupAlerts[existingIndex]) {
|
||||
this.groupAlerts[existingIndex].isNew = false;
|
||||
this.groupAlerts = [...this.groupAlerts];
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.groupAlerts = [...this.groupAlerts];
|
||||
}
|
||||
}
|
||||
|
||||
private checkAlertMatchesFilter(alert: ExtendedGroupAlert): boolean {
|
||||
if (this.filterStatus && alert.status !== this.filterStatus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.filterContent) {
|
||||
const searchContent = this.filterContent.toLowerCase();
|
||||
|
||||
const hasMatchingContent = alert.alerts?.some(singleAlert => singleAlert.content?.toLowerCase().includes(searchContent));
|
||||
|
||||
const hasMatchingLabels = Object.entries(alert.groupLabels || {}).some(
|
||||
([key, value]) => key.toLowerCase().includes(searchContent) || value.toLowerCase().includes(searchContent)
|
||||
);
|
||||
|
||||
if (!hasMatchingContent && !hasMatchingLabels) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadAlertsTable() {
|
||||
|
||||
Reference in New Issue
Block a user