daShangDao_centerBook/health_api.go
2026-03-17 18:01:50 +08:00

393 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}