Compare commits

...
Author SHA1 Message Date
Logic 7dcb4c3566 test(alerter): add mock for AlertSseManager in AlertNoticeDispatchTest
- Introduce mock object for AlertSseManager
- Inject the mock into AlertNoticeDispatch instance
- This change prepares for testing server-sent events (SSE) related functionality
2025-02-05 19:37:26 +08:00
Logic 206f256e96 test(alerter): add mock for AlertSseManager in AlertNoticeDispatchTest
- Introduce mock object for AlertSseManager
- Inject the mock into AlertNoticeDispatch instance
- This change prepares for testing server-sent events (SSE) related functionality
2025-02-05 18:40:13 +08:00
Logic 1c7ea61b59 Merge remote-tracking branch 'origin/alert-sse' into alert-sse 2025-02-05 17:36:20 +08:00
Logic 5932645422 refactor(alerter): add class comments for AlertSseController and AlertSseManager
- Added class comment for AlertSseController: "SSE controller for alert"
- Added class comment for AlertSseManager: "SSE manager for alert"
- Improved code readability and maintainability
2025-02-05 17:35:26 +08:00
Logic 6b7b9e0476 Merge branch 'master' into alert-sse 2025-02-05 17:31:29 +08:00
Logicandgithub-actions[bot] 9e482e985d Update hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Logic <zqr10159@126.com>
2025-02-05 17:25:42 +08:00
Logicandgithub-actions[bot] b49353a567 Update hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Logic <zqr10159@126.com>
2025-02-05 17:25:32 +08:00
Logic 2111280283 feat(alert): implement new alert animation and filtering
- Add slide-in animation for new alerts
- Implement alert filtering based on status and content
- Update alert list handling in component
- Modify alert store and dispatch logic
- Format alert timestamps in JSON response
2025-02-05 17:20:16 +08:00
Logic bb729dd33e feat(alerter): add logging before broadcasting alert
- Add log statement in AlertNoticeDispatch before broadcasting alert to SSE client
- Improve traceability and debugging for alert broadcasting process
2025-02-04 23:01:54 +08:00
Logic 95ee0d272f feat(alert): implement real-time alert updates using Server-Sent Events (SSE)
- Add SSE subscription to receive real-time alerts
- Update alert list with new data from SSE
- Handle SSE errors and close connection on component destruction- Remove refresh button as real-time updates eliminate the need for manual refreshing
2025-02-04 17:28:13 +08:00
Logic 5f1bf5ea95 feat(alerter): implement server-sent events (SSE) for real-time alert notifications
- Add AlertSseManager to handle SSE emitters and broadcast alerts
- Create AlertSseController to manage SSE connections
- Update AlertNoticeDispatch to send alerts via SSE
- Modify NotifyComponent to use SSE for real-time updates
- Update Manager to enable async support
2025-02-03 22:04:24 +08:00
15 changed files with 355 additions and 59 deletions
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}))));
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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()
@@ -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());
}
}
@@ -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
@@ -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() {