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

774 lines
33 KiB
Go

package monitor
import (
"net/http"
"sort"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
// APIMonitorController API 监控控制器
type APIMonitorController struct{}
// NewAPIMonitorController 创建 API 监控控制器
func NewAPIMonitorController() *APIMonitorController {
return &APIMonitorController{}
}
// GetAPIStats 获取指定 API 的统计信息
func (amc *APIMonitorController) GetAPIStats(c *gin.Context) {
endpoint := c.Query("endpoint")
if endpoint == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少 endpoint 参数",
})
return
}
apiMonitorsMu.RLock()
monitor, exists := apiMonitors[endpoint]
apiMonitorsMu.RUnlock()
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "该接口暂无监控数据",
})
return
}
stats := monitor.GetStats()
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": stats,
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
})
}
// GetAllAPIStats 获取所有 API 的统计信息
func (amc *APIMonitorController) GetAllAPIStats(c *gin.Context) {
apiMonitorsMu.RLock()
defer apiMonitorsMu.RUnlock()
allStats := make([]*APIStats, 0, len(apiMonitors))
endpoints := make([]string, 0, len(apiMonitors))
// 收集所有 endpoint
for endpoint := range apiMonitors {
endpoints = append(endpoints, endpoint)
}
// 按 endpoint 字母顺序排序
sort.Strings(endpoints)
// 按排序后的顺序获取统计信息
for _, endpoint := range endpoints {
allStats = append(allStats, apiMonitors[endpoint].GetStats())
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": allStats,
"count": len(allStats),
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
})
}
// GetAPICallDetail 获取指定 API 调用的详细信息
func (amc *APIMonitorController) GetAPICallDetail(c *gin.Context) {
endpoint := c.Query("endpoint")
callIDStr := c.Query("call_id")
callType := c.Query("type") // es or redis
if endpoint == "" || callIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少 endpoint 或 call_id 参数",
})
return
}
callID, err := strconv.ParseInt(callIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "无效的 call_id",
})
return
}
apiMonitorsMu.RLock()
monitor, exists := apiMonitors[endpoint]
apiMonitorsMu.RUnlock()
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "该接口暂无监控数据",
})
return
}
// 查找对应的调用记录
var foundRecord *APICallRecord
var recordType string
if callType == "es" {
foundRecord = monitor.GetESCallByID(callID)
if foundRecord != nil {
recordType = "ES"
}
} else if callType == "redis" {
foundRecord = monitor.GetRedisCallByID(callID)
if foundRecord != nil {
recordType = "Redis"
}
} else {
c.JSON(http.StatusBadRequest, gin.H{
"error": "type 参数必须是 es 或 redis",
})
return
}
if foundRecord == nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "未找到对应的调用记录",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": gin.H{
"id": foundRecord.ID,
"type": recordType,
"timestamp": foundRecord.Timestamp.Format("2006-01-02 15:04:05"),
"duration_ms": foundRecord.Duration,
"success": foundRecord.Success,
"error": foundRecord.Error,
"operation": foundRecord.Operation,
"key_or_index": foundRecord.KeyOrIndex,
"query": foundRecord.Query,
"request": foundRecord.Request,
"response": foundRecord.Response,
},
})
}
// GetAPIESCalls 获取指定 API 的 ES 调用记录
func (amc *APIMonitorController) GetAPIESCalls(c *gin.Context) {
endpoint := c.Query("endpoint")
if endpoint == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少 endpoint 参数",
})
return
}
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "50")
page, err := strconv.Atoi(pageStr)
if err != nil || page <= 0 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize <= 0 {
pageSize = 50
}
if pageSize > 500 {
pageSize = 500
}
apiMonitorsMu.RLock()
monitor, exists := apiMonitors[endpoint]
apiMonitorsMu.RUnlock()
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "该接口暂无监控数据",
})
return
}
calls, total := monitor.GetRecentESCalls(page, pageSize)
totalPages := (total + pageSize - 1) / pageSize
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": gin.H{
"calls": calls,
"count": len(calls),
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": totalPages,
},
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
})
}
// GetAPIRedisCalls 获取指定 API 的 Redis 调用记录
func (amc *APIMonitorController) GetAPIRedisCalls(c *gin.Context) {
endpoint := c.Query("endpoint")
if endpoint == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少 endpoint 参数",
})
return
}
pageStr := c.DefaultQuery("page", "1")
pageSizeStr := c.DefaultQuery("page_size", "50")
page, err := strconv.Atoi(pageStr)
if err != nil || page <= 0 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize <= 0 {
pageSize = 50
}
if pageSize > 500 {
pageSize = 500
}
apiMonitorsMu.RLock()
monitor, exists := apiMonitors[endpoint]
apiMonitorsMu.RUnlock()
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "该接口暂无监控数据",
})
return
}
calls, total := monitor.GetRecentRedisCalls(page, pageSize)
totalPages := (total + pageSize - 1) / pageSize
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": gin.H{
"calls": calls,
"count": len(calls),
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": totalPages,
},
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
})
}
// GetAPIMonitorDashboard 获取 API 监控仪表板
func (amc *APIMonitorController) GetAPIMonitorDashboard(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>API 监控仪表板</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 1800px; 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(200px, 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; }
.table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
.table th, .table td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
.table th { background-color: #f8f9fa; font-weight: bold; }
.query-text { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: monospace; cursor: pointer; }
.query-text:hover { background-color: #e9ecef; }
.endpoint-card { border-left: 4px solid #007bff; }
.qps-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.qps-high { background: #dc3545; color: white; }
.qps-medium { background: #ffc107; color: black; }
.qps-low { background: #28a745; color: white; }
.tab { display: flex; gap: 10px; margin-bottom: 15px; }
.tab-btn { padding: 8px 16px; border: none; background: #e9ecef; cursor: pointer; border-radius: 4px; }
.tab-btn.active { background: #007bff; color: white; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; margin: 2% auto; padding: 20px; border-radius: 8px; width: 90%; max-height: 90vh; overflow-y: auto; }
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.close:hover { color: black; }
.detail-section { margin: 15px 0; }
.detail-label { font-weight: bold; color: #333; margin-bottom: 5px; }
.detail-value { background: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-break: break-word; max-height: 300px; overflow-y: auto; }
.json-viewer { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 4px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; line-height: 1.5; }
.loading { text-align: center; padding: 20px; color: #666; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 15px; }
.pagination button { padding: 6px 12px; border: 1px solid #007bff; background: white; color: #007bff; cursor: pointer; border-radius: 4px; }
.pagination button:hover:not(:disabled) { background: #007bff; color: white; }
.pagination button:disabled { border-color: #ccc; color: #ccc; cursor: not-allowed; }
.pagination button.active { background: #007bff; color: white; }
.pagination-info { margin: 0 10px; font-size: 13px; color: #666; }
.page-size-selector { margin-left: 10px; }
.page-size-selector select { padding: 6px; border: 1px solid #007bff; border-radius: 4px; color: #007bff; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>🔍 API 监控仪表板 - ES & Redis 调用</h1>
<div class="controls">
<button class="btn" onclick="refreshData()">立即刷新</button>
<label>
<input type="checkbox" id="autoRefresh" checked> 自动刷新 (5 秒)
</label>
</div>
<div class="card">
<h2>所有接口概览</h2>
<div id="endpointsList"></div>
</div>
<div class="card endpoint-card">
<h2>详细监控数据</h2>
<div class="tab">
<button class="tab-btn active" onclick="switchTab('stats', event)">📊 统计信息</button>
<button class="tab-btn" onclick="switchTab('es-calls', event)">🔵 ES 调用</button>
<button class="tab-btn" onclick="switchTab('redis-calls', event)">🔴 Redis 调用</button>
</div>
<div id="statsTab" class="tab-content active">
<div class="stats-grid" id="statsGrid"></div>
</div>
<div id="es-callsTab" class="tab-content">
<div id="esCallsContent"></div>
<div id="esCallsPagination" class="pagination"></div>
</div>
<div id="redis-callsTab" class="tab-content">
<div id="redisCallsContent"></div>
<div id="redisCallsPagination" class="pagination"></div>
</div>
</div>
</div>
<!-- 详情弹窗 -->
<div id="detailModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2 id="modalTitle">调用详情</h2>
<div id="modalContent"></div>
</div>
</div>
<script>
let autoRefreshInterval;
let currentEndpoint = '';
let esCurrentPage = 1;
let esPageSize = 50;
let esTotalPages = 1;
let redisCurrentPage = 1;
let redisPageSize = 50;
let redisTotalPages = 1;
function refreshData() {
loadAllEndpoints();
if (currentEndpoint) {
loadEndpointDetails(currentEndpoint);
}
}
function loadAllEndpoints() {
fetch('/api/api-monitor/all-stats')
.then(response => response.json())
.then(data => {
const endpoints = data.data;
const container = document.getElementById('endpointsList');
if (!endpoints || endpoints.length === 0) {
container.innerHTML = '<p>暂无监控数据</p>';
return;
}
let html = '<table class="table"><thead><tr><th>接口</th><th>总调用</th><th>ES 调用</th><th>Redis 调用</th><th>ES QPS</th><th>Redis QPS</th><th>成功率</th><th>平均耗时</th></tr></thead><tbody>';
endpoints.forEach(function(stat) {
var esQpsClass = stat.es_qps > 10 ? 'qps-high' : stat.es_qps > 1 ? 'qps-medium' : 'qps-low';
var redisQpsClass = stat.redis_qps > 10 ? 'qps-high' : stat.redis_qps > 1 ? 'qps-medium' : 'qps-low';
var successClass = stat.success_rate >= 95 ? 'success' : stat.success_rate >= 90 ? 'warning' : 'danger';
html += '<tr onclick="selectEndpoint(\'' + stat.endpoint + '\')" style="cursor: pointer;">' +
'<td>' + stat.endpoint + '</td>' +
'<td>' + stat.total_calls + '</td>' +
'<td>' + stat.es_calls + '</td>' +
'<td>' + stat.redis_calls + '</td>' +
'<td><span class="qps-badge ' + esQpsClass + '">' + stat.es_qps.toFixed(2) + '</span></td>' +
'<td><span class="qps-badge ' + redisQpsClass + '">' + stat.redis_qps.toFixed(2) + '</span></td>' +
'<td class="' + successClass + '">' + stat.success_rate.toFixed(2) + '%</td>' +
'<td>' + stat.avg_duration_ms + 'ms</td>' +
'</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(function(error) { console.error('加载接口列表失败:', error); });
}
function selectEndpoint(endpoint) {
currentEndpoint = endpoint;
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.classList.remove('active');
});
const firstTab = document.querySelector('.tab-btn');
if (firstTab) {
firstTab.classList.add('active');
}
loadEndpointDetails(endpoint);
}
function loadEndpointDetails(endpoint) {
fetch('/api/api-monitor/stats?endpoint=' + encodeURIComponent(endpoint))
.then(response => response.json())
.then(data => {
const stat = data.data;
const statsGrid = document.getElementById('statsGrid');
var successClass = stat.success_rate >= 95 ? 'success' : 'warning';
statsGrid.innerHTML = '' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.total_calls + '</div>' +
'<div class="stat-label">总调用次数</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.es_calls + '</div>' +
'<div class="stat-label">ES 调用</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.redis_calls + '</div>' +
'<div class="stat-label">Redis 调用</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.es_qps.toFixed(2) + '</div>' +
'<div class="stat-label">ES QPS</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.redis_qps.toFixed(2) + '</div>' +
'<div class="stat-label">Redis QPS</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value ' + successClass + '">' + stat.success_rate.toFixed(2) + '%</div>' +
'<div class="stat-label">成功率</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.avg_duration_ms + 'ms</div>' +
'<div class="stat-label">平均耗时</div>' +
'</div>' +
'<div class="stat-item">' +
'<div class="stat-value">' + stat.last_update + '</div>' +
'<div class="stat-label">最后更新</div>' +
'</div>';
})
.catch(function(error) { console.error('加载统计数据失败:', error); });
loadESCalls(endpoint);
loadRedisCalls(endpoint);
}
function loadESCalls(endpoint) {
fetch('/api/api-monitor/es-calls?endpoint=' + encodeURIComponent(endpoint) + '&page=' + esCurrentPage + '&page_size=' + esPageSize)
.then(response => response.json())
.then(data => {
if (!data.data || !data.data.calls) {
const container = document.getElementById('esCallsContent');
container.innerHTML = '<p>暂无 ES 调用记录</p>';
document.getElementById('esCallsPagination').innerHTML = '';
return;
}
const calls = data.data.calls;
const container = document.getElementById('esCallsContent');
if (!calls || calls.length === 0) {
container.innerHTML = '<p>暂无 ES 调用记录</p>';
document.getElementById('esCallsPagination').innerHTML = '';
return;
}
esTotalPages = data.data.total_pages || 1;
const total = data.data.total || 0;
let html = '<table class="table"><thead><tr><th>ID</th><th>操作</th><th>索引</th><th>查询</th><th>耗时 (ms)</th><th>时间</th><th>状态</th><th>详情</th></tr></thead><tbody>';
calls.forEach(function(call) {
var durationClass = call.duration_ms > 1000 ? 'danger' : call.duration_ms > 500 ? 'warning' : '';
var statusClass = call.success ? 'success' : 'danger';
html += '<tr>' +
'<td>' + call.id + '</td>' +
'<td>' + call.operation + '</td>' +
'<td>' + call.key_or_index + '</td>' +
'<td class="query-text" title="' + escapeHtml(call.query) + '" onclick="viewCallDetail(' + call.id + ', \'es\')">' + (call.query || '-') + '</td>' +
'<td class="' + durationClass + '">' + call.duration_ms + '</td>' +
'<td>' + new Date(call.timestamp).toLocaleString() + '</td>' +
'<td class="' + statusClass + '">' + (call.success ? '成功' : '失败') + '</td>' +
'<td><button class="btn" style="padding: 4px 8px; font-size: 11px;" onclick="viewCallDetail(' + call.id + ', \'es\')">查看详情</button></td>' +
'</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
renderESPagination(total);
})
.catch(function(error) { console.error('加载 ES 调用记录失败:', error); });
}
function renderESPagination(total) {
const container = document.getElementById('esCallsPagination');
if (esTotalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '<button onclick="changeESPage(1)"' + (esCurrentPage === 1 ? ' disabled' : '') + '>首页</button>' +
'<button onclick="changeESPage(' + (esCurrentPage - 1) + ')"' + (esCurrentPage === 1 ? ' disabled' : '') + '>上一页</button>' +
'<span class="pagination-info">第 ' + esCurrentPage + ' / ' + esTotalPages + ' 页 (共 ' + total + ' 条)</span>' +
'<button onclick="changeESPage(' + (esCurrentPage + 1) + ')"' + (esCurrentPage === esTotalPages ? ' disabled' : '') + '>下一页</button>' +
'<button onclick="changeESPage(' + esTotalPages + ')"' + (esCurrentPage === esTotalPages ? ' disabled' : '') + '>末页</button>' +
'<div class="page-size-selector">' +
'<select onchange="changeESPageSize(this.value)">' +
'<option value="20"' + (esPageSize === 20 ? ' selected' : '') + '>20 条/页</option>' +
'<option value="50"' + (esPageSize === 50 ? ' selected' : '') + '>50 条/页</option>' +
'<option value="100"' + (esPageSize === 100 ? ' selected' : '') + '>100 条/页</option>' +
'<option value="200"' + (esPageSize === 200 ? ' selected' : '') + '>200 条/页</option>' +
'</select>' +
'</div>';
container.innerHTML = html;
}
function changeESPage(page) {
if (page < 1 || page > esTotalPages) return;
esCurrentPage = page;
loadESCalls(currentEndpoint);
}
function changeESPageSize(size) {
esPageSize = parseInt(size);
esCurrentPage = 1;
loadESCalls(currentEndpoint);
}
function loadRedisCalls(endpoint) {
fetch('/api/api-monitor/redis-calls?endpoint=' + encodeURIComponent(endpoint) + '&page=' + redisCurrentPage + '&page_size=' + redisPageSize)
.then(response => response.json())
.then(data => {
if (!data.data || !data.data.calls) {
const container = document.getElementById('redisCallsContent');
container.innerHTML = '<p>暂无 Redis 调用记录</p>';
document.getElementById('redisCallsPagination').innerHTML = '';
return;
}
const calls = data.data.calls;
const container = document.getElementById('redisCallsContent');
if (!calls || calls.length === 0) {
container.innerHTML = '<p>暂无 Redis 调用记录</p>';
document.getElementById('redisCallsPagination').innerHTML = '';
return;
}
redisTotalPages = data.data.total_pages || 1;
const total = data.data.total || 0;
let html = '<table class="table"><thead><tr><th>ID</th><th>操作</th><th>Key</th><th>值</th><th>耗时 (ms)</th><th>时间</th><th>状态</th><th>详情</th></tr></thead><tbody>';
calls.forEach(function(call) {
var durationClass = call.duration_ms > 100 ? 'danger' : call.duration_ms > 50 ? 'warning' : '';
var statusClass = call.success ? 'success' : 'danger';
html += '<tr>' +
'<td>' + call.id + '</td>' +
'<td>' + call.operation + '</td>' +
'<td>' + call.key_or_index + '</td>' +
'<td class="query-text" title="' + escapeHtml(call.query) + '" onclick="viewCallDetail(' + call.id + ', \'redis\')">' + (call.query || '-') + '</td>' +
'<td class="' + durationClass + '">' + call.duration_ms + '</td>' +
'<td>' + new Date(call.timestamp).toLocaleString() + '</td>' +
'<td class="' + statusClass + '">' + (call.success ? '成功' : '失败') + '</td>' +
'<td><button class="btn" style="padding: 4px 8px; font-size: 11px;" onclick="viewCallDetail(' + call.id + ', \'redis\')">查看详情</button></td>' +
'</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
renderRedisPagination(total);
})
.catch(function(error) { console.error('加载 Redis 调用记录失败:', error); });
}
function renderRedisPagination(total) {
const container = document.getElementById('redisCallsPagination');
if (redisTotalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '<button onclick="changeRedisPage(1)"' + (redisCurrentPage === 1 ? ' disabled' : '') + '>首页</button>' +
'<button onclick="changeRedisPage(' + (redisCurrentPage - 1) + ')"' + (redisCurrentPage === 1 ? ' disabled' : '') + '>上一页</button>' +
'<span class="pagination-info">第 ' + redisCurrentPage + ' / ' + redisTotalPages + ' 页 (共 ' + total + ' 条)</span>' +
'<button onclick="changeRedisPage(' + (redisCurrentPage + 1) + ')"' + (redisCurrentPage === redisTotalPages ? ' disabled' : '') + '>下一页</button>' +
'<button onclick="changeRedisPage(' + redisTotalPages + ')"' + (redisCurrentPage === redisTotalPages ? ' disabled' : '') + '>末页</button>' +
'<div class="page-size-selector">' +
'<select onchange="changeRedisPageSize(this.value)">' +
'<option value="20"' + (redisPageSize === 20 ? ' selected' : '') + '>20 条/页</option>' +
'<option value="50"' + (redisPageSize === 50 ? ' selected' : '') + '>50 条/页</option>' +
'<option value="100"' + (redisPageSize === 100 ? ' selected' : '') + '>100 条/页</option>' +
'<option value="200"' + (redisPageSize === 200 ? ' selected' : '') + '>200 条/页</option>' +
'</select>' +
'</div>';
container.innerHTML = html;
}
function changeRedisPage(page) {
if (page < 1 || page > redisTotalPages) return;
redisCurrentPage = page;
loadRedisCalls(currentEndpoint);
}
function changeRedisPageSize(size) {
redisPageSize = parseInt(size);
redisCurrentPage = 1;
loadRedisCalls(currentEndpoint);
}
function viewCallDetail(callId, type) {
const modal = document.getElementById('detailModal');
const modalTitle = document.getElementById('modalTitle');
const modalContent = document.getElementById('modalContent');
modalTitle.innerText = (type === 'es' ? 'ES' : 'Redis') + ' 调用详情 - ID: ' + callId;
modalContent.innerHTML = '<div class="loading">加载中...</div>';
modal.style.display = 'block';
fetch('/api/api-monitor/call-detail?endpoint=' + encodeURIComponent(currentEndpoint) + '&call_id=' + callId + '&type=' + type)
.then(response => response.json())
.then(data => {
const detail = data.data;
let html = '' +
'<div class="detail-section">' +
'<div class="detail-label">基本信息</div>' +
'<table class="table">' +
'<tr><th width="150">调用 ID</th><td>' + detail.id + '</td></tr>' +
'<tr><th>类型</th><td>' + detail.type + '</td></tr>' +
'<tr><th>时间</th><td>' + detail.timestamp + '</td></tr>' +
'<tr><th>操作</th><td>' + detail.operation + '</td></tr>' +
'<tr><th>耗时</th><td class="' + (detail.duration_ms > 500 ? 'danger' : 'success') + '">' + detail.duration_ms + ' ms</td></tr>' +
'<tr><th>状态</th><td class="' + (detail.success ? 'success' : 'danger') + '">' + (detail.success ? '✓ 成功' : '✗ 失败') + '</td></tr>' +
(detail.error ? '<tr><th>错误信息</th><td class="danger">' + escapeHtml(detail.error) + '</td></tr>' : '') +
'</table>' +
'</div>' +
'<div class="detail-section">' +
'<div class="detail-label">查询信息</div>' +
'<table class="table">' +
'<tr><th>索引/Key</th><td>' + escapeHtml(detail.key_or_index) + '</td></tr>' +
'<tr><th>查询内容</th><td><div class="json-viewer">' + formatJSON(detail.query) + '</div></td></tr>' +
'</table>' +
'</div>';
if (detail.request) {
html += '<div class="detail-section">' +
'<div class="detail-label">请求内容</div>' +
'<div class="json-viewer">' + formatJSON(detail.request) + '</div>' +
'</div>';
}
if (detail.response) {
html += '<div class="detail-section">' +
'<div class="detail-label">响应内容</div>' +
'<div class="json-viewer">' + formatJSON(detail.response) + '</div>' +
'</div>';
}
modalContent.innerHTML = html;
})
.catch(function(error) {
modalContent.innerHTML = '<div class="danger">加载详情失败:' + error + '</div>';
});
}
function closeModal() {
document.getElementById('detailModal').style.display = 'none';
}
function switchTab(tabName, event) {
document.querySelectorAll('.tab-content').forEach(function(content) {
content.classList.remove('active');
});
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.classList.remove('active');
});
var targetTab = document.getElementById(tabName + 'Tab');
if (targetTab) {
targetTab.classList.add('active');
}
if (event && event.target) {
event.target.classList.add('active');
}
}
function toggleAutoRefresh() {
const checkbox = document.getElementById('autoRefresh');
if (checkbox.checked) {
autoRefreshInterval = setInterval(refreshData, 5000);
} else {
clearInterval(autoRefreshInterval);
}
}
function escapeHtml(text) {
if (!text) return '';
return text.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatJSON(jsonStr) {
if (!jsonStr) return '';
try {
if (typeof jsonStr === 'string') {
const obj = JSON.parse(jsonStr);
return JSON.stringify(obj, null, 2);
}
return JSON.stringify(jsonStr, null, 2);
} catch (e) {
return jsonStr;
}
}
window.onclick = function(event) {
const modal = document.getElementById('detailModal');
if (event.target == modal) {
closeModal();
}
}
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)
}