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]

Reply via email to