diff --git a/backend/internal/api/handlers/sql_audit.go b/backend/internal/api/handlers/sql_audit.go
new file mode 100644
index 0000000..2b0dd3e
--- /dev/null
+++ b/backend/internal/api/handlers/sql_audit.go
@@ -0,0 +1,435 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "ezops/internal/models"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// SQLAuditHandler SQL审计处理器 // Logan.Li
+type SQLAuditHandler struct {
+ db *gorm.DB
+}
+
+func NewSQLAuditHandler(db *gorm.DB) *SQLAuditHandler {
+ return &SQLAuditHandler{db: db}
+}
+
+// ListRecords 获取审计记录列表 // Logan.Li
+func (h *SQLAuditHandler) ListRecords(c *gin.Context) {
+ // 分页
+ page := 1
+ pageSize := 50
+ if c.Query("page") != "" {
+ fmt.Sscanf(c.Query("page"), "%d", &page)
+ }
+ if c.Query("page_size") != "" {
+ fmt.Sscanf(c.Query("page_size"), "%d", &pageSize)
+ }
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 500 {
+ pageSize = 50
+ }
+
+ query := h.db.Model(&models.SQLAuditRecord{})
+
+ // 筛选
+ if status := c.Query("status"); status != "" {
+ query = query.Where("status = ?", status)
+ }
+ if risk := c.Query("risk_level"); risk != "" {
+ query = query.Where("risk_level = ?", risk)
+ }
+ if keyword := c.Query("keyword"); keyword != "" {
+ query = query.Where("sql_content LIKE ? OR database_name LIKE ? OR executor LIKE ?",
+ "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
+ }
+
+ var total int64
+ query.Count(&total)
+
+ var records []models.SQLAuditRecord
+ query.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records)
+
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Message: "success",
+ Data: models.ListResponse{
+ List: records,
+ Total: total,
+ },
+ })
+}
+
+// GetRecord 获取审计记录详情 // Logan.Li
+func (h *SQLAuditHandler) GetRecord(c *gin.Context) {
+ id := c.Param("id")
+ var record models.SQLAuditRecord
+ if err := h.db.First(&record, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "记录不存在"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: record})
+}
+
+// AnalyzeSQL 分析SQL并创建审计记录 // Logan.Li
+func (h *SQLAuditHandler) AnalyzeSQL(c *gin.Context) {
+ var req models.AnalyzeSQLRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
+ return
+ }
+ if strings.TrimSpace(req.SQL) == "" {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "SQL不能为空"})
+ return
+ }
+
+ sqlUpper := strings.ToUpper(strings.TrimSpace(req.SQL))
+
+ // 判断 SQL 类型
+ sqlType := detectSQLType(sqlUpper)
+
+ // 提取表名
+ tableName := extractTableName(sqlUpper)
+
+ // 风险分析
+ riskLevel, matchedRules := h.analyzeRisk(sqlUpper)
+
+ // 获取当前用户
+ username := c.GetString("username")
+ if username == "" {
+ username = "system"
+ }
+
+ rulesJSON := "[]"
+ if len(matchedRules) > 0 {
+ rulesJSON = fmt.Sprintf(`["%s"]`, strings.Join(matchedRules, `","`))
+ }
+
+ record := models.SQLAuditRecord{
+ SQLContent: req.SQL,
+ SQLType: sqlType,
+ DatabaseName: req.DatabaseName,
+ TableName: tableName,
+ RiskLevel: riskLevel,
+ RiskRules: rulesJSON,
+ Status: "pending",
+ Executor: username,
+ Application: req.Application,
+ SourceIP: c.ClientIP(),
+ }
+
+ if err := h.db.Create(&record).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建审计记录失败"})
+ return
+ }
+
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Message: "分析完成",
+ Data: models.AnalyzeSQLResponse{
+ ID: int64(record.ID),
+ RiskLevel: riskLevel,
+ RiskRules: matchedRules,
+ SQLType: sqlType,
+ TableName: tableName,
+ },
+ })
+}
+
+// ApproveRecord 审批通过 // Logan.Li
+func (h *SQLAuditHandler) ApproveRecord(c *gin.Context) {
+ id := c.Param("id")
+ username := c.GetString("username")
+ if username == "" {
+ username = "admin"
+ }
+
+ result := h.db.Model(&models.SQLAuditRecord{}).Where("id = ? AND status = ?", id, "pending").
+ Updates(map[string]interface{}{"status": "approved", "approver": username})
+ if result.Error != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "审批失败"})
+ return
+ }
+ if result.RowsAffected == 0 {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "记录不存在或非待审批状态"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "审批通过"})
+}
+
+// RejectRecord 审批驳回 // Logan.Li
+func (h *SQLAuditHandler) RejectRecord(c *gin.Context) {
+ id := c.Param("id")
+ username := c.GetString("username")
+ if username == "" {
+ username = "admin"
+ }
+
+ result := h.db.Model(&models.SQLAuditRecord{}).Where("id = ? AND status = ?", id, "pending").
+ Updates(map[string]interface{}{"status": "rejected", "approver": username})
+ if result.Error != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "驳回失败"})
+ return
+ }
+ if result.RowsAffected == 0 {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "记录不存在或非待审批状态"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已驳回"})
+}
+
+// ExecuteRecord 执行SQL(模拟) // Logan.Li
+func (h *SQLAuditHandler) ExecuteRecord(c *gin.Context) {
+ id := c.Param("id")
+
+ var record models.SQLAuditRecord
+ if err := h.db.First(&record, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "记录不存在"})
+ return
+ }
+
+ if record.Status != "approved" {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "只能执行已审批的SQL"})
+ return
+ }
+
+ now := time.Now()
+ // 模拟执行(真实环境需连接目标数据库执行)
+ h.db.Model(&record).Updates(map[string]interface{}{
+ "status": "executed",
+ "execute_time": now,
+ "affected_rows": 0,
+ "execution_time": 10,
+ })
+
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "SQL执行成功"})
+}
+
+// DeleteRecord 删除审计记录 // Logan.Li
+func (h *SQLAuditHandler) DeleteRecord(c *gin.Context) {
+ id := c.Param("id")
+ if err := h.db.Delete(&models.SQLAuditRecord{}, id).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
+}
+
+// ==================== 审计规则管理 ====================
+
+// ListRules 获取审计规则列表 // Logan.Li
+func (h *SQLAuditHandler) ListRules(c *gin.Context) {
+ var rules []models.SQLAuditRule
+ h.db.Order("created_at DESC").Find(&rules)
+
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Message: "success",
+ Data: models.ListResponse{
+ List: rules,
+ Total: int64(len(rules)),
+ },
+ })
+}
+
+// CreateRule 创建审计规则 // Logan.Li
+func (h *SQLAuditHandler) CreateRule(c *gin.Context) {
+ var rule models.SQLAuditRule
+ if err := c.ShouldBindJSON(&rule); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
+ return
+ }
+ if rule.Name == "" {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "规则名称不能为空"})
+ return
+ }
+ if rule.Severity == "" {
+ rule.Severity = "medium"
+ }
+ rule.Enabled = true
+
+ if err := h.db.Create(&rule).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建规则失败"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已创建", Data: rule})
+}
+
+// UpdateRule 更新审计规则 // Logan.Li
+func (h *SQLAuditHandler) UpdateRule(c *gin.Context) {
+ id := c.Param("id")
+ var rule models.SQLAuditRule
+ if err := h.db.First(&rule, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"})
+ return
+ }
+
+ var req models.SQLAuditRule
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
+ return
+ }
+
+ h.db.Model(&rule).Updates(map[string]interface{}{
+ "name": req.Name,
+ "description": req.Description,
+ "pattern": req.Pattern,
+ "severity": req.Severity,
+ "category": req.Category,
+ "enabled": req.Enabled,
+ })
+
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已更新", Data: rule})
+}
+
+// DeleteRule 删除审计规则 // Logan.Li
+func (h *SQLAuditHandler) DeleteRule(c *gin.Context) {
+ id := c.Param("id")
+ if err := h.db.Delete(&models.SQLAuditRule{}, id).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已删除"})
+}
+
+// ToggleRule 启用/禁用规则 // Logan.Li
+func (h *SQLAuditHandler) ToggleRule(c *gin.Context) {
+ id := c.Param("id")
+ var rule models.SQLAuditRule
+ if err := h.db.First(&rule, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"})
+ return
+ }
+ h.db.Model(&rule).Update("enabled", !rule.Enabled)
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "状态已更新"})
+}
+
+// ==================== 辅助函数 ====================
+
+// detectSQLType 检测SQL类型 // Logan.Li
+func detectSQLType(sqlUpper string) string {
+ switch {
+ case strings.HasPrefix(sqlUpper, "DROP"):
+ return "DDL-DROP"
+ case strings.HasPrefix(sqlUpper, "TRUNCATE"):
+ return "DDL-TRUNCATE"
+ case strings.HasPrefix(sqlUpper, "ALTER"):
+ return "DDL-ALTER"
+ case strings.HasPrefix(sqlUpper, "CREATE"):
+ return "DDL-CREATE"
+ case strings.HasPrefix(sqlUpper, "DELETE"):
+ return "DML-DELETE"
+ case strings.HasPrefix(sqlUpper, "UPDATE"):
+ return "DML-UPDATE"
+ case strings.HasPrefix(sqlUpper, "INSERT"):
+ return "DML-INSERT"
+ case strings.HasPrefix(sqlUpper, "SELECT"):
+ return "DQL"
+ case strings.HasPrefix(sqlUpper, "GRANT"), strings.HasPrefix(sqlUpper, "REVOKE"):
+ return "DCL"
+ default:
+ return "OTHER"
+ }
+}
+
+// extractTableName 提取表名 // Logan.Li
+func extractTableName(sqlUpper string) string {
+ // 匹配 FROM/INTO/UPDATE/TABLE 后面的表名
+ patterns := []string{
+ `FROM\s+(\w+)`,
+ `INTO\s+(\w+)`,
+ `UPDATE\s+(\w+)`,
+ `TABLE\s+(\w+)`,
+ `JOIN\s+(\w+)`,
+ }
+ for _, p := range patterns {
+ re := regexp.MustCompile(p)
+ m := re.FindStringSubmatch(sqlUpper)
+ if len(m) > 1 {
+ return strings.ToLower(m[1])
+ }
+ }
+ return ""
+}
+
+// analyzeRisk 分析SQL风险等级 // Logan.Li
+func (h *SQLAuditHandler) analyzeRisk(sqlUpper string) (string, []string) {
+ // 从数据库加载启用的规则
+ var rules []models.SQLAuditRule
+ h.db.Where("enabled = ?", true).Find(&rules)
+
+ // 如果没有数据库规则,使用内置规则
+ if len(rules) == 0 {
+ rules = builtinRules()
+ }
+
+ var matched []string
+ maxSeverity := "safe"
+
+ for _, rule := range rules {
+ if matchRule(sqlUpper, rule) {
+ matched = append(matched, rule.Name)
+ if severityRank(rule.Severity) > severityRank(maxSeverity) {
+ maxSeverity = rule.Severity
+ }
+ }
+ }
+
+ return maxSeverity, matched
+}
+
+// matchRule 检查SQL是否匹配规则 // Logan.Li
+func matchRule(sqlUpper string, rule models.SQLAuditRule) string {
+ // 按 category 快速匹配
+ if rule.Category != "" {
+ if strings.Contains(sqlUpper, strings.ToUpper(rule.Category)) {
+ return rule.Name
+ }
+ }
+ // 按 pattern 正则匹配
+ if rule.Pattern != "" {
+ if re, err := regexp.Compile("(?i)" + rule.Pattern); err == nil {
+ if re.MatchString(sqlUpper) {
+ return rule.Name
+ }
+ }
+ }
+ return ""
+}
+
+// severityRank 风险等级排序 // Logan.Li
+func severityRank(s string) int {
+ switch s {
+ case "high":
+ return 4
+ case "medium":
+ return 3
+ case "low":
+ return 2
+ default:
+ return 1
+ }
+}
+
+// builtinRules 内置审计规则 // Logan.Li
+func builtinRules() []models.SQLAuditRule {
+ return []models.SQLAuditRule{
+ {Name: "DROP TABLE", Category: "DROP TABLE", Severity: "high", Description: "删除表操作,不可恢复"},
+ {Name: "TRUNCATE TABLE", Category: "TRUNCATE", Severity: "high", Description: "清空表数据"},
+ {Name: "DELETE 无 WHERE", Pattern: `DELETE\s+FROM\s+\w+\s*(?:;|$)`, Severity: "high", Description: "无条件的DELETE操作"},
+ {Name: "UPDATE 无 WHERE", Pattern: `UPDATE\s+\w+\s+SET.*(?:;|$)`, Severity: "high", Description: "无条件的UPDATE操作"},
+ {Name: "ALTER TABLE", Category: "ALTER", Severity: "medium", Description: "修改表结构"},
+ {Name: "DROP DATABASE", Category: "DROP DATABASE", Severity: "high", Description: "删除数据库"},
+ {Name: "GRANT/REVOKE", Category: "GRANT", Severity: "medium", Description: "权限变更操作"},
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b895e2f..4906d92 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -32,6 +32,8 @@ import { AIFaultDiagnosis } from '@/components/aiops/AIFaultDiagnosis';
import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics';
import LogManager from '@/components/monitor/logs/LogManager';
import AlertCenter from '@/components/monitor/alert-center/AlertCenter';
+import TraceManager from '@/components/monitor/trace/TraceManager';
+import GrafanaDashboard from '@/components/monitor/dashboard/GrafanaDashboard';
import { AgentMonitor } from '@/components/agent/AgentMonitor';
import { AgentAlerts } from '@/components/agent/AgentAlerts';
@@ -73,6 +75,8 @@ function App() {
} />
} />
} />
+ } />
+ } />
} />
} />
} />
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index b7dad1e..966676d 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -32,6 +32,7 @@ import {
Activity,
BarChart3,
GitBranch,
+ Monitor,
Bot,
AlertTriangle,
} from 'lucide-react';
@@ -90,6 +91,8 @@ const menuItems: MenuItem[] = [
{ id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' },
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' },
{ id: 'logs-monitor', name: '日志能力', icon: FileCode, path: '/logs-monitor' },
+ { id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' },
+ { id: 'grafana', name: '监控大屏', icon: Monitor, path: '/grafana' },
{ id: 'agent-alerts', name: 'Agent告警', icon: AlertTriangle, path: '/agent-alerts' },
],
},