393 lines
14 KiB
Go
393 lines
14 KiB
Go
package main
|
||
|
||
import (
|
||
"github.com/gin-gonic/gin"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
)
|
||
|
||
// SQLHealthController SQL健康监控控制器
|
||
type SQLHealthController struct{}
|
||
|
||
// NewSQLHealthController 创建SQL健康监控控制器
|
||
func NewSQLHealthController() *SQLHealthController {
|
||
return &SQLHealthController{}
|
||
}
|
||
|
||
// GetSQLStats 获取SQL执行统计信息
|
||
func (shc *SQLHealthController) GetSQLStats(c *gin.Context) {
|
||
if globalSQLMonitor == nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"error": "SQL监控器未初始化",
|
||
})
|
||
return
|
||
}
|
||
|
||
stats := globalSQLMonitor.GetStats()
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": stats,
|
||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// GetRecentSQLRecords 获取最近的SQL执行记录
|
||
func (shc *SQLHealthController) GetRecentSQLRecords(c *gin.Context) {
|
||
if globalSQLMonitor == nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"error": "SQL监控器未初始化",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 获取limit参数,默认50条
|
||
limitStr := c.DefaultQuery("limit", "50")
|
||
limit, err := strconv.Atoi(limitStr)
|
||
if err != nil || limit <= 0 {
|
||
limit = 50
|
||
}
|
||
if limit > 500 {
|
||
limit = 500 // 最大限制500条
|
||
}
|
||
|
||
records := globalSQLMonitor.GetRecentRecords(limit)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": gin.H{
|
||
"records": records,
|
||
"count": len(records),
|
||
"limit": limit,
|
||
},
|
||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// GetSlowQueries 获取慢查询
|
||
func (shc *SQLHealthController) GetSlowQueries(c *gin.Context) {
|
||
if globalSQLMonitor == nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"error": "SQL监控器未初始化",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 获取阈值参数,默认1000ms
|
||
thresholdStr := c.DefaultQuery("threshold", "1000")
|
||
threshold, err := strconv.ParseInt(thresholdStr, 10, 64)
|
||
if err != nil || threshold <= 0 {
|
||
threshold = 1000
|
||
}
|
||
|
||
slowQueries := globalSQLMonitor.GetSlowQueries(threshold)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": gin.H{
|
||
"slow_queries": slowQueries,
|
||
"count": len(slowQueries),
|
||
"threshold_ms": threshold,
|
||
},
|
||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// GetFailedQueries 获取失败的查询
|
||
func (shc *SQLHealthController) GetFailedQueries(c *gin.Context) {
|
||
if globalSQLMonitor == nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"error": "SQL监控器未初始化",
|
||
})
|
||
return
|
||
}
|
||
|
||
failedQueries := globalSQLMonitor.GetFailedQueries()
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": gin.H{
|
||
"failed_queries": failedQueries,
|
||
"count": len(failedQueries),
|
||
},
|
||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// ClearSQLRecords 清空SQL记录
|
||
func (shc *SQLHealthController) ClearSQLRecords(c *gin.Context) {
|
||
if globalSQLMonitor == nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"error": "SQL监控器未初始化",
|
||
})
|
||
return
|
||
}
|
||
|
||
globalSQLMonitor.ClearRecords()
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"message": "SQL记录已清空",
|
||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// GetSQLHealthDashboard 获取SQL健康监控仪表板
|
||
func (shc *SQLHealthController) GetSQLHealthDashboard(c *gin.Context) {
|
||
dashboardHTML := `
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>SQL健康监控仪表板</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
|
||
.container { max-width: 1400px; margin: 0 auto; }
|
||
.card { background: white; border-radius: 8px; padding: 20px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
|
||
.stat-item { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 6px; }
|
||
.stat-value { font-size: 24px; font-weight: bold; color: #007bff; }
|
||
.stat-label { font-size: 14px; color: #666; margin-top: 5px; }
|
||
.success { color: #28a745; }
|
||
.warning { color: #ffc107; }
|
||
.danger { color: #dc3545; }
|
||
.btn { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
|
||
.btn:hover { background: #0056b3; }
|
||
.btn-danger { background: #dc3545; }
|
||
.btn-danger:hover { background: #c82333; }
|
||
.table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
|
||
.table th, .table td { padding: 6px; text-align: left; border-bottom: 1px solid #ddd; }
|
||
.table th { background-color: #f8f9fa; font-weight: bold; }
|
||
.query-text { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: monospace; }
|
||
.auto-refresh { margin: 10px 0; }
|
||
.controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||
.filter-input { padding: 5px; border: 1px solid #ddd; border-radius: 4px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>SQL健康监控仪表板</h1>
|
||
|
||
<div class="controls">
|
||
<label>
|
||
<input type="checkbox" id="autoRefresh" checked> 自动刷新 (10秒)
|
||
</label>
|
||
<button class="btn" onclick="refreshData()">立即刷新</button>
|
||
<button class="btn btn-danger" onclick="clearRecords()">清空记录</button>
|
||
<input type="number" id="limitInput" class="filter-input" placeholder="记录数量" value="50" min="1" max="500">
|
||
<button class="btn" onclick="updateLimit()">更新数量</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>SQL执行统计</h2>
|
||
<div class="stats-grid" id="statsGrid">
|
||
<!-- 统计数据将在这里动态加载 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>最近SQL执行记录</h2>
|
||
<div id="recentRecords">
|
||
<!-- SQL记录将在这里动态加载 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>慢查询 (>1000ms)</h2>
|
||
<div id="slowQueries">
|
||
<!-- 慢查询将在这里动态加载 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>失败查询</h2>
|
||
<div id="failedQueries">
|
||
<!-- 失败查询将在这里动态加载 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let autoRefreshInterval;
|
||
let currentLimit = 50;
|
||
|
||
function refreshData() {
|
||
loadStats();
|
||
loadRecentRecords();
|
||
loadSlowQueries();
|
||
loadFailedQueries();
|
||
}
|
||
|
||
function loadStats() {
|
||
fetch('/api/sql-health/stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const stats = data.data;
|
||
const statsGrid = document.getElementById('statsGrid');
|
||
statsGrid.innerHTML = ` + "`" + `
|
||
<div class="stat-item">
|
||
<div class="stat-value">${stats.total_queries}</div>
|
||
<div class="stat-label">总查询数</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value success">${stats.success_queries}</div>
|
||
<div class="stat-label">成功查询</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value danger">${stats.failed_queries}</div>
|
||
<div class="stat-label">失败查询</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value ${parseFloat(stats.success_rate) >= 95 ? 'success' : parseFloat(stats.success_rate) >= 90 ? 'warning' : 'danger'}">${stats.success_rate}%</div>
|
||
<div class="stat-label">成功率</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${stats.avg_duration_ms}ms</div>
|
||
<div class="stat-label">平均耗时</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${stats.max_duration_ms}ms</div>
|
||
<div class="stat-label">最大耗时</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">${stats.min_duration_ms}ms</div>
|
||
<div class="stat-label">最小耗时</div>
|
||
</div>
|
||
` + "`" + `;
|
||
})
|
||
.catch(error => console.error('加载统计数据失败:', error));
|
||
}
|
||
|
||
function loadRecentRecords() {
|
||
fetch(` + "`" + `/api/sql-health/recent?limit=${currentLimit}` + "`" + `)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const records = data.data.records;
|
||
const container = document.getElementById('recentRecords');
|
||
|
||
if (!records || records.length === 0) {
|
||
container.innerHTML = '<p>暂无记录</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>耗时(ms)</th><th>时间</th><th>接口</th><th>状态</th><th>影响行数</th></tr></thead><tbody>';
|
||
records.forEach(record => {
|
||
html += ` + "`" + `<tr>
|
||
<td>${record.id}</td>
|
||
<td class="query-text" title="${record.query}">${record.query}</td>
|
||
<td class="${record.duration_ms > 1000 ? 'danger' : record.duration_ms > 500 ? 'warning' : ''}">${record.duration_ms}</td>
|
||
<td>${new Date(record.timestamp).toLocaleString()}</td>
|
||
<td>${record.endpoint}</td>
|
||
<td class="${record.success ? 'success' : 'danger'}">${record.success ? '成功' : '失败'}</td>
|
||
<td>${record.rows_affected || 0}</td>
|
||
</tr>` + "`" + `;
|
||
});
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
})
|
||
.catch(error => console.error('加载记录失败:', error));
|
||
}
|
||
|
||
function loadSlowQueries() {
|
||
fetch('/api/sql-health/slow-queries?threshold=1000')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const slowQueries = data.data.slow_queries;
|
||
const container = document.getElementById('slowQueries');
|
||
|
||
if (!slowQueries || slowQueries.length === 0) {
|
||
container.innerHTML = '<p>暂无慢查询</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>耗时(ms)</th><th>时间</th><th>接口</th></tr></thead><tbody>';
|
||
slowQueries.forEach(query => {
|
||
html += ` + "`" + `<tr>
|
||
<td>${query.id}</td>
|
||
<td class="query-text" title="${query.query}">${query.query}</td>
|
||
<td class="danger">${query.duration_ms}</td>
|
||
<td>${new Date(query.timestamp).toLocaleString()}</td>
|
||
<td>${query.endpoint}</td>
|
||
</tr>` + "`" + `;
|
||
});
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
})
|
||
.catch(error => console.error('加载慢查询失败:', error));
|
||
}
|
||
|
||
function loadFailedQueries() {
|
||
fetch('/api/sql-health/failed-queries')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const failedQueries = data.data.failed_queries;
|
||
const container = document.getElementById('failedQueries');
|
||
|
||
if (!failedQueries || failedQueries.length === 0) {
|
||
container.innerHTML = '<p>暂无失败查询</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>错误信息</th><th>时间</th><th>接口</th></tr></thead><tbody>';
|
||
failedQueries.forEach(query => {
|
||
html += ` + "`" + `<tr>
|
||
<td>${query.id}</td>
|
||
<td class="query-text" title="${query.query}">${query.query}</td>
|
||
<td class="danger">${query.error}</td>
|
||
<td>${new Date(query.timestamp).toLocaleString()}</td>
|
||
<td>${query.endpoint}</td>
|
||
</tr>` + "`" + `;
|
||
});
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
})
|
||
.catch(error => console.error('加载失败查询失败:', error));
|
||
}
|
||
|
||
function clearRecords() {
|
||
if (confirm('确定要清空所有SQL记录吗?')) {
|
||
fetch('/api/sql-health/clear', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
alert('记录已清空');
|
||
refreshData();
|
||
})
|
||
.catch(error => console.error('清空记录失败:', error));
|
||
}
|
||
}
|
||
|
||
function updateLimit() {
|
||
const limitInput = document.getElementById('limitInput');
|
||
const newLimit = parseInt(limitInput.value);
|
||
if (newLimit > 0 && newLimit <= 500) {
|
||
currentLimit = newLimit;
|
||
loadRecentRecords();
|
||
} else {
|
||
alert('记录数量必须在1-500之间');
|
||
}
|
||
}
|
||
|
||
function toggleAutoRefresh() {
|
||
const checkbox = document.getElementById('autoRefresh');
|
||
if (checkbox.checked) {
|
||
autoRefreshInterval = setInterval(refreshData, 10000);
|
||
} else {
|
||
clearInterval(autoRefreshInterval);
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
document.getElementById('autoRefresh').addEventListener('change', toggleAutoRefresh);
|
||
refreshData();
|
||
toggleAutoRefresh();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`
|
||
|
||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||
c.String(http.StatusOK, dashboardHTML)
|
||
}
|