You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@hertzbeat.apache.org by zh...@apache.org on 2025/02/03 14:06:27 UTC
(hertzbeat) branch alert-sse created (now 5f1bf5ea95)
This is an automated email from the ASF dual-hosted git repository.
zhaoqingran pushed a change to branch alert-sse
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
at 5f1bf5ea95 feat(alerter): implement server-sent events (SSE) for real-time alert notifications
This branch includes the following new commits:
new 5f1bf5ea95 feat(alerter): implement server-sent events (SSE) for real-time alert notifications
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org
(hertzbeat) 01/01: feat(alerter): implement server-sent events (SSE) for real-time alert notifications
Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
zhaoqingran pushed a commit to branch alert-sse
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 5f1bf5ea9539565017e030140bbe397e3c8e90d6
Author: Logic <zq...@dromara.org>
AuthorDate: Mon Feb 3 22:04:24 2025 +0800
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
---
.../hertzbeat/alert/config/AlertSseManager.java | 41 +++++++++++++++++++++
.../alert/controller/AlertSseController.java | 28 ++++++++++++++
.../alert/notice/AlertNoticeDispatch.java | 8 +++-
.../java/org/apache/hertzbeat/manager/Manager.java | 2 +
hertzbeat-manager/src/main/resources/sureness.yml | 1 +
.../app/layout/basic/widgets/notify.component.ts | 43 ++++++++++++++++++----
6 files changed, 114 insertions(+), 9 deletions(-)
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java
new file mode 100644
index 0000000000..635154a971
--- /dev/null
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/AlertSseManager.java
@@ -0,0 +1,41 @@
+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;
+
+@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);
+ }
+}
\ No newline at end of file
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java
new file mode 100644
index 0000000000..c38bfd163e
--- /dev/null
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSseController.java
@@ -0,0 +1,28 @@
+package org.apache.hertzbeat.alert.controller;
+
+import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
+import java.util.UUID;
+import org.apache.hertzbeat.alert.config.AlertSseManager;
+import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.common.util.SnowFlakeIdWorker;
+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;
+
+@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);
+ }
+}
\ No newline at end of file
diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
index be790dd8a9..1d7b568219 100644
--- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
+++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java
@@ -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));
}
@@ -111,6 +115,8 @@ public class AlertNoticeDispatch {
pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(groupAlert));
// Execute the plugin if enable with params
pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(groupAlert, pluginContext));
+ // Send alarm information to the client
+ emitterManager.broadcast(JsonUtil.toJson(groupAlert));
}
}
diff --git a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/Manager.java b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/Manager.java
index e9aac8c0c0..ce10a4e51b 100644
--- a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/Manager.java
+++ b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/Manager.java
@@ -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);
diff --git a/hertzbeat-manager/src/main/resources/sureness.yml b/hertzbeat-manager/src/main/resources/sureness.yml
index 9ebd316af1..b2c394f46d 100644
--- a/hertzbeat-manager/src/main/resources/sureness.yml
+++ b/hertzbeat-manager/src/main/resources/sureness.yml
@@ -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
diff --git a/web-app/src/app/layout/basic/widgets/notify.component.ts b/web-app/src/app/layout/basic/widgets/notify.component.ts
index acd5c1ebf2..f1f55781ff 100644
--- a/web-app/src/app/layout/basic/widgets/notify.component.ts
+++ b/web-app/src/app/layout/basic/widgets/notify.component.ts
@@ -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 | undefined;
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);
+ };
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@hertzbeat.apache.org
For additional commands, e-mail: notifications-help@hertzbeat.apache.org