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 950138a340 [feat] Add an SQL editor and prevent SQL injection (#3900)
950138a340 is described below

commit 950138a3408de81d979759a63a605760a9f7713d
Author: Yang Chen <[email protected]>
AuthorDate: Fri Dec 12 17:52:31 2025 +0800

    [feat] Add an SQL editor and prevent SQL injection (#3900)
    
    Signed-off-by: Yang Chen <[email protected]>
    Co-authored-by: Copilot <[email protected]>
---
 .../alert/service/impl/DataSourceServiceImpl.java  |  46 ++-
 .../alert/service/DataSourceServiceTest.java       | 157 +++++++-
 hertzbeat-common/pom.xml                           |   6 +-
 .../common/support/valid/SqlSecurityException.java |  32 ++
 .../common/support/valid/SqlSecurityValidator.java | 150 +++++++
 .../support/valid/SqlSecurityValidatorTest.java    | 267 +++++++++++++
 .../warehouse/constants/WarehouseConstants.java    |   2 +
 .../warehouse/db/GreptimeSqlQueryExecutor.java     |  91 ++---
 .../tsdb/greptime/GreptimeDbDataStorage.java       |   3 +-
 .../warehouse/db/GreptimeSqlQueryExecutorTest.java |  16 +-
 material/licenses/backend/LICENSE                  |   1 +
 pom.xml                                            |   8 +-
 .../alert-setting/alert-setting.component.html     |  23 +-
 .../alert/alert-setting/alert-setting.component.ts |  21 +-
 .../sql-editor/sql-editor.component.html           |  28 ++
 .../sql-editor/sql-editor.component.less           |  68 ++++
 .../components/sql-editor/sql-editor.component.ts  | 438 +++++++++++++++++++++
 web-app/src/app/shared/shared.module.ts            |   6 +-
 web-app/src/assets/i18n/en-US.json                 |   1 -
 web-app/src/assets/i18n/ja-JP.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 -
 23 files changed, 1279 insertions(+), 89 deletions(-)

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 4ee3c4e879..93fd115928 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
@@ -31,12 +31,16 @@ import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
 import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
 import org.apache.hertzbeat.alert.service.DataSourceService;
 import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
+import org.apache.hertzbeat.common.support.valid.SqlSecurityException;
+import org.apache.hertzbeat.common.support.valid.SqlSecurityValidator;
 import org.apache.hertzbeat.common.util.ResourceBundleUtil;
+import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
 import org.apache.hertzbeat.warehouse.db.QueryExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
@@ -49,12 +53,18 @@ import java.util.concurrent.TimeUnit;
 @Slf4j
 public class DataSourceServiceImpl implements DataSourceService {
 
+    /**
+     * Default allowed tables for SQL queries
+     */
+    private static final List<String> DEFAULT_ALLOWED_TABLES = 
List.of(WarehouseConstants.LOG_TABLE_NAME);
+
     protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
 
     @Setter
-    @Autowired(required = false)
     private List<QueryExecutor> executors;
 
+    private final SqlSecurityValidator sqlSecurityValidator;
+
     @Getter
     private final Cache<String, ParseTree> expressionCache = 
Caffeine.newBuilder()
             .maximumSize(256)
@@ -69,6 +79,11 @@ public class DataSourceServiceImpl implements 
DataSourceService {
             .recordStats()
             .build();
 
+    public DataSourceServiceImpl(@Autowired(required = false) 
List<QueryExecutor> executors) {
+        this.executors = executors != null ? executors : 
Collections.emptyList();
+        this.sqlSecurityValidator = new 
SqlSecurityValidator(DEFAULT_ALLOWED_TABLES);
+    }
+
     @Override
     public List<Map<String, Object>> calculate(String datasource, String expr) 
{
         if (!StringUtils.hasText(expr)) {
@@ -110,11 +125,36 @@ public class DataSourceServiceImpl implements 
DataSourceService {
         }
         // replace all white space
         expr = expr.replaceAll("\\s+", " ");
+
+        // SQL security validation for SQL-based datasources
+        if (isSqlDatasource(datasource)) {
+            validateSqlSecurity(expr);
+        }
+
         try {
             return executor.execute(expr);
         } catch (Exception e) {
             log.error("Error executing query on datasource {}: {}", 
datasource, e.getMessage());
-            throw new RuntimeException("Query execution failed", e);
+            throw new AlertExpressionException(e.getMessage());
+        }
+    }
+
+    /**
+     * Check if the datasource is SQL-based
+     */
+    private boolean isSqlDatasource(String datasource) {
+        return datasource != null && 
datasource.equalsIgnoreCase(WarehouseConstants.SQL);
+    }
+
+    /**
+     * Validate SQL statement for security
+     */
+    private void validateSqlSecurity(String sql) {
+        try {
+            sqlSecurityValidator.validate(sql);
+        } catch (SqlSecurityException e) {
+            log.warn("SQL security validation failed: {}", e.getMessage());
+            throw new AlertExpressionException("SQL security validation 
failed: " + e.getMessage());
         }
     }
 
@@ -133,4 +173,4 @@ public class DataSourceServiceImpl implements 
DataSourceService {
         AlertExpressionLexer lexer = new 
AlertExpressionLexer(CharStreams.fromString(expr));
         return new CommonTokenStream(lexer);
     }
-}
\ No newline at end of file
+}
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
index dab28ab766..8fd9e4acc2 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/DataSourceServiceTest.java
@@ -36,7 +36,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
@@ -48,7 +51,7 @@ class DataSourceServiceTest {
 
     @BeforeEach
     void setUp() {
-        dataSourceService = new DataSourceServiceImpl();
+        dataSourceService = new DataSourceServiceImpl(null);
     }
 
     @Test
@@ -639,4 +642,156 @@ class DataSourceServiceTest {
 
         assertThrows(AlertExpressionException.class, () -> 
dataSourceService.calculate("promql", 
"http_server_requests_seconds_count{!@~!!#$%^&}"));
     }
+
+    @Test
+    void query1() {
+        List<Map<String, Object>> sqlData = List.of(
+                new HashMap<>(Map.of("count", 10, "severity_text", "ERROR"))
+        );
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        when(mockExecutor.execute(anyString())).thenReturn(sqlData);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        String validSql = "SELECT count(*) FROM hertzbeat_logs WHERE 
severity_text = 'ERROR'";
+        List<Map<String, Object>> result = dataSourceService.query("sql", 
validSql);
+
+        assertNotNull(result);
+        assertEquals(1, result.size());
+        verify(mockExecutor).execute(anyString());
+    }
+
+    @Test
+    void query2() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "INSERT INTO 
hertzbeat_logs (body) VALUES ('test')"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query3() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "DELETE FROM 
hertzbeat_logs WHERE id = 1"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query4() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "UPDATE hertzbeat_logs 
SET body = 'hacked' WHERE id = 1"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query5() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "DROP TABLE 
hertzbeat_logs"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query6() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM 
hertzbeat_logs UNION SELECT * FROM users"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query7() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM 
hertzbeat_logs WHERE id IN (SELECT id FROM other_table)"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query8() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELECT * FROM users"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query9() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "WITH cte AS (SELECT * 
FROM hertzbeat_logs) SELECT * FROM cte"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
+
+    @Test
+    void query10() {
+        List<Map<String, Object>> sqlData = List.of(
+                new HashMap<>(Map.of("errorCount", 5))
+        );
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        when(mockExecutor.execute(anyString())).thenReturn(sqlData);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        String complexSql = "SELECT count(*) AS errorCount FROM hertzbeat_logs 
"
+                + "WHERE time_unix_nano >= NOW() AND severity_text = 'ERROR' "
+                + "GROUP BY severity_text HAVING count(*) > 2 ORDER BY 
errorCount LIMIT 10";
+
+        List<Map<String, Object>> result = dataSourceService.query("sql", 
complexSql);
+        assertNotNull(result);
+        verify(mockExecutor).execute(anyString());
+    }
+
+    @Test
+    void query11() {
+        List<Map<String, Object>> prometheusData = List.of(
+                new HashMap<>(Map.of("__value__", 100.0))
+        );
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("promql")).thenReturn(true);
+        when(mockExecutor.execute(anyString())).thenReturn(prometheusData);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        List<Map<String, Object>> result = dataSourceService.query("promql", 
"node_cpu_seconds_total > 50");
+
+        assertNotNull(result);
+        verify(mockExecutor).execute(anyString());
+    }
+
+    @Test
+    void query12() {
+        QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
+        when(mockExecutor.support("sql")).thenReturn(true);
+        dataSourceService.setExecutors(List.of(mockExecutor));
+
+        assertThrows(AlertExpressionException.class,
+                () -> dataSourceService.query("sql", "SELEC * FORM 
hertzbeat_logs"));
+        verify(mockExecutor, never()).execute(anyString());
+    }
 }
diff --git a/hertzbeat-common/pom.xml b/hertzbeat-common/pom.xml
index ea93cd48c8..379245e6ae 100644
--- a/hertzbeat-common/pom.xml
+++ b/hertzbeat-common/pom.xml
@@ -171,7 +171,7 @@
             <groupId>org.apache.arrow</groupId>
             <artifactId>arrow-memory-netty</artifactId>
         </dependency>
-        
+
         <dependency>
             <groupId>org.xerial.snappy</groupId>
             <artifactId>snappy-java</artifactId>
@@ -183,6 +183,10 @@
             <version>${javaparser.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.github.jsqlparser</groupId>
+            <artifactId>jsqlparser</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java
 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java
new file mode 100644
index 0000000000..d343cb7ef8
--- /dev/null
+++ 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.common.support.valid;
+
+/**
+ * SQL security validation exception
+ */
+public class SqlSecurityException extends RuntimeException {
+
+    public SqlSecurityException(String message) {
+        super(message);
+    }
+
+    public SqlSecurityException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git 
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java
 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java
new file mode 100644
index 0000000000..6d58dff18b
--- /dev/null
+++ 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidator.java
@@ -0,0 +1,150 @@
+/*
+ * 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.common.support.valid;
+
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.select.LateralSubSelect;
+import net.sf.jsqlparser.statement.select.ParenthesedSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.select.SetOperationList;
+import net.sf.jsqlparser.statement.select.WithItem;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * SQL Security Validator using JSqlParser 5.1+.
+ * Security Policy:
+ * 1. Only SELECT statements are allowed.
+ * 2. All referenced tables must be in the whitelist.
+ * 3. Subqueries, UNION, CTE, LATERAL are blocked.
+ */
+@Slf4j
+public class SqlSecurityValidator {
+
+    private final Set<String> allowedTables;
+
+    public SqlSecurityValidator(Collection<String> allowedTables) {
+        if (CollectionUtils.isEmpty(allowedTables)) {
+            this.allowedTables = new HashSet<>();
+        } else {
+            this.allowedTables = allowedTables.stream()
+                    .map(this::normalizeIdentifier)
+                    .collect(Collectors.toSet());
+        }
+    }
+
+    public void validate(String sql) throws SqlSecurityException {
+        if (sql == null || sql.trim().isEmpty()) {
+            throw new SqlSecurityException("SQL statement cannot be empty");
+        }
+
+        Statement statement;
+        try {
+            statement = CCJSqlParserUtil.parse(sql);
+        } catch (JSQLParserException e) {
+            log.warn("Failed to parse SQL: {}", sql, e);
+            throw new SqlSecurityException("Invalid SQL syntax: " + 
e.getMessage(), e);
+        }
+
+        if (!(statement instanceof Select select)) {
+            throw new SqlSecurityException("Only SELECT statements are 
allowed.");
+        }
+
+        // Check for CTE at top level
+        if (select.getWithItemsList() != null && 
!select.getWithItemsList().isEmpty()) {
+            throw new SqlSecurityException("CTE (WITH clause) is not allowed");
+        }
+
+        // Use custom TablesNamesFinder that throws on dangerous structures
+        SecurityTablesNamesFinder finder = new SecurityTablesNamesFinder();
+        List<String> tables;
+        try {
+            tables = finder.getTableList(statement);
+        } catch (SecurityViolationException e) {
+            throw new SqlSecurityException(e.getMessage());
+        }
+
+        validateTables(tables);
+    }
+
+    private void validateTables(List<String> tables) throws 
SqlSecurityException {
+        if (CollectionUtils.isEmpty(tables)) {
+            return;
+        }
+        if (allowedTables.isEmpty()) {
+            throw new SqlSecurityException("No access allowed: whitelist is 
empty.");
+        }
+
+        for (String table : tables) {
+            String normalizedTable = normalizeIdentifier(table);
+            if (!allowedTables.contains(normalizedTable)) {
+                throw new SqlSecurityException("Access to table '" + table + 
"' is not allowed. "
+                        + "Allowed tables: " + allowedTables);
+            }
+        }
+    }
+
+    private String normalizeIdentifier(String identifier) {
+        if (identifier == null) {
+            return "";
+        }
+        return identifier.replace("\"", "").replace("`", "").replace("'", 
"").toLowerCase();
+    }
+
+    private static class SecurityViolationException extends RuntimeException {
+        SecurityViolationException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Custom TablesNamesFinder that throws exceptions on dangerous SQL 
structures.
+     * Extends TablesNamesFinder with proper generic type to avoid raw type 
warnings.
+     */
+    private static class SecurityTablesNamesFinder extends 
TablesNamesFinder<Void> {
+
+        @Override
+        public Void visit(ParenthesedSelect parenthesedSelect, Object context) 
{
+            throw new SecurityViolationException("Subqueries are not allowed");
+        }
+
+        @Override
+        public Void visit(SetOperationList setOpList, Object context) {
+            throw new SecurityViolationException("UNION and set operations are 
not allowed");
+        }
+
+        @Override
+        public Void visit(LateralSubSelect lateralSubSelect, Object context) {
+            throw new SecurityViolationException("LATERAL subqueries are not 
allowed");
+        }
+
+        @Override
+        public Void visit(WithItem withItem, Object context) {
+            throw new SecurityViolationException("CTE (WITH clause) is not 
allowed");
+        }
+    }
+}
diff --git 
a/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java
 
b/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java
new file mode 100644
index 0000000000..782ee31383
--- /dev/null
+++ 
b/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/support/valid/SqlSecurityValidatorTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.common.support.valid;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test for {@link SqlSecurityValidator}
+ */
+class SqlSecurityValidatorTest {
+
+    private SqlSecurityValidator validator;
+
+    @BeforeEach
+    void setUp() {
+        validator = new SqlSecurityValidator(Arrays.asList("hertzbeat_logs", 
"app_logs", "access_logs"));
+    }
+
+    @Test
+    void testValidSelectStatement() {
+        assertDoesNotThrow(() -> validator.validate("SELECT * FROM 
hertzbeat_logs"));
+        assertDoesNotThrow(() -> validator.validate("SELECT id, message FROM 
hertzbeat_logs WHERE level = 'ERROR'"));
+        assertDoesNotThrow(() -> validator.validate("SELECT COUNT(*) FROM 
app_logs"));
+        assertDoesNotThrow(() -> validator.validate("select * from 
HERTZBEAT_LOGS")); // case insensitive
+    }
+
+    @Test
+    void testSelectWithJoin() {
+        assertDoesNotThrow(() -> validator.validate(
+                "SELECT a.id, b.message FROM hertzbeat_logs a JOIN app_logs b 
ON a.id = b.id"));
+    }
+
+    @Test
+    void testSelectWithSubqueryNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs WHERE 
id IN (SELECT id FROM app_logs)"));
+    }
+
+    @Test
+    void testSelectWithSubqueryInFromNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM (SELECT * FROM 
hertzbeat_logs) AS subq"));
+    }
+
+    @Test
+    void testEmptySql() {
+        assertThrows(SqlSecurityException.class, () -> 
validator.validate(null));
+        assertThrows(SqlSecurityException.class, () -> validator.validate(""));
+        assertThrows(SqlSecurityException.class, () -> validator.validate("   
"));
+    }
+
+    @Test
+    void testInvalidSqlSyntax() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FORM hertzbeat_logs")); // 
typo: FORM instead of FROM
+    }
+
+    @Test
+    void testInsertNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("INSERT INTO hertzbeat_logs (message) 
VALUES ('test')"));
+    }
+
+    @Test
+    void testUpdateNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("UPDATE hertzbeat_logs SET message = 
'test' WHERE id = 1"));
+    }
+
+    @Test
+    void testDeleteNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("DELETE FROM hertzbeat_logs WHERE id 
= 1"));
+    }
+
+    @Test
+    void testDropNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("DROP TABLE hertzbeat_logs"));
+    }
+
+    @Test
+    void testTruncateNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("TRUNCATE TABLE hertzbeat_logs"));
+    }
+
+    @Test
+    void testAlterNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("ALTER TABLE hertzbeat_logs ADD 
COLUMN new_col VARCHAR(100)"));
+    }
+
+    @Test
+    void testCreateNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("CREATE TABLE new_table (id INT)"));
+    }
+
+    @Test
+    void testUnauthorizedTable() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM users"));
+    }
+
+    @Test
+    void testUnauthorizedTableInJoin() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs JOIN 
users ON hertzbeat_logs.user_id = users.id"));
+    }
+
+    @Test
+    void testUnauthorizedTableInSubquery() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs WHERE 
user_id IN (SELECT id FROM users)"));
+    }
+
+    @Test
+    void testTableWithQuotes() {
+        assertDoesNotThrow(() -> validator.validate("SELECT * FROM 
\"hertzbeat_logs\""));
+        assertDoesNotThrow(() -> validator.validate("SELECT * FROM 
`hertzbeat_logs`"));
+    }
+
+    @Test
+    void testEmptyAllowedTables() {
+        SqlSecurityValidator emptyValidator = new 
SqlSecurityValidator(Collections.emptyList());
+        assertThrows(SqlSecurityException.class,
+                () -> emptyValidator.validate("SELECT * FROM any_table"));
+    }
+
+    @Test
+    void testNullAllowedTables() {
+        SqlSecurityValidator nullValidator = new SqlSecurityValidator(null);
+        assertThrows(SqlSecurityException.class,
+                () -> nullValidator.validate("SELECT * FROM any_table"));
+    }
+
+    @Test
+    void testComplexSelectWithAggregation() {
+        assertDoesNotThrow(() -> validator.validate(
+                "SELECT level, COUNT(*) as cnt FROM hertzbeat_logs GROUP BY 
level HAVING COUNT(*) > 10 ORDER BY cnt DESC LIMIT 100"));
+    }
+
+    @Test
+    void testSelectWithUnionNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs UNION 
SELECT * FROM app_logs"));
+    }
+
+    @Test
+    void testSelectWithUnionAllNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs UNION 
ALL SELECT * FROM app_logs"));
+    }
+
+    @Test
+    void testSelectWithIntersectNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs 
INTERSECT SELECT * FROM app_logs"));
+    }
+
+    @Test
+    void testSelectWithExceptNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs EXCEPT 
SELECT * FROM app_logs"));
+    }
+
+    @Test
+    void testLateralSubqueryNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM hertzbeat_logs, 
LATERAL (SELECT * FROM app_logs) AS t"));
+    }
+
+    @Test
+    void testWithClauseNotAllowed() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("WITH cte AS (SELECT * FROM 
hertzbeat_logs) SELECT * FROM cte"));
+    }
+
+    @Test
+    void testSqlInjectionAttemptDropTable() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("DROP TABLE users"));
+    }
+
+    @Test
+    void testSqlInjectionAttemptUnauthorizedTable() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("SELECT * FROM users"));
+    }
+
+    @Test
+    void testSqlInjectionAttemptDeleteFrom() {
+        assertThrows(SqlSecurityException.class,
+                () -> validator.validate("DELETE FROM hertzbeat_logs WHERE 
1=1"));
+    }
+
+    @Test
+    void testBypassInSelectItems() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT (SELECT password FROM secret_table) FROM hertzbeat_logs"));
+    }
+
+    @Test
+    void testBypassInWhereClauseAnd() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE 1=1 AND id IN (SELECT id FROM 
secret_table)"));
+    }
+
+    @Test
+    void testBypassInFunction() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE id = abs((SELECT count(*) FROM 
secret_table))"));
+    }
+
+    @Test
+    void testBypassInCaseWhen() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE status = (CASE WHEN (SELECT 1 
FROM secret_table)=1 THEN 1 ELSE 0 END)"));
+    }
+
+    @Test
+    void testBypassWithAndExpression() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE 1=1 AND id = (SELECT id FROM 
secret_table)"));
+    }
+
+    @Test
+    void testBypassWithGreaterThan() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE id > (SELECT count(*) FROM 
secret_table)"));
+    }
+
+    @Test
+    void testBypassWithBetween() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE id BETWEEN 1 AND (SELECT id 
FROM secret_table)"));
+    }
+
+    @Test
+    void testBypassWithMathOperations() {
+        assertThrows(SqlSecurityException.class, () -> validator.validate(
+            "SELECT * FROM hertzbeat_logs WHERE id = 1 + (SELECT id FROM 
secret_table)"));
+    }
+}
diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
index ad9cfb8253..45ad3f038f 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
@@ -68,4 +68,6 @@ public interface WarehouseConstants {
 
     String INSTANT = "instant";
 
+    String LOG_TABLE_NAME = "hertzbeat_logs";
+
 }
diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutor.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutor.java
index 7e4c64341b..9d874868c6 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutor.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutor.java
@@ -63,61 +63,64 @@ public class GreptimeSqlQueryExecutor extends 
SqlQueryExecutor {
     @Override
     public List<Map<String, Object>> execute(String queryString) {
         List<Map<String, Object>> results = new LinkedList<>();
-        try {
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
-            headers.setAccept(List.of(MediaType.APPLICATION_JSON));
-            if (StringUtils.hasText(greptimeProperties.username())
-                    && StringUtils.hasText(greptimeProperties.password())) {
-                String authStr = greptimeProperties.username() + ":" + 
greptimeProperties.password();
-                String encodedAuth = Base64Util.encode(authStr);
-                headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC 
+ SignConstants.BLANK + encodedAuth);
-            }
 
-            String requestBody = "sql=" + queryString;
-            HttpEntity<String> httpEntity = new HttpEntity<>(requestBody, 
headers);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        headers.setAccept(List.of(MediaType.APPLICATION_JSON));
+        if (StringUtils.hasText(greptimeProperties.username())
+                && StringUtils.hasText(greptimeProperties.password())) {
+            String authStr = greptimeProperties.username() + ":" + 
greptimeProperties.password();
+            String encodedAuth = Base64Util.encode(authStr);
+            headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC + 
SignConstants.BLANK + encodedAuth);
+        }
+
+        String requestBody = "sql=" + queryString;
+        HttpEntity<String> httpEntity = new HttpEntity<>(requestBody, headers);
 
-            String url = greptimeProperties.httpEndpoint() + QUERY_PATH;
-            if (StringUtils.hasText(greptimeProperties.database())) {
-                url += "?db=" + greptimeProperties.database();
-            }
+        String url = greptimeProperties.httpEndpoint() + QUERY_PATH;
+        if (StringUtils.hasText(greptimeProperties.database())) {
+            url += "?db=" + greptimeProperties.database();
+        }
 
-            ResponseEntity<GreptimeSqlQueryContent> responseEntity = 
restTemplate.exchange(url,
+        ResponseEntity<GreptimeSqlQueryContent> responseEntity;
+        try {
+            responseEntity = restTemplate.exchange(url,
                     HttpMethod.POST, httpEntity, 
GreptimeSqlQueryContent.class);
+        } catch (Exception e) {
+            log.error("Exception occurred while querying GreptimeDB SQL: {}", 
e.getMessage(), e);
+            throw new RuntimeException("Failed to execute GreptimeDB SQL 
query", e);
+        }
 
-            if (responseEntity.getStatusCode().is2xxSuccessful()) {
-                GreptimeSqlQueryContent responseBody = 
responseEntity.getBody();
-                if (responseBody != null && responseBody.getCode() == 0
-                        && responseBody.getOutput() != null && 
!responseBody.getOutput().isEmpty()) {
-
-                    for (GreptimeSqlQueryContent.Output output : 
responseBody.getOutput()) {
-                        if (output.getRecords() != null && 
output.getRecords().getRows() != null) {
-                            GreptimeSqlQueryContent.Output.Records.Schema 
schema = output.getRecords().getSchema();
-                            List<List<Object>> rows = 
output.getRecords().getRows();
-
-                            for (List<Object> row : rows) {
-                                Map<String, Object> rowMap = new HashMap<>();
-                                if (schema != null && 
schema.getColumnSchemas() != null) {
-                                    for (int i = 0; i < 
Math.min(schema.getColumnSchemas().size(), row.size()); i++) {
-                                        String columnName = 
schema.getColumnSchemas().get(i).getName();
-                                        Object value = row.get(i);
-                                        rowMap.put(columnName, value);
-                                    }
-                                } else {
-                                    for (int i = 0; i < row.size(); i++) {
-                                        rowMap.put("col_" + i, row.get(i));
-                                    }
+        if (responseEntity.getStatusCode().is2xxSuccessful()) {
+            GreptimeSqlQueryContent responseBody = responseEntity.getBody();
+            if (responseBody != null && responseBody.getCode() == 0
+                    && responseBody.getOutput() != null && 
!responseBody.getOutput().isEmpty()) {
+
+                for (GreptimeSqlQueryContent.Output output : 
responseBody.getOutput()) {
+                    if (output.getRecords() != null && 
output.getRecords().getRows() != null) {
+                        GreptimeSqlQueryContent.Output.Records.Schema schema = 
output.getRecords().getSchema();
+                        List<List<Object>> rows = 
output.getRecords().getRows();
+
+                        for (List<Object> row : rows) {
+                            Map<String, Object> rowMap = new HashMap<>();
+                            if (schema != null && schema.getColumnSchemas() != 
null) {
+                                for (int i = 0; i < 
Math.min(schema.getColumnSchemas().size(), row.size()); i++) {
+                                    String columnName = 
schema.getColumnSchemas().get(i).getName();
+                                    Object value = row.get(i);
+                                    rowMap.put(columnName, value);
+                                }
+                            } else {
+                                for (int i = 0; i < row.size(); i++) {
+                                    rowMap.put("col_" + i, row.get(i));
                                 }
-                                results.add(rowMap);
                             }
+                            results.add(rowMap);
                         }
                     }
                 }
-            } else {
-                log.error("query metrics data from greptime failed. {}", 
responseEntity);
             }
-        } catch (Exception e) {
-            log.error("query metrics data from greptime error. {}", 
e.getMessage(), e);
+        } else {
+            log.error("query metrics data from greptime failed. {}", 
responseEntity);
         }
         return results;
     }
diff --git 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
index 3fe4aafd31..ada81ec742 100644
--- 
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
+++ 
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
@@ -61,6 +61,7 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
 import org.apache.hertzbeat.common.util.Base64Util;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.common.util.TimePeriodUtil;
+import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
 import 
org.apache.hertzbeat.warehouse.store.history.tsdb.AbstractHistoryDataStorage;
 import org.apache.hertzbeat.warehouse.store.history.tsdb.vm.PromQlQueryContent;
 import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor;
@@ -90,7 +91,7 @@ public class GreptimeDbDataStorage extends 
AbstractHistoryDataStorage {
     private static final String LABEL_KEY_NAME = "__name__";
     private static final String LABEL_KEY_FIELD = "__field__";
     private static final String LABEL_KEY_INSTANCE = "instance";
-    private static final String LOG_TABLE_NAME = "hertzbeat_logs";
+    private static final String LOG_TABLE_NAME = 
WarehouseConstants.LOG_TABLE_NAME;
     private static final String LABEL_KEY_START_TIME = "start";
     private static final String LABEL_KEY_END_TIME = "end";
     private static final int LOG_BATCH_SIZE = 500;
diff --git 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java
 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java
index 99f83855ae..1445ed5f0c 100644
--- 
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java
+++ 
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java
@@ -21,7 +21,7 @@ package org.apache.hertzbeat.warehouse.db;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
@@ -62,7 +62,7 @@ class GreptimeSqlQueryExecutorTest {
         when(greptimeProperties.database()).thenReturn("hertzbeat");
         when(greptimeProperties.username()).thenReturn("username");
         when(greptimeProperties.password()).thenReturn("password");
-        
+
         greptimeSqlQueryExecutor = new 
GreptimeSqlQueryExecutor(greptimeProperties, restTemplate);
     }
 
@@ -70,7 +70,7 @@ class GreptimeSqlQueryExecutorTest {
     void testExecuteSuccess() {
         // Mock successful response
         GreptimeSqlQueryContent mockResponse = createMockResponse();
-        ResponseEntity<GreptimeSqlQueryContent> responseEntity = 
+        ResponseEntity<GreptimeSqlQueryContent> responseEntity =
             new ResponseEntity<>(mockResponse, HttpStatus.OK);
 
         when(restTemplate.exchange(
@@ -101,11 +101,7 @@ class GreptimeSqlQueryExecutorTest {
         )).thenThrow(new RuntimeException("Connection error"));
 
         // Execute
-        List<Map<String, Object>> result = 
greptimeSqlQueryExecutor.execute("SELECT * FROM metrics");
-
-        // Verify returns empty list on error
-        assertNotNull(result);
-        assertTrue(result.isEmpty());
+        assertThrows(RuntimeException.class, () -> 
greptimeSqlQueryExecutor.execute("SELECT * FROM metrics"));
     }
 
     private GreptimeSqlQueryContent createMockResponse() {
@@ -117,7 +113,7 @@ class GreptimeSqlQueryExecutorTest {
         columnSchemas.add(new 
GreptimeSqlQueryContent.Output.Records.Schema.ColumnSchema("metric_name", 
"String"));
         columnSchemas.add(new 
GreptimeSqlQueryContent.Output.Records.Schema.ColumnSchema("value", "Float64"));
 
-        GreptimeSqlQueryContent.Output.Records.Schema schema = 
+        GreptimeSqlQueryContent.Output.Records.Schema schema =
             new GreptimeSqlQueryContent.Output.Records.Schema();
         schema.setColumnSchemas(columnSchemas);
 
@@ -126,7 +122,7 @@ class GreptimeSqlQueryExecutorTest {
         rows.add(List.of("cpu", 85.5));
 
         // Build response structure
-        GreptimeSqlQueryContent.Output.Records records = 
+        GreptimeSqlQueryContent.Output.Records records =
             new GreptimeSqlQueryContent.Output.Records();
         records.setSchema(schema);
         records.setRows(rows);
diff --git a/material/licenses/backend/LICENSE 
b/material/licenses/backend/LICENSE
index 826b50efca..12f85ea0e7 100644
--- a/material/licenses/backend/LICENSE
+++ b/material/licenses/backend/LICENSE
@@ -212,6 +212,7 @@ The text of each license is the standard Apache 2.0 license.
     
https://mvnrepository.com/artifact/com.fasterxml.woodstox/woodstox-core/6.5.1 
Apache-2.0
     https://mvnrepository.com/artifact/com.fasterxml/classmate/1.6.0 Apache-2.0
     
https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine/2.9.3 
Apache-2.0
+    https://mvnrepository.com/artifact/com.github.jsqlparser/jsqlparser/5.1 
Apache-2.0
     https://mvnrepository.com/artifact/com.google.android/annotations/4.1.1.4 
Apache-2.0
     
https://mvnrepository.com/artifact/com.google.api.grpc/proto-google-common-protos/2.17.0
 Apache-2.0
     https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305/3.0.2 
Apache-2.0
diff --git a/pom.xml b/pom.xml
index 5897eb51b6..08ad1b0a1b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,7 +34,7 @@
     <url>https://hertzbeat.apache.org/</url>
 
     <description>
-        Apache HertzBeat™, a real-time observability system with agentless, 
performance cluster, prometheus-compatible, 
+        Apache HertzBeat™, a real-time observability system with agentless, 
performance cluster, prometheus-compatible,
         custom monitoring and status page building capabilities.
     </description>
 
@@ -116,6 +116,7 @@
         <springdoc.version>2.8.5</springdoc.version>
         
<spring-boot-starter-sureness.version>1.1.0</spring-boot-starter-sureness.version>
         <javaparser.version>3.26.1</javaparser.version>
+        <jsqlparser.version>5.1</jsqlparser.version>
         <nekohtml.version>1.9.22</nekohtml.version>
         <json-path.version>2.9.0</json-path.version>
         <gson.version>2.10.1</gson.version>
@@ -329,6 +330,11 @@
                 <artifactId>commons-jexl3</artifactId>
                 <version>${commons-jexl3}</version>
             </dependency>
+            <dependency>
+                <groupId>com.github.jsqlparser</groupId>
+                <artifactId>jsqlparser</artifactId>
+                <version>${jsqlparser.version}</version>
+            </dependency>
             <!-- database migration -->
             <dependency>
                 <groupId>org.flywaydb</groupId>
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 2cd18a9f29..f1e726e893 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
@@ -198,7 +198,7 @@
         </nz-form-control>
       </nz-form-item>
       <!-- Data Type Selection -->
-      <nz-form-item>
+      <nz-form-item *ngIf="isManageModalAdd">
         <nz-form-label nzSpan="7" nzRequired="true" nzFor="dataType" 
[nzTooltipTitle]="'alert.setting.datatype.tip' | i18n">
           {{ 'alert.setting.datatype' | i18n }}
         </nz-form-label>
@@ -733,19 +733,14 @@
       <nz-form-item *ngIf="define.type == 'periodic_log'">
         <nz-form-label [nzSpan]="7" [nzNoColon]="true" 
nzFor="periodic_expr"></nz-form-label>
         <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
-          <nz-textarea-count [nzMaxCharacterCount]="100">
-            <textarea
-              [(ngModel)]="define.expr"
-              required
-              rows="3"
-              nz-input
-              name="periodic_expr"
-              id="periodic_expr"
-              [placeholder]="'alert.setting.log.query.placeholder' | i18n"
-            >
-            </textarea>
-          </nz-textarea-count>
-          <button nz-button nzType="primary" style="width: 100%; 
margin-bottom: 10px" (click)="onPreviewLogExpr()">
+          <app-sql-editor
+            [(ngModel)]="define.expr"
+            name="periodic_expr"
+            [height]="'150px'"
+            tableName="hertzbeat_logs"
+            required
+          ></app-sql-editor>
+          <button nz-button nzType="primary" style="width: 100%; margin-top: 
10px; margin-bottom: 10px" (click)="onPreviewLogExpr()">
             <i nz-icon nzType="eye" nzTheme="outline"></i>
             {{ 'common.preview.button' | i18n }}
           </button>
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 776320e040..8e51fc531e 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
@@ -136,7 +136,7 @@ export class AlertSettingComponent implements OnInit {
   previewData: any[] = [];
   previewColumns: Array<{ title: string; key: string; width?: string }> = [];
   previewTableLoading = false;
-
+  private defaultSql = `SELECT count(*) AS errorCount FROM hertzbeat_logs 
WHERE time_unix_nano >= NOW() - INTERVAL '30 second' AND severity_text = 
'ERROR' HAVING count(*) > 2`;
   /**
    * Initialize log fields(todo: from backend api)
    */
@@ -346,7 +346,14 @@ export class AlertSettingComponent implements OnInit {
   }
 
   private updateAlertDefineType() {
-    // Combine main type with data type
+    // First: Reset form state when switching data source type
+    this.userExpr = '';
+    this.cascadeValues = [];
+    this.currentMetrics = [];
+    this.resetQbDataDefault();
+    this.clearPreview();
+
+    // Second: Init state when switching data source type
     if (this.alertType === 'realtime' && this.dataType === 'metric') {
       this.define.type = 'realtime_metric';
     } else if (this.alertType === 'realtime' && this.dataType === 'log') {
@@ -358,14 +365,9 @@ export class AlertSettingComponent implements OnInit {
     } else if (this.alertType === 'periodic' && this.dataType === 'log') {
       this.define.type = 'periodic_log';
       this.define.datasource = 'sql';
+      this.define.expr = this.defaultSql;
       this.updateLogQbConfig();
     }
-
-    // Reset form state when switching data source type
-    this.userExpr = '';
-    this.cascadeValues = [];
-    this.currentMetrics = [];
-    this.resetQbDataDefault();
   }
 
   onSelectTypeModalCancel() {
@@ -1027,6 +1029,9 @@ export class AlertSettingComponent implements OnInit {
   onManageModalCancel() {
     this.cascadeValues = [];
     this.isExpr = false;
+    if (this.isManageModalAdd) {
+      this.dataType = 'metric';
+    }
     this.resetQbDataDefault();
     this.isManageModalVisible = false;
   }
diff --git 
a/web-app/src/app/shared/components/sql-editor/sql-editor.component.html 
b/web-app/src/app/shared/components/sql-editor/sql-editor.component.html
new file mode 100644
index 0000000000..aab273844e
--- /dev/null
+++ b/web-app/src/app/shared/components/sql-editor/sql-editor.component.html
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.
+-->
+
+<div class="sql-editor-wrapper" [style.height]="height">
+  <nz-code-editor
+    class="sql-editor"
+    [ngModel]="code"
+    (ngModelChange)="onCodeChange($event)"
+    [nzEditorOption]="editorOption"
+    (nzEditorInitialized)="onEditorInit($event)"
+  ></nz-code-editor>
+</div>
diff --git 
a/web-app/src/app/shared/components/sql-editor/sql-editor.component.less 
b/web-app/src/app/shared/components/sql-editor/sql-editor.component.less
new file mode 100644
index 0000000000..b86b9dc9ca
--- /dev/null
+++ b/web-app/src/app/shared/components/sql-editor/sql-editor.component.less
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+.sql-editor-wrapper {
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  width: 100%;
+  position: relative;
+
+  &:hover {
+    border-color: #40a9ff;
+  }
+
+  &:focus-within {
+    border-color: #40a9ff;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+  }
+
+  .sql-editor {
+    height: 100%;
+    width: 100%;
+  }
+}
+
+:host {
+  display: block;
+  position: relative;
+}
+
+:host ::ng-deep {
+  .monaco-editor {
+    .margin {
+      background-color: #fafafa;
+    }
+  }
+
+  .monaco-editor .suggest-widget {
+    z-index: 9999 !important;
+
+    .suggest-widget-status-bar {
+      display: none !important;
+    }
+  }
+
+  .monaco-editor .editor-widget {
+    z-index: 9999 !important;
+  }
+
+  .monaco-editor .overflowingContentWidgets {
+    z-index: 9999 !important;
+  }
+}
diff --git 
a/web-app/src/app/shared/components/sql-editor/sql-editor.component.ts 
b/web-app/src/app/shared/components/sql-editor/sql-editor.component.ts
new file mode 100644
index 0000000000..ece97fc719
--- /dev/null
+++ b/web-app/src/app/shared/components/sql-editor/sql-editor.component.ts
@@ -0,0 +1,438 @@
+/*
+ * 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.
+ */
+
+import { Component, EventEmitter, forwardRef, Input, OnDestroy, Output } from 
'@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+declare const monaco: any;
+
+export interface SqlValidationError {
+  message: string;
+  startLine: number;
+  startColumn: number;
+  endLine: number;
+  endColumn: number;
+}
+
+export interface LogTableColumn {
+  name: string;
+  type: string;
+  description?: string;
+}
+
+@Component({
+  selector: 'app-sql-editor',
+  templateUrl: './sql-editor.component.html',
+  styleUrls: ['./sql-editor.component.less'],
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => SqlEditorComponent),
+      multi: true
+    }
+  ]
+})
+export class SqlEditorComponent implements OnDestroy, ControlValueAccessor {
+  @Input() height: string = '120px';
+  @Input() tableName: string = 'hertzbeat_logs';
+
+  @Output() readonly editorInit = new EventEmitter<any>();
+  @Output() readonly validationChange = new 
EventEmitter<SqlValidationError[]>();
+
+  code: string = '';
+  private editorInstance: any;
+  private validationTimeout: any;
+  editorOption: any = {
+    language: 'sql',
+    theme: 'vs',
+    minimap: { enabled: false },
+    lineNumbers: 'on',
+    scrollBeyondLastLine: false,
+    automaticLayout: true,
+    folding: false,
+    wordWrap: 'on',
+    fontSize: 13,
+    tabSize: 2,
+    suggestOnTriggerCharacters: true,
+    quickSuggestions: true,
+    wordBasedSuggestions: false,
+    fixedOverflowWidgets: true,
+    overviewRulerLanes: 0
+  };
+
+  private completionProvider: any;
+  private onChange: (value: string) => void = () => {};
+  private onTouched: () => void = () => {};
+
+  private readonly LOG_TABLE_COLUMNS: LogTableColumn[] = [
+    { name: 'time_unix_nano', type: 'TimestampNanosecond', description: 'Log 
timestamp in nanoseconds' },
+    { name: 'observed_time_unix_nano', type: 'TimestampNanosecond', 
description: 'Observed timestamp in nanoseconds' },
+    { name: 'severity_number', type: 'Int32', description: 'Severity level 
number (1-24)' },
+    { name: 'severity_text', type: 'String', description: 'Severity text 
(DEBUG, INFO, WARN, ERROR, etc.)' },
+    { name: 'body', type: 'Json', description: 'Log message body content' },
+    { name: 'trace_id', type: 'String', description: 'Distributed tracing 
trace ID' },
+    { name: 'span_id', type: 'String', description: 'Distributed tracing span 
ID' },
+    { name: 'trace_flags', type: 'Int32', description: 'Trace flags' },
+    { name: 'attributes', type: 'Json', description: 'Log attributes as JSON' 
},
+    { name: 'resource', type: 'Json', description: 'Resource information as 
JSON' },
+    { name: 'instrumentation_scope', type: 'Json', description: 
'Instrumentation scope information' },
+    { name: 'dropped_attributes_count', type: 'Int32', description: 'Number of 
dropped attributes' }
+  ];
+
+  ngOnDestroy(): void {
+    if (this.completionProvider) {
+      this.completionProvider.dispose();
+    }
+    if (this.validationTimeout) {
+      clearTimeout(this.validationTimeout);
+    }
+  }
+
+  onEditorInit(editor: any): void {
+    this.editorInstance = editor;
+    this.registerCompletionProvider();
+    this.configureSuggestWidget(editor);
+    this.editorInit.emit(editor);
+  }
+
+  private configureSuggestWidget(editor: any): void {
+    if (typeof monaco === 'undefined') {
+      return;
+    }
+    try {
+      editor.updateOptions({
+        suggest: {
+          maxVisibleSuggestions: 8,
+          showStatusBar: false,
+          preview: false
+        }
+      });
+    } catch (e) {
+      console.warn('Failed to configure suggest widget:', e);
+    }
+  }
+
+  writeValue(value: string): void {
+    this.code = value || '';
+  }
+
+  registerOnChange(fn: (value: string) => void): void {
+    this.onChange = fn;
+  }
+
+  registerOnTouched(fn: () => void): void {
+    this.onTouched = fn;
+  }
+
+  onCodeChange(value: string): void {
+    this.code = value;
+    this.onChange(value);
+    this.onTouched();
+    this.validateSqlWithDebounce(value);
+  }
+
+  private validateSqlWithDebounce(sql: string): void {
+    if (this.validationTimeout) {
+      clearTimeout(this.validationTimeout);
+    }
+    this.validationTimeout = setTimeout(() => {
+      const errors = this.validateSql(sql);
+      this.setEditorMarkers(errors);
+      this.validationChange.emit(errors);
+    }, 500);
+  }
+
+  /**
+   * SQL Security Validator - Simple regex-based validation
+   * Mirrors backend SqlSecurityValidator security policy:
+   * 1. Only SELECT statements are allowed
+   * 2. Table must be in whitelist
+   * 3. Dangerous patterns (subqueries, UNION, CTE) are blocked
+   * Note: This is for UX feedback only, real security is enforced by backend
+   */
+  private validateSql(sql: string): SqlValidationError[] {
+    const errors: SqlValidationError[] = [];
+    if (!sql || !sql.trim()) {
+      return errors;
+    }
+
+    const lines = sql.split('\n');
+    const lastLine = lines.length;
+    const lastCol = lines[lastLine - 1].length + 1;
+
+    // Remove string literals and comments to avoid false positives
+    const sanitizedSql = this.removeStringsAndComments(sql);
+    const upperSql = sanitizedSql.toUpperCase().trim();
+
+    // Helper to create error
+    const addError = (message: string) => {
+      errors.push({
+        message,
+        startLine: 1,
+        startColumn: 1,
+        endLine: lastLine,
+        endColumn: lastCol
+      });
+    };
+
+    // 1. Must start with SELECT
+    if (!upperSql.startsWith('SELECT')) {
+      addError('Only SELECT statements are allowed');
+      return errors;
+    }
+
+    // 2. Check for dangerous statements
+    const dangerousPatterns = [
+      { pattern: 
/\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE)\b/i, message: 
'Only SELECT statements are allowed' },
+      { pattern: /\bUNION\b/i, message: 'UNION is not allowed' },
+      { pattern: /\bINTERSECT\b/i, message: 'INTERSECT is not allowed' },
+      { pattern: /\bEXCEPT\b/i, message: 'EXCEPT is not allowed' },
+      { pattern: /^\s*WITH\b/i, message: 'CTE (WITH clause) is not allowed' }
+    ];
+
+    for (const { pattern, message } of dangerousPatterns) {
+      if (pattern.test(sanitizedSql)) {
+        addError(message);
+        return errors;
+      }
+    }
+
+    // 3. Check for subqueries (SELECT inside parentheses, but not in function 
calls)
+    if (this.hasSubquery(sanitizedSql)) {
+      addError('Subqueries are not allowed');
+      return errors;
+    }
+
+    // 4. Must have FROM clause
+    if (!/\bFROM\b/i.test(sanitizedSql)) {
+      addError('SQL query must contain FROM clause');
+      return errors;
+    }
+
+    // 5. Validate table name
+    const tableMatch = 
sanitizedSql.match(/\bFROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
+    if (tableMatch) {
+      const tableName = tableMatch[1];
+      if (tableName.toLowerCase() !== this.tableName.toLowerCase()) {
+        addError(`Access to table '${tableName}' is not allowed. Allowed 
table: ${this.tableName}`);
+      }
+    }
+
+    // 6. Basic syntax checks
+    const openParens = (sanitizedSql.match(/\(/g) || []).length;
+    const closeParens = (sanitizedSql.match(/\)/g) || []).length;
+    if (openParens !== closeParens) {
+      addError('Mismatched parentheses');
+    }
+
+    return errors;
+  }
+
+  /**
+   * Remove string literals and comments to avoid false positives in pattern 
matching
+   */
+  private removeStringsAndComments(sql: string): string {
+    return (
+      sql
+        // Remove single-quoted strings (handle escaped quotes)
+        .replace(/'(?:[^'\\]|\\.)*'/g, "''")
+        // Remove double-quoted strings
+        .replace(/"(?:[^"\\]|\\.)*"/g, '""')
+        // Remove single-line comments
+        .replace(/--.*$/gm, '')
+        // Remove multi-line comments
+        .replace(/\/\*[\s\S]*?\*\//g, '')
+    );
+  }
+
+  /**
+   * Check for subqueries (nested SELECT statements)
+   * Detects SELECT inside parentheses that's not part of IN(...values...)
+   */
+  private hasSubquery(sql: string): boolean {
+    // Pattern to detect SELECT inside parentheses
+    const subqueryPattern = /\(\s*SELECT\b/i;
+    return subqueryPattern.test(sql);
+  }
+
+  private setEditorMarkers(errors: SqlValidationError[]): void {
+    if (typeof monaco === 'undefined' || !this.editorInstance) {
+      return;
+    }
+
+    const model = this.editorInstance.getModel();
+    if (!model) {
+      return;
+    }
+
+    const markers = errors.map(error => ({
+      severity: monaco.MarkerSeverity.Error,
+      message: error.message,
+      startLineNumber: error.startLine,
+      startColumn: error.startColumn,
+      endLineNumber: error.endLine,
+      endColumn: error.endColumn
+    }));
+
+    monaco.editor.setModelMarkers(model, 'sql-validator', markers);
+  }
+
+  private registerCompletionProvider(): void {
+    if (typeof monaco === 'undefined') {
+      return;
+    }
+
+    if (this.completionProvider) {
+      this.completionProvider.dispose();
+    }
+
+    this.completionProvider = 
monaco.languages.registerCompletionItemProvider('sql', {
+      triggerCharacters: [' ', '.', ',', '(', '\n'],
+      provideCompletionItems: (model: any, position: any) => {
+        const textUntilPosition = model.getValueInRange({
+          startLineNumber: 1,
+          startColumn: 1,
+          endLineNumber: position.lineNumber,
+          endColumn: position.column
+        });
+
+        const word = model.getWordUntilPosition(position);
+        const range = {
+          startLineNumber: position.lineNumber,
+          endLineNumber: position.lineNumber,
+          startColumn: word.startColumn,
+          endColumn: word.endColumn
+        };
+
+        const suggestions: any[] = [];
+        const upperText = textUntilPosition.toUpperCase();
+
+        const isAfterFrom = /FROM\s+$/i.test(textUntilPosition) || 
/FROM\s+\w*$/i.test(textUntilPosition);
+        const isAfterSelect = /SELECT\s+$/i.test(textUntilPosition) || 
/SELECT\s+.*,\s*$/i.test(textUntilPosition);
+        const isAfterWhere =
+          /WHERE\s+$/i.test(textUntilPosition) || 
/AND\s+$/i.test(textUntilPosition) || /OR\s+$/i.test(textUntilPosition);
+        const hasTableContext = new RegExp(this.tableName, 
'i').test(textUntilPosition);
+        const isEmpty = !textUntilPosition.trim();
+
+        // Empty or start - show templates first
+        if (isEmpty) {
+          suggestions.push({
+            label: `SELECT * FROM ${this.tableName}`,
+            kind: monaco.languages.CompletionItemKind.Snippet,
+            insertText: `SELECT * FROM ${this.tableName} WHERE $0`,
+            insertTextRules: 
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+            detail: 'Query template',
+            sortText: '0',
+            range: range
+          });
+          suggestions.push({
+            label: `SELECT COUNT(*) FROM ${this.tableName}`,
+            kind: monaco.languages.CompletionItemKind.Snippet,
+            insertText: `SELECT COUNT(*) as count FROM ${this.tableName} WHERE 
$0`,
+            insertTextRules: 
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+            detail: 'Count template',
+            sortText: '1',
+            range: range
+          });
+        }
+
+        // After FROM - only show table name
+        if (isAfterFrom) {
+          suggestions.push({
+            label: this.tableName,
+            kind: monaco.languages.CompletionItemKind.Class,
+            insertText: this.tableName,
+            detail: 'Log table',
+            sortText: '0',
+            range: range
+          });
+          return { suggestions };
+        }
+
+        // After SELECT - show columns and *
+        if (isAfterSelect) {
+          suggestions.push({
+            label: '*',
+            kind: monaco.languages.CompletionItemKind.Operator,
+            insertText: '*',
+            detail: 'All columns',
+            sortText: '0',
+            range: range
+          });
+          this.LOG_TABLE_COLUMNS.forEach((column, idx) => {
+            suggestions.push({
+              label: column.name,
+              kind: monaco.languages.CompletionItemKind.Field,
+              insertText: column.name,
+              detail: column.type,
+              sortText: `1${idx}`,
+              range: range
+            });
+          });
+          return { suggestions };
+        }
+
+        // After WHERE/AND/OR - show columns for conditions
+        if (isAfterWhere || hasTableContext) {
+          this.LOG_TABLE_COLUMNS.forEach((column, idx) => {
+            suggestions.push({
+              label: column.name,
+              kind: monaco.languages.CompletionItemKind.Field,
+              insertText: column.name,
+              detail: column.type,
+              sortText: `0${idx}`,
+              range: range
+            });
+          });
+        }
+
+        // Show relevant keywords based on context
+        const keywordsToShow = this.getContextKeywords(upperText);
+        keywordsToShow.forEach((keyword, idx) => {
+          suggestions.push({
+            label: keyword,
+            kind: monaco.languages.CompletionItemKind.Keyword,
+            insertText: keyword,
+            detail: 'SQL',
+            sortText: `2${idx}`,
+            range: range
+          });
+        });
+
+        return { suggestions };
+      }
+    });
+  }
+
+  private getContextKeywords(upperText: string): string[] {
+    if (!upperText.includes('SELECT')) {
+      return ['SELECT'];
+    }
+    if (!upperText.includes('FROM')) {
+      return ['FROM'];
+    }
+    if (!upperText.includes('WHERE') && upperText.includes('FROM')) {
+      return ['WHERE', 'ORDER BY', 'LIMIT', 'GROUP BY'];
+    }
+    if (upperText.includes('WHERE')) {
+      return ['AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', 'IS NULL', 'IS NOT 
NULL', 'ORDER BY', 'LIMIT', 'GROUP BY'];
+    }
+    return ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'LIMIT'];
+  }
+}
diff --git a/web-app/src/app/shared/shared.module.ts 
b/web-app/src/app/shared/shared.module.ts
index e111363ddb..1c2735e285 100644
--- a/web-app/src/app/shared/shared.module.ts
+++ b/web-app/src/app/shared/shared.module.ts
@@ -9,6 +9,7 @@ import { DelonFormModule } from '@delon/form';
 import { AlainThemeModule } from '@delon/theme';
 import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb';
 import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCodeEditorModule } from 'ng-zorro-antd/code-editor';
 import { NzDividerComponent } from 'ng-zorro-antd/divider';
 import { NzIconModule } from 'ng-zorro-antd/icon';
 import { NzInputModule } from 'ng-zorro-antd/input';
@@ -28,6 +29,7 @@ import { LabelSelectorComponent } from 
'./components/label-selector/label-select
 import { MonitorSelectListComponent } from 
'./components/monitor-select-list/monitor-select-list.component';
 import { MonitorSelectMenuComponent } from 
'./components/monitor-select-menu/monitor-select-menu.component';
 import { MultiFuncInputComponent } from 
'./components/multi-func-input/multi-func-input.component';
+import { SqlEditorComponent } from 
'./components/sql-editor/sql-editor.component';
 import { ToolbarComponent } from './components/toolbar/toolbar.component';
 import { ElapsedTimePipe } from './pipe/elapsed-time.pipe';
 import { I18nElsePipe } from './pipe/i18n-else.pipe';
@@ -44,7 +46,8 @@ const COMPONENTS: Array<Type<void>> = [
   FormFieldComponent,
   MonitorSelectMenuComponent,
   MonitorSelectListComponent,
-  LabelSelectorComponent
+  LabelSelectorComponent,
+  SqlEditorComponent
 ];
 const DIRECTIVES: Array<Type<void>> = [TimezonePipe, I18nElsePipe, 
ElapsedTimePipe];
 
@@ -70,6 +73,7 @@ const DIRECTIVES: Array<Type<void>> = [TimezonePipe, 
I18nElsePipe, ElapsedTimePi
     NzInputModule,
     NzIconModule.forChild(icons),
     NzSpinModule,
+    NzCodeEditorModule,
     AiChatModule
   ],
   declarations: [...COMPONENTS, ...DIRECTIVES, HelpMessageShowComponent],
diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index 0e4e09a42c..9a89c2fef0 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -318,7 +318,6 @@
   "alert.setting.rule.string-value.place-holder": "Please input string",
   "alert.setting.log.query": "Log Query",
   "alert.setting.log.query.tip": "Configure log query conditions to filter 
logs that need to be monitored",
-  "alert.setting.log.query.placeholder": "Please enter log query conditions",
   "alert.setting.log.expr": "Log Alert Expression",
   "alert.setting.log.expr.tip": "Configure log alert trigger conditions based 
on query results",
   "alert.setting.log.expr.placeholder": "Please enter alert expression, e.g.: 
count > 10",
diff --git a/web-app/src/assets/i18n/ja-JP.json 
b/web-app/src/assets/i18n/ja-JP.json
index 485d539291..2c1d697e1d 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -303,7 +303,6 @@
   "alert.setting.rule.string-value.place-holder": "文字列を入力してください",
   "alert.setting.log.query": "ログクエリ",
   "alert.setting.log.query.tip": "監視するログをフィルタリングするためのログクエリ条件を設定",
-  "alert.setting.log.query.placeholder": "ログクエリ条件を入力してください",
   "alert.setting.log.expr": "ログアラート式",
   "alert.setting.log.expr.tip": "クエリ結果に基づいてログアラートトリガー条件を設定",
   "alert.setting.log.expr.placeholder": "アラート式を入力してください。例: count > 10",
diff --git a/web-app/src/assets/i18n/pt-BR.json 
b/web-app/src/assets/i18n/pt-BR.json
index f84f582eda..ac2bd8f590 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -38,7 +38,6 @@
   "alert.setting.rule.string-value.place-holder": "Digite o texto",
   "alert.setting.log.query": "Consulta de Log",
   "alert.setting.log.query.tip": "Configure condições de consulta de log para 
filtrar logs que precisam ser monitorados",
-  "alert.setting.log.query.placeholder": "Digite as condições de consulta de 
log",
   "alert.setting.log.expr": "Expressão de Alerta de Log",
   "alert.setting.log.expr.tip": "Configure condições de disparo de alerta de 
log baseadas nos resultados da consulta",
   "alert.setting.log.expr.placeholder": "Digite a expressão de alerta, ex: 
count > 10",
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index 31a7d4bcfc..ca614fefc9 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -318,7 +318,6 @@
   "alert.setting.rule.string-value.place-holder": "请输入匹配字符串",
   "alert.setting.log.query": "日志查询",
   "alert.setting.log.query.tip": "配置日志查询条件,用于筛选需要监控的日志",
-  "alert.setting.log.query.placeholder": "请输入日志查询条件",
   "alert.setting.log.expr": "日志告警表达式",
   "alert.setting.log.expr.tip": "配置日志告警触发条件,基于查询结果进行告警判断",
   "alert.setting.log.expr.placeholder": "请输入告警表达式,例如: count > 10",
diff --git a/web-app/src/assets/i18n/zh-TW.json 
b/web-app/src/assets/i18n/zh-TW.json
index 1dad50dc10..b5f277c382 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -298,7 +298,6 @@
   "alert.setting.rule.string-value.place-holder": "請輸入匹配字符串",
   "alert.setting.log.query": "日誌查詢",
   "alert.setting.log.query.tip": "配置日誌查詢條件,用於篩選需要監控的日誌",
-  "alert.setting.log.query.placeholder": "請輸入日誌查詢條件",
   "alert.setting.log.expr": "日誌告警表達式",
   "alert.setting.log.expr.tip": "配置日誌告警觸發條件,基於查詢結果進行告警判斷",
   "alert.setting.log.expr.placeholder": "請輸入告警表達式,例如: count > 10",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to