mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[feature] support exporting alerts to Excel (#4332)
Co-authored-by: aias00 <liuhongyu@apache.org>
This commit is contained in:
@@ -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>
|
||||
|
||||
+12
@@ -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(
|
||||
|
||||
+55
@@ -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;
|
||||
}
|
||||
+12
-1
@@ -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
|
||||
|
||||
+69
-4
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "アラートステータス",
|
||||
|
||||
@@ -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": "알람 상태",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "告警状态",
|
||||
|
||||
@@ -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": "告警狀態",
|
||||
|
||||
Reference in New Issue
Block a user