This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 6fec531cf2 [feature] support exporting alerts to Excel (#4332)
6fec531cf2 is described below
commit 6fec531cf2ff3f41e013541f9801bb9f3e4161f6
Author: NekoPunch <[email protected]>
AuthorDate: Sat Aug 22 21:56:28 2026 -0700
[feature] support exporting alerts to Excel (#4332)
Co-authored-by: aias00 <[email protected]>
---
hertzbeat-alerter/pom.xml | 5 ++
.../alert/controller/AlertsController.java | 12 ++++
.../hertzbeat/alert/dto/SingleAlertExportDTO.java | 55 ++++++++++++++++
.../hertzbeat/alert/service/AlertService.java | 13 +++-
.../alert/service/impl/AlertServiceImpl.java | 73 ++++++++++++++++++++--
.../alert/alert-center/alert-center.component.html | 5 ++
.../alert/alert-center/alert-center.component.ts | 34 +++++++++-
web-app/src/app/service/alert.service.ts | 18 +++++-
web-app/src/assets/i18n/en-US.json | 1 +
web-app/src/assets/i18n/ja-JP.json | 1 +
web-app/src/assets/i18n/ko-KR.json | 1 +
web-app/src/assets/i18n/pt-BR.json | 1 +
web-app/src/assets/i18n/zh-CN.json | 1 +
web-app/src/assets/i18n/zh-TW.json | 1 +
14 files changed, 214 insertions(+), 7 deletions(-)
diff --git a/hertzbeat-alerter/pom.xml b/hertzbeat-alerter/pom.xml
index c3a3890d2c..dbaf128b48 100644
--- a/hertzbeat-alerter/pom.xml
+++ b/hertzbeat-alerter/pom.xml
@@ -108,6 +108,11 @@
<version>${easy-poi.version}</version>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>cn.afterturn</groupId>
+ <artifactId>easypoi-base</artifactId>
+ <version>${easy-poi.version}</version>
+ </dependency>
<dependency>
<groupId>com.huaweicloud.sdk</groupId>
<artifactId>huaweicloud-sdk-smn</artifactId>
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
index d3b49575ca..4ffd7bb313 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java
@@ -21,6 +21,7 @@ import static
org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.List;
import org.apache.hertzbeat.alert.dto.AlertSummary;
@@ -63,6 +64,17 @@ public class AlertsController {
return ResponseEntity.ok(Message.success(alertPage));
}
+ @GetMapping("/export")
+ @Operation(summary = "Export Alarms", description = "Export single alarms
matching the filters as an Excel sheet")
+ public void exportAlerts(
+ @Parameter(description = "Alarm Status", example = "resolved")
@RequestParam(required = false) String status,
+ @Parameter(description = "Alarm content fuzzy query", example =
"linux") @RequestParam(required = false) String search,
+ @Parameter(description = "Sort field, default activeAt", example =
"activeAt") @RequestParam(defaultValue = "activeAt") String sort,
+ @Parameter(description = "Sort Type", example = "desc")
@RequestParam(defaultValue = "desc") String order,
+ HttpServletResponse response) {
+ alertService.exportSingleAlerts(status, search, sort, order, response);
+ }
+
@GetMapping("/group")
@Operation(summary = "Query Group Alarms")
public ResponseEntity<Message<Page<GroupAlert>>> getGroupAlerts(
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SingleAlertExportDTO.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SingleAlertExportDTO.java
new file mode 100644
index 0000000000..c785ad5393
--- /dev/null
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/SingleAlertExportDTO.java
@@ -0,0 +1,55 @@
+/*
+ * 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.dto;
+
+import cn.afterturn.easypoi.excel.annotation.Excel;
+import lombok.Data;
+
+/**
+ * A SingleAlert with its Map and timestamp fields pre-rendered to strings,
since easypoi cannot map those to cells.
+ */
+@Data
+public class SingleAlertExportDTO {
+
+ @Excel(name = "Status", width = 12)
+ private String status;
+
+ @Excel(name = "Content", width = 60)
+ private String content;
+
+ @Excel(name = "Labels", width = 40)
+ private String labels;
+
+ @Excel(name = "Annotations", width = 40)
+ private String annotations;
+
+ @Excel(name = "Fingerprint", width = 24)
+ private String fingerprint;
+
+ @Excel(name = "Trigger Times", width = 12)
+ private Integer triggerTimes;
+
+ @Excel(name = "Start At", width = 20)
+ private String startAt;
+
+ @Excel(name = "Active At", width = 20)
+ private String activeAt;
+
+ @Excel(name = "End At", width = 20)
+ private String endAt;
+}
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
index ea5c9bb9a2..684b900592 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.alert.service;
+import jakarta.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.List;
import org.apache.hertzbeat.alert.dto.AlertSummary;
@@ -40,7 +41,17 @@ public interface AlertService {
* @return single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String
sort, String order, int pageIndex, int pageSize);
-
+
+ /**
+ * export single alerts matching the filters to an Excel sheet
+ * @param status status
+ * @param search search
+ * @param sort sort
+ * @param order order
+ * @param response servlet response the Excel sheet is written to
+ */
+ void exportSingleAlerts(String status, String search, String sort, String
order, HttpServletResponse response);
+
/**
* Dynamic conditional query
* @param status Alarm Status
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
index 80bb95258b..98c3dabeed 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java
@@ -17,21 +17,33 @@
package org.apache.hertzbeat.alert.service.impl;
+import cn.afterturn.easypoi.excel.ExcelExportUtil;
+import cn.afterturn.easypoi.excel.entity.ExportParams;
+import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import jakarta.persistence.criteria.Predicate;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.GroupAlertDao;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.dto.AlertSummary;
+import org.apache.hertzbeat.alert.dto.SingleAlertExportDTO;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -47,7 +59,9 @@ import
org.springframework.transaction.annotation.Transactional;
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class AlertServiceImpl implements AlertService {
-
+
+ private static final DateTimeFormatter EXPORT_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
@Autowired
private GroupAlertDao groupAlertDao;
@@ -59,7 +73,13 @@ public class AlertServiceImpl implements AlertService {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search,
String sort, String order, int pageIndex, int pageSize) {
- Specification<SingleAlert> specification = (root, query,
criteriaBuilder) -> {
+ Sort sortExp = Sort.by(new
Sort.Order(Sort.Direction.fromString(order), sort));
+ PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
+ return singleAlertDao.findAll(buildSingleAlertSpecification(status,
search), pageRequest);
+ }
+
+ private Specification<SingleAlert> buildSingleAlertSpecification(String
status, String search) {
+ return (root, query, criteriaBuilder) -> {
List<Predicate> andList = new ArrayList<>();
if (status != null) {
Predicate predicate =
criteriaBuilder.equal(root.get("status"), status);
@@ -88,9 +108,54 @@ public class AlertServiceImpl implements AlertService {
return query.where(andPredicate, orPredicate).getRestriction();
}
};
+ }
+
+ @Override
+ public void exportSingleAlerts(String status, String search, String sort,
String order, HttpServletResponse response) {
Sort sortExp = Sort.by(new
Sort.Order(Sort.Direction.fromString(order), sort));
- PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
- return singleAlertDao.findAll(specification, pageRequest);
+ List<SingleAlert> alerts =
singleAlertDao.findAll(buildSingleAlertSpecification(status, search), sortExp);
+ // easypoi mutates the list in place, so it must be mutable (not
Stream.toList()).
+ List<SingleAlertExportDTO> rows =
alerts.stream().map(this::toExportRow).collect(Collectors.toList());
+ try (Workbook workbook = ExcelExportUtil.exportExcel(
+ new ExportParams("Alert Records", "Alerts", ExcelType.XSSF),
SingleAlertExportDTO.class, rows)) {
+ String fileName = "hertzbeat_alerts_" + System.currentTimeMillis()
+ ".xlsx";
+
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setHeader("Content-Disposition", "attachment;filename=" +
fileName);
+ workbook.write(response.getOutputStream());
+ } catch (IOException e) {
+ log.error("export alerts to excel error: {}", e.getMessage(), e);
+ throw new RuntimeException("Failed to export alerts", e);
+ }
+ }
+
+ private SingleAlertExportDTO toExportRow(SingleAlert alert) {
+ SingleAlertExportDTO row = new SingleAlertExportDTO();
+ row.setStatus(alert.getStatus());
+ row.setContent(alert.getContent());
+ row.setLabels(mapToString(alert.getLabels()));
+ row.setAnnotations(mapToString(alert.getAnnotations()));
+ row.setFingerprint(alert.getFingerprint());
+ row.setTriggerTimes(alert.getTriggerTimes());
+ row.setStartAt(formatEpochMilli(alert.getStartAt()));
+ row.setActiveAt(formatEpochMilli(alert.getActiveAt()));
+ row.setEndAt(formatEpochMilli(alert.getEndAt()));
+ return row;
+ }
+
+ private String mapToString(Map<String, String> map) {
+ if (map == null || map.isEmpty()) {
+ return "";
+ }
+ return map.entrySet().stream()
+ .map(entry -> entry.getKey() + "=" + entry.getValue())
+ .collect(Collectors.joining("; "));
+ }
+
+ private String formatEpochMilli(Long epochMilli) {
+ if (epochMilli == null) {
+ return "";
+ }
+ return
EXPORT_TIME_FORMATTER.format(Instant.ofEpochMilli(epochMilli).atZone(ZoneId.systemDefault()));
}
@Override
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 1aca0a8e14..2c9024fd0b 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
@@ -54,6 +54,11 @@
<nz-option [nzLabel]="'alert.status.firing' | i18n"
[nzValue]="'firing'"></nz-option>
<nz-option [nzLabel]="'alert.status.resolved' | i18n"
[nzValue]="'resolved'"></nz-option>
</nz-select>
+
+ <button nz-button [nzLoading]="exportButtonLoading"
(click)="exportAlerts()">
+ <i nz-icon nzType="download" nzTheme="outline"></i>
+ {{ 'alert.center.export' | i18n }}
+ </button>
</div>
</ng-template>
</app-toolbar>
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 6c94f3e184..d3d257d9a8 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
@@ -22,7 +22,7 @@ import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
-import { Subscription } from 'rxjs';
+import { finalize, Subscription } from 'rxjs';
import { GroupAlert } from '../../../pojo/GroupAlert';
import { AlertService } from '../../../service/alert.service';
@@ -54,6 +54,7 @@ export class AlertCenterComponent implements OnInit,
OnDestroy {
checkedAlertIds = new Set<number>();
filterStatus!: string;
filterContent: string | undefined;
+ exportButtonLoading: boolean = false;
private alertStream$!: Subscription;
ngOnInit(): void {
@@ -298,4 +299,35 @@ export class AlertCenterComponent implements OnInit,
OnDestroy {
}
);
}
+
+ exportAlerts() {
+ this.exportButtonLoading = true;
+ const exportAlerts$ = this.alertSvc
+ .exportAlerts(this.filterStatus, this.filterContent)
+ .pipe(
+ finalize(() => {
+ this.exportButtonLoading = false;
+ exportAlerts$.unsubscribe();
+ })
+ )
+ .subscribe(
+ response => {
+ const body = response.body!;
+ if (body.type == 'application/json') {
+
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), '');
+ } else {
+ const blob = new Blob([body], { type:
response.headers.get('Content-Type')! });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.download =
response.headers.get('Content-Disposition')!.split(';')[1].split('filename=')[1];
+ a.href = url;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+ },
+ error => {
+
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'),
error.msg);
+ }
+ );
+ }
}
diff --git a/web-app/src/app/service/alert.service.ts
b/web-app/src/app/service/alert.service.ts
index 67dff8238b..2ca690eb0a 100644
--- a/web-app/src/app/service/alert.service.ts
+++ b/web-app/src/app/service/alert.service.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { HttpClient, HttpParams } from '@angular/common/http';
+import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@@ -30,6 +30,7 @@ const alerts_summary_uri = '/alerts/summary';
const alerts_group_uri = '/alerts/group';
const alerts_group_status_uri = '/alerts/group/status';
const alerts_uri = '/alerts';
+const alerts_export_uri = '/alerts/export';
@Injectable({
providedIn: 'root'
@@ -89,6 +90,21 @@ export class AlertService {
return this.http.get<Message<Page<GroupAlert>>>(alerts_group_uri, options);
}
+ public exportAlerts(status: string | undefined, search: string | undefined):
Observable<HttpResponse<Blob>> {
+ let httpParams = new HttpParams();
+ if (status != undefined) {
+ httpParams = httpParams.append('status', status);
+ }
+ if (search != undefined && search != '' && search.trim() != '') {
+ httpParams = httpParams.append('search', search.trim());
+ }
+ return this.http.get(alerts_export_uri, {
+ params: httpParams,
+ observe: 'response',
+ responseType: 'blob'
+ });
+ }
+
public deleteGroupAlerts(alertIds: Set<number>): Observable<Message<any>> {
let httpParams = new HttpParams();
alertIds.forEach(alertId => {
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index 8d770b5394..078d296a34 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -27,6 +27,7 @@
"alert.center.content": "Alert Content",
"alert.center.deal": "Mark Processed",
"alert.center.delete": "Delete Alerts",
+ "alert.center.export": "Export Alerts",
"alert.center.end-time": "End Time",
"alert.center.filter-priority": "Alert Priority",
"alert.center.filter-status": "Alert Status",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index 36ffd2b42b..77002873ea 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -27,6 +27,7 @@
"alert.center.content": "アラート内容",
"alert.center.deal": "処理済みにマーク",
"alert.center.delete": "アラートを削除",
+ "alert.center.export": "アラートをエクスポート",
"alert.center.end-time": "終了時間",
"alert.center.filter-priority": "アラート優先度",
"alert.center.filter-status": "アラートステータス",
diff --git a/web-app/src/assets/i18n/ko-KR.json
b/web-app/src/assets/i18n/ko-KR.json
index df7b93bcb9..0dd23716b5 100644
--- a/web-app/src/assets/i18n/ko-KR.json
+++ b/web-app/src/assets/i18n/ko-KR.json
@@ -27,6 +27,7 @@
"alert.center.content": "알람 내용",
"alert.center.deal": "처리 완료로 표시",
"alert.center.delete": "알람 삭제",
+ "alert.center.export": "알람 내보내기",
"alert.center.end-time": "종료 시간",
"alert.center.filter-priority": "알람 우선순위",
"alert.center.filter-status": "알람 상태",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index 851d45ae89..468fcee0c0 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -102,6 +102,7 @@
"alert.converge.eval-interval": "Intervalo de Convergência de Repetição de
Alerta (s)",
"alert.converge.enable": "Habilitar Convergência",
"alert.center.delete": "Excluir Alertas",
+ "alert.center.export": "Exportar Alertas",
"alert.center.clear": "Limpar Tudo",
"alert.center.deal": "Marcar como Processado",
"alert.center.no-deal": "Marcar como Pendente",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 08bb0eb2ed..ec3b26382d 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -27,6 +27,7 @@
"alert.center.content": "告警内容",
"alert.center.deal": "标记已处理",
"alert.center.delete": "删除告警",
+ "alert.center.export": "导出告警",
"alert.center.end-time": "结束",
"alert.center.filter-priority": "告警级别",
"alert.center.filter-status": "告警状态",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index 9cc7a00dbb..7c389d746e 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -27,6 +27,7 @@
"alert.center.content": "告警內容",
"alert.center.deal": "標記已處理",
"alert.center.delete": "刪除告警",
+ "alert.center.export": "匯出告警",
"alert.center.end-time": "结束",
"alert.center.filter-priority": "告警級別",
"alert.center.filter-status": "告警狀態",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]