This is an automated email from the ASF dual-hosted git repository.
zhaoqingran 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 b510d6e5ea [feature] add datasource status retrieval and update alert
settings UI (#3985)
b510d6e5ea is described below
commit b510d6e5ea761c9ef182311c6b547d6b590d1695
Author: Logic <[email protected]>
AuthorDate: Tue Jan 20 22:43:47 2026 +0800
[feature] add datasource status retrieval and update alert settings UI
(#3985)
Signed-off-by: Logic <[email protected]>
Signed-off-by: Duansg <[email protected]>
Co-authored-by: aias00 <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Duansg <[email protected]>
---
.../alert/controller/AlertDefineController.java | 12 ++++
.../hertzbeat/alert/service/DataSourceService.java | 6 ++
.../alert/service/impl/DataSourceServiceImpl.java | 30 ++++++++
.../alert-setting/alert-setting.component.html | 84 +++++++++++++++++-----
.../alert-setting/alert-setting.component.less | 54 ++++++++++++++
.../alert/alert-setting/alert-setting.component.ts | 53 +++++++++++++-
web-app/src/app/service/alert-define.service.ts | 4 ++
web-app/src/assets/i18n/en-US.json | 14 ++++
web-app/src/assets/i18n/zh-CN.json | 14 ++++
9 files changed, 251 insertions(+), 20 deletions(-)
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
index 2486189447..04b9de2e47 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java
@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.apache.hertzbeat.alert.service.AlertDefineService;
+import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
@@ -57,6 +58,9 @@ public class AlertDefineController {
@Autowired
private AlertDefineService alertDefineService;
+ @Autowired
+ private DataSourceService dataSourceService;
+
@PostMapping
@Operation(summary = "New Alarm Definition", description = "Added an alarm
definition")
public ResponseEntity<Message<Void>> addNewAlertDefine(@Valid @RequestBody
AlertDefine alertDefine) {
@@ -112,4 +116,12 @@ public class AlertDefineController {
}
}
+ @GetMapping(path = "/datasource/status")
+ @Operation(summary = "Get available datasource executors status",
+ description = "Get status of available datasource executors for
periodic alerts")
+ public ResponseEntity<Message<Map<String, Object>>> getDatasourceStatus() {
+ Map<String, Object> status = dataSourceService.getAvailableExecutors();
+ return ResponseEntity.ok(Message.successWithData(status));
+ }
+
}
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
index 8baca0b733..a3dc79bf02 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/DataSourceService.java
@@ -40,4 +40,10 @@ public interface DataSourceService {
* @return result
*/
List<Map<String, Object>> query(String datasource, String expr);
+
+ /**
+ * Get available datasource executors status
+ * @return map containing available executors by type (promql, sql)
+ */
+ Map<String, Object> getAvailableExecutors();
}
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
index 470ddeee86..136777c085 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/DataSourceServiceImpl.java
@@ -168,4 +168,34 @@ public class DataSourceServiceImpl implements
DataSourceService {
AlertExpressionLexer lexer = new
AlertExpressionLexer(CharStreams.fromString(expr));
return new CommonTokenStream(lexer);
}
+
+ @Override
+ public Map<String, Object> getAvailableExecutors() {
+ boolean hasPromqlExecutor = false;
+ boolean hasSqlExecutor = false;
+ java.util.Set<String> availableExecutors = new java.util.HashSet<>();
+
+ if (executors != null) {
+ for (QueryExecutor executor : executors) {
+ String datasource = executor.getDatasource();
+ availableExecutors.add(datasource);
+
+ // Check if executor supports promql
+ if (executor.support(WarehouseConstants.PROMQL)) {
+ hasPromqlExecutor = true;
+ }
+ // Check if executor supports sql
+ if (executor.support(WarehouseConstants.SQL)) {
+ hasSqlExecutor = true;
+ }
+ }
+ }
+
+ Map<String, Object> result = new java.util.HashMap<>(8);
+ result.put("hasPromqlExecutor", hasPromqlExecutor);
+ result.put("hasSqlExecutor", hasSqlExecutor);
+ result.put("availableExecutors", availableExecutors);
+
+ return result;
+ }
}
diff --git
a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
index def222c507..0ebeabc310 100644
--- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
+++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
@@ -472,7 +472,7 @@
<div class="template-input-wrapper">
<a nz-dropdown [nzDropdownMenu]="logExprMenu"
class="env-vars-dropdown" nzTrigger="click" [nzPlacement]="'bottomRight'">
<button nz-button nzType="link" nz-tooltip
[nzTooltipTitle]="'alert.setting.log.expr.tip' | i18n">
- <i nz-icon nzType="snippets" nzTheme="outline"></i>
+ <i nz-icon nzType="unordered-list" nzTheme="outline"></i>
</button>
</a>
<nz-dropdown-menu #logExprMenu="nzDropdownMenu">
@@ -507,7 +507,7 @@
nz-input
name="logExpr"
id="logExpr"
- [placeholder]="'alert.setting.log.expr.example' | i18n"
+ [placeholder]="'alert.setting.log.expr.placeholder' | i18n"
>
</textarea>
</nz-textarea-count>
@@ -656,16 +656,35 @@
</nz-form-item>
<nz-form-item *ngIf="define.type == 'periodic_metric'" [ngStyle]="{
marginBottom: '5px' }">
<nz-form-label [nzSpan]="7" nzFor="datasource" nzRequired="true"
[nzTooltipTitle]="'alert.setting.rule.label' | i18n">
- {{ 'alert.setting.rule' | i18n }}
+ {{ 'alert.setting.query.language' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' |
i18n">
- <ng-container *ngIf="cascadeValues[1] !== 'availability'">
- <nz-radio-group [(ngModel)]="define.datasource"
nzButtonStyle="solid" name="datasource" id="datasource">
- <label nz-radio-button [nzValue]="'promql'">
- {{ 'PromQL' | i18n }}
- </label>
- </nz-radio-group>
- </ng-container>
+ <nz-radio-group
+ [(ngModel)]="define.datasource"
+ (ngModelChange)="onDatasourceChange()"
+ nzButtonStyle="solid"
+ name="datasource"
+ id="datasource"
+ >
+ <label nz-radio-button [nzValue]="'promql'" [nzDisabled]="false">
+ {{ 'alert.setting.query.language.promql' | i18n }}
+ </label>
+ <label
+ nz-radio-button
+ [nzValue]="'sql'"
+ [nzDisabled]="true"
+ nz-tooltip
+
[nzTooltipTitle]="'alert.setting.query.language.sql.not.support.metric' | i18n"
+ >
+ {{ 'alert.setting.query.language.sql' | i18n }}
+ </label>
+ </nz-radio-group>
+ <div class="datasource-tip" *ngIf="define.datasource === 'promql'">
+ <small [innerHTML]="'alert.setting.promql.description' |
i18n"></small>
+ </div>
+ <div class="datasource-tip" *ngIf="define.datasource === 'sql'">
+ <small [innerHTML]="'alert.setting.sql.description' |
i18n"></small>
+ </div>
</nz-form-control>
</nz-form-item>
<!-- Metric query and alert expression -->
@@ -717,16 +736,35 @@
</nz-form-item>
<nz-form-item *ngIf="define.type == 'periodic_log'" [ngStyle]="{
marginBottom: '5px' }">
<nz-form-label [nzSpan]="7" nzRequired="true" nzFor="periodic_expr"
[nzTooltipTitle]="'alert.setting.log.query.tip' | i18n">
- {{ 'alert.setting.log.query' | i18n }}
+ {{ 'alert.setting.query.language' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' |
i18n">
- <ng-container *ngIf="cascadeValues[1] !== 'availability'">
- <nz-radio-group [(ngModel)]="define.datasource"
nzButtonStyle="solid" name="datasource" id="datasource">
- <label nz-radio-button [nzValue]="'sql'">
- {{ 'SQL' | i18n }}
- </label>
- </nz-radio-group>
- </ng-container>
+ <nz-radio-group
+ [(ngModel)]="define.datasource"
+ (ngModelChange)="onDatasourceChange()"
+ nzButtonStyle="solid"
+ name="datasource"
+ id="datasource"
+ >
+ <label
+ nz-radio-button
+ [nzValue]="'promql'"
+ [nzDisabled]="true"
+ nz-tooltip
+
[nzTooltipTitle]="'alert.setting.query.language.promql.not.support.log' | i18n"
+ >
+ {{ 'alert.setting.query.language.promql' | i18n }}
+ </label>
+ <label nz-radio-button [nzValue]="'sql'" [nzDisabled]="false">
+ {{ 'alert.setting.query.language.sql' | i18n }}
+ </label>
+ </nz-radio-group>
+ <div class="datasource-tip" *ngIf="define.datasource === 'promql'">
+ <small [innerHTML]="'alert.setting.promql.description' |
i18n"></small>
+ </div>
+ <div class="datasource-tip" *ngIf="define.datasource === 'sql'">
+ <small [innerHTML]="'alert.setting.sql.description' |
i18n"></small>
+ </div>
</nz-form-control>
</nz-form-item>
<!-- Log query expression -->
@@ -1109,10 +1147,18 @@
<h3>{{ 'alert.setting.type.realtime' | i18n }}</h3>
<p>{{ 'alert.setting.type.realtime.desc' | i18n }}</p>
</nz-card>
- <nz-card (click)="onSelectAlertType('periodic')" class="alert-type-card"
[nzHoverable]="true">
+ <nz-card
+ (click)="onSelectAlertType('periodic')"
+ class="alert-type-card"
+ [nzHoverable]="isPeriodicAlertEnabled"
+ [class.alert-type-card-disabled]="!isPeriodicAlertEnabled"
+ >
<i nz-icon nzType="clock-circle" nzTheme="outline"></i>
<h3>{{ 'alert.setting.type.periodic' | i18n }}</h3>
<p>{{ 'alert.setting.type.periodic.desc' | i18n }}</p>
+ <div *ngIf="!isPeriodicAlertEnabled" class="disabled-corner-indicator">
+ <i nz-icon nzType="info-circle" nzTheme="outline"></i>
+ </div>
</nz-card>
</div>
</div>
diff --git
a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.less
b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.less
index 18fb75f8c2..1474e07759 100644
--- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.less
+++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.less
@@ -149,9 +149,22 @@
.alert-type-card {
width: 200px;
+ min-height: 160px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
text-align: center;
cursor: pointer;
+ ::ng-deep .ant-card-body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 100%;
+ padding-top: 30px;
+ }
+
i {
font-size: 24px;
margin-bottom: 8px;
@@ -368,4 +381,45 @@
height: 200px;
overflow-y: auto;
}
+
+ .alert-type-card-disabled {
+ opacity: 0.5;
+ cursor: not-allowed !important;
+
+ &:hover {
+ transform: none !important;
+ box-shadow: none !important;
+ }
+ }
+
+ .disabled-corner-indicator {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 50px;
+ height: 50px;
+ background: linear-gradient(45deg, transparent 50%, #faad14 50%);
+ pointer-events: none;
+ z-index: 10;
+
+ i {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ color: #fff;
+
+ svg {
+ width: 22px;
+ height: 22px;
+ }
+ }
+ }
+
+ .datasource-tip {
+ margin-top: 8px;
+ padding: 8px 12px;
+ background-color: #f6f6f6;
+ border-radius: 4px;
+ line-height: 1.5;
+ }
}
diff --git
a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
index bb6725af8c..ea8e7bc0f5 100644
--- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
+++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
@@ -133,6 +133,14 @@ export class AlertSettingComponent implements OnInit {
dataType: string = 'metric'; // Default to metric
alertType: string = 'realtime'; // Default to realtime
+ // Datasource status
+ datasourceStatus = {
+ hasPromqlExecutor: false,
+ hasSqlExecutor: false,
+ loaded: false
+ };
+ isPeriodicAlertEnabled = true;
+
previewData: any[] = [];
previewColumns: Array<{ title: string; key: string; width?: string }> = [];
previewTableLoading = false;
@@ -208,6 +216,7 @@ export class AlertSettingComponent implements OnInit {
ngOnInit(): void {
this.initLogFields();
+ this.loadDatasourceStatus();
this.loadAlertDefineTable();
// query monitoring hierarchy
const getHierarchy$ = this.appDefineSvc
@@ -321,7 +330,45 @@ export class AlertSettingComponent implements OnInit {
this.isSelectTypeModalVisible = true;
}
+ loadDatasourceStatus() {
+ this.alertDefineSvc.getDatasourceStatus().subscribe({
+ next: res => {
+ if (res.code === 0 && res.data) {
+ this.datasourceStatus = {
+ hasPromqlExecutor: res.data.hasPromqlExecutor || false,
+ hasSqlExecutor: res.data.hasSqlExecutor || false,
+ loaded: true
+ };
+ // Check if periodic alert is enabled (requires at least one
executor)
+ this.isPeriodicAlertEnabled =
this.datasourceStatus.hasPromqlExecutor || this.datasourceStatus.hasSqlExecutor;
+ }
+ },
+ error: err => {
+ console.warn('Failed to load datasource status:', err);
+ // If failed to load, disable periodic alerts and use a safe fallback
status
+ this.isPeriodicAlertEnabled = false;
+ this.datasourceStatus = {
+ hasPromqlExecutor: false,
+ hasSqlExecutor: false,
+ loaded: true
+ };
+ this.notifySvc.warning('Failed to load datasource status. Periodic
alerts are disabled.', '');
+ }
+ });
+ }
+
+ onDatasourceChange() {
+ // Update placeholder when datasource changes
+ // Placeholder is already set in template with dynamic binding
+ }
+
onSelectAlertType(type: string) {
+ // Check if periodic alert is enabled
+ if (type === 'periodic' && !this.isPeriodicAlertEnabled) {
+
this.notifySvc.warning(this.i18nSvc.fanyi('alert.setting.type.periodic.not.available'),
'');
+ return;
+ }
+
this.isSelectTypeModalVisible = false;
this.alertType = type;
this.define = new AlertDefine();
@@ -1442,7 +1489,10 @@ export class AlertSettingComponent implements OnInit {
// Handle variable click event
onExprVarClick(item: { value: string; description?: string }) {
- const textarea = document.getElementById('expr') as HTMLTextAreaElement;
+ // Determine the correct textarea ID based on alert type
+ const textareaId = this.define.type === 'realtime_log' ? 'logExpr' :
'expr';
+ const textarea = document.getElementById(textareaId) as
HTMLTextAreaElement;
+
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
@@ -1460,6 +1510,7 @@ export class AlertSettingComponent implements OnInit {
}
}
+ // Update the expression variable
this.userExpr = `${before} ${insertText} ${after}`;
// Restore cursor position
diff --git a/web-app/src/app/service/alert-define.service.ts
b/web-app/src/app/service/alert-define.service.ts
index 124833d9f6..af3e83e132 100644
--- a/web-app/src/app/service/alert-define.service.ts
+++ b/web-app/src/app/service/alert-define.service.ts
@@ -107,4 +107,8 @@ export class AlertDefineService {
responseType: 'blob'
});
}
+
+ public getDatasourceStatus(): Observable<Message<any>> {
+ return
this.http.get<Message<any>>(`${alert_define_uri}/datasource/status`);
+ }
}
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index 9a89c2fef0..36c3c1fd11 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -362,6 +362,20 @@
"alert.setting.type.realtime.desc": "Calculate data in real-time with
instant alert on threshold breach",
"alert.setting.type.realtime.log": "Log RealTime",
"alert.setting.type.realtime.metric": "Metric RealTime",
+ "alert.setting.query.language": "Query Language",
+ "alert.setting.query.language.promql": "PromQL",
+ "alert.setting.query.language.sql": "SQL",
+ "alert.setting.query.language.sql.not.support.metric": "SQL queries are not
supported for periodic metric alerts yet",
+ "alert.setting.query.language.promql.not.support.log": "PromQL queries are
not supported for periodic log alerts yet",
+ "alert.setting.promql.description": "PromQL Query
Language<br/><br/>Examples:<br/>• <code>cpu_usage > 80</code><br/>•
<code>cpu_usage{instance=\"server1\"} > 80</code><br/>•
<code>rate(http_requests_total[5m]) > 100</code><br/>• <code>cpu > 80 and
memory > 70</code>",
+ "alert.setting.sql.description": "SQL Query
Language<br/><br/>Examples:<br/>• <code>SELECT COUNT(*) FROM hertzbeat_logs
WHERE severity_text = 'ERROR'</code><br/>• <code>SELECT service_name, COUNT(*)
FROM hertzbeat_logs GROUP BY service_name</code><br/>• <code>SELECT * FROM
hertzbeat_logs WHERE time_unix_nano >= NOW() - INTERVAL '5 minute'</code>",
+ "alert.setting.type.realtime": "Real-time Alert",
+ "alert.setting.type.periodic": "Periodic Alert",
+ "alert.setting.type.realtime.desc": "Trigger alerts immediately upon data
collection",
+ "alert.setting.type.periodic.desc": "Execute PromQL/SQL queries periodically
to check alert conditions",
+ "alert.setting.type.periodic.not.configured": "Please configure a supported
time-series database (Prometheus, GreptimeDB, VictoriaMetrics, etc.) to use
periodic alerts",
+ "alert.setting.type.periodic.not.available": "Periodic alerts are not
available, please configure a time-series database first",
+ "alert.setting.type.periodic.not.available.short": "Need Time-Series DB",
"alert.severity": "Alarm Severity",
"alert.severity.0": "Emergency",
"alert.severity.1": "Critical",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index ca614fefc9..5347b004f6 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -365,6 +365,20 @@
"alert.setting.window": "计算窗口",
"alert.setting.window.tip": "实时阈值计算窗口时间,单位秒",
"alert.setting.window.placeholder": "请输入计算窗口时间",
+ "alert.setting.query.language": "查询语言",
+ "alert.setting.query.language.promql": "PromQL",
+ "alert.setting.query.language.sql": "SQL",
+ "alert.setting.query.language.sql.not.support.metric": "周期性监控指标暂不支持 SQL 查询",
+ "alert.setting.query.language.promql.not.support.log": "周期性日志暂不支持 PromQL 查询",
+ "alert.setting.promql.description": "PromQL 查询语言<br/><br/>示例:<br/>•
<code>cpu_usage > 80</code><br/>• <code>cpu_usage{instance=\"server1\"} >
80</code><br/>• <code>rate(http_requests_total[5m]) > 100</code><br/>•
<code>cpu > 80 and memory > 70</code>",
+ "alert.setting.sql.description": "SQL 查询语言<br/><br/>示例:<br/>• <code>SELECT
COUNT(*) FROM hertzbeat_logs WHERE severity_text = 'ERROR'</code><br/>•
<code>SELECT service_name, COUNT(*) FROM hertzbeat_logs GROUP BY
service_name</code><br/>• <code>SELECT * FROM hertzbeat_logs WHERE
time_unix_nano >= NOW() - INTERVAL '5 minute'</code>",
+ "alert.setting.type.realtime": "实时告警",
+ "alert.setting.type.periodic": "周期告警",
+ "alert.setting.type.realtime.desc": "实时监控数据采集即触发告警",
+ "alert.setting.type.periodic.desc": "按周期执行 PromQL/SQL 查询判断告警",
+ "alert.setting.type.periodic.not.configured":
"请先配置支持的时序库(Prometheus、GreptimeDB、VictoriaMetrics 等)才能使用周期告警",
+ "alert.setting.type.periodic.not.available": "周期告警暂不可用,请先配置时序库",
+ "alert.setting.type.periodic.not.available.short": "需配置时序库",
"alert.severity": "告警级别",
"alert.severity.0": "紧急告警",
"alert.severity.1": "严重告警",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]