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]

Reply via email to