mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 08:31:55 +08:00
feat(metrics): enhance metrics dashboard and data handling
- Improved the metrics dashboard layout with new styles for better visual presentation, including enhanced chart and control elements. - Added functionality for auto-refreshing metrics and exporting data to CSV, improving user interaction and data accessibility. - Implemented persistent statistics tracking in the collector, allowing for historical data retrieval and better performance monitoring. - Enhanced database operations with optimizations for saving metrics and cleaning up old data, ensuring efficient data management. - Introduced performance metrics tracking, providing insights into average response times and throughput. These changes significantly enhance the usability and functionality of the metrics dashboard, providing a more robust framework for performance monitoring and data analysis.
This commit is contained in:
parent
2d658c35e6
commit
26c945a3f9
@ -319,6 +319,16 @@ var metricsTemplate = `
|
||||
.chart {
|
||||
height: 200px;
|
||||
margin-bottom: 50px;
|
||||
position: relative;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.chart h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
#timeRange {
|
||||
padding: 8px;
|
||||
@ -354,6 +364,93 @@ var metricsTemplate = `
|
||||
color: white;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.controls select {
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
+ #statusCodes {
|
||||
+ display: flex;
|
||||
+ gap: 20px;
|
||||
+ align-items: center;
|
||||
+ flex-wrap: wrap;
|
||||
+ }
|
||||
+
|
||||
+ #statusCodes .metric {
|
||||
+ flex: 0 0 auto;
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ gap: 10px;
|
||||
+ padding: 5px 15px;
|
||||
+ background: #f8f9fa;
|
||||
+ border-radius: 20px;
|
||||
+ margin: 0;
|
||||
+ border: none;
|
||||
+ }
|
||||
+
|
||||
.status-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
+ .loading {
|
||||
+ position: relative;
|
||||
+ opacity: 0.6;
|
||||
+ }
|
||||
+ .loading::after {
|
||||
+ content: "加载中...";
|
||||
+ position: absolute;
|
||||
+ top: 50%;
|
||||
+ left: 50%;
|
||||
+ transform: translate(-50%, -50%);
|
||||
+ background: rgba(255,255,255,0.9);
|
||||
+ padding: 10px 20px;
|
||||
+ border-radius: 4px;
|
||||
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
+ }
|
||||
+ .error-message {
|
||||
+ position: fixed;
|
||||
+ top: 20px;
|
||||
+ right: 20px;
|
||||
+ background: #dc3545;
|
||||
+ color: white;
|
||||
+ padding: 10px 20px;
|
||||
+ border-radius: 4px;
|
||||
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
+ z-index: 1000;
|
||||
+ display: none;
|
||||
+ }
|
||||
+ .export-btn {
|
||||
+ padding: 8px 16px;
|
||||
+ background: #28a745;
|
||||
+ color: white;
|
||||
+ border: none;
|
||||
+ border-radius: 4px;
|
||||
+ cursor: pointer;
|
||||
+ transition: all 0.3s;
|
||||
+ }
|
||||
+ .export-btn:hover {
|
||||
+ background: #218838;
|
||||
+ }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -474,17 +571,40 @@ var metricsTemplate = `
|
||||
|
||||
<div class="card">
|
||||
<h2>历史数据</h2>
|
||||
<div class="controls">
|
||||
<label>
|
||||
<input type="checkbox" id="autoRefresh" checked>
|
||||
自动刷新
|
||||
</label>
|
||||
<select id="refreshInterval">
|
||||
<option value="5000">5秒</option>
|
||||
<option value="10000">10秒</option>
|
||||
<option value="30000">30秒</option>
|
||||
<option value="60000">1分钟</option>
|
||||
</select>
|
||||
<button onclick="exportData()" class="export-btn">
|
||||
导出数据
|
||||
</button>
|
||||
</div>
|
||||
<div class="time-range-buttons">
|
||||
<div class="time-range-group">
|
||||
<span class="group-label">最近:</span>
|
||||
<button class="time-btn" data-hours="0.5">30分钟</button>
|
||||
<button class="time-btn" data-hours="1">1小时</button>
|
||||
<button class="time-btn" data-hours="3">3小时</button>
|
||||
<button class="time-btn" data-hours="6">6小时</button>
|
||||
<button class="time-btn" data-hours="12">12小时</button>
|
||||
<button class="time-btn active" data-hours="24">24小时</button>
|
||||
</div>
|
||||
<div class="time-range-group">
|
||||
<span class="group-label">历史:</span>
|
||||
<button class="time-btn" data-hours="72">3天</button>
|
||||
<button class="time-btn" data-hours="120">5天</button>
|
||||
<button class="time-btn" data-hours="168">7天</button>
|
||||
<button class="time-btn" data-hours="360">15天</button>
|
||||
<button class="time-btn" data-hours="720">30天</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="historyChart">
|
||||
<div class="chart-container">
|
||||
<div class="chart">
|
||||
@ -506,6 +626,8 @@ var metricsTemplate = `
|
||||
<span id="lastUpdate"></span>
|
||||
<button class="refresh" onclick="refreshMetrics()">刷新</button>
|
||||
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
|
||||
<script>
|
||||
// 检查登录状态
|
||||
const token = localStorage.getItem('metricsToken');
|
||||
@ -554,15 +676,13 @@ var metricsTemplate = `
|
||||
document.getElementById('totalBytes').textContent = formatBytes(data.total_bytes);
|
||||
document.getElementById('bytesPerSecond').textContent = formatBytes(data.bytes_per_second) + '/s';
|
||||
|
||||
// 更新状态码统计
|
||||
// 更新状态码计
|
||||
const statusCodesHtml = Object.entries(data.status_code_stats)
|
||||
.map(([status, count]) => {
|
||||
const statusClass = 'status-' + status.charAt(0) + 'xx';
|
||||
return '<div class="metric">' +
|
||||
'<span class="metric-label">' +
|
||||
'<span class="status-badge ' + statusClass + '">' + status + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="metric-value">' + count + '</span>' +
|
||||
'<span class="metric-value">' + count.toLocaleString() + '</span>' +
|
||||
'</div>';
|
||||
})
|
||||
.join('');
|
||||
@ -605,6 +725,15 @@ var metricsTemplate = `
|
||||
document.getElementById('lastUpdate').textContent = '最后更新: ' + new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
errorDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function refreshMetrics() {
|
||||
fetch('/metrics', {
|
||||
headers: {
|
||||
@ -613,17 +742,19 @@ var metricsTemplate = `
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
// token 无效,跳转到登录页
|
||||
localStorage.removeItem('metricsToken');
|
||||
window.location.href = '/metrics/ui';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('获取数据失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data) updateMetrics(data);
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
.catch(error => showError(error.message));
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
@ -640,6 +771,9 @@ var metricsTemplate = `
|
||||
};
|
||||
|
||||
function loadHistoryData(hours) {
|
||||
const chartContainer = document.getElementById('historyChart');
|
||||
chartContainer.classList.add('loading');
|
||||
|
||||
fetch('/metrics/history?hours=' + hours, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
@ -672,6 +806,30 @@ var metricsTemplate = `
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
if (context.chart.canvas.id === 'bytesChart') {
|
||||
label += formatBytes(context.parsed.y * 1024 * 1024);
|
||||
} else if (context.chart.canvas.id === 'errorRateChart') {
|
||||
label += context.parsed.y.toFixed(2) + '%';
|
||||
} else {
|
||||
label += context.parsed.y.toLocaleString();
|
||||
}
|
||||
}
|
||||
return label;
|
||||
},
|
||||
title: function(tooltipItems) {
|
||||
const date = new Date(tooltipItems[0].label);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@ -679,12 +837,26 @@ var metricsTemplate = `
|
||||
display: true,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 20
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (this.chart.canvas.id === 'bytesChart') {
|
||||
return formatBytes(value * 1024 * 1024);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -693,8 +865,15 @@ var metricsTemplate = `
|
||||
tension: 0.4
|
||||
},
|
||||
point: {
|
||||
radius: 2
|
||||
radius: 2,
|
||||
hitRadius: 10,
|
||||
hoverRadius: 5
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
}
|
||||
};
|
||||
|
||||
@ -706,7 +885,9 @@ var metricsTemplate = `
|
||||
updateChart('bytesChart', 'bytes', labels, data, '流量 (MB)',
|
||||
m => m.total_bytes / (1024 * 1024), '#28a745', commonOptions);
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
.finally(() => {
|
||||
chartContainer.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
|
||||
function updateChart(canvasId, chartKey, labels, data, label, valueGetter, color, options) {
|
||||
@ -756,6 +937,69 @@ var metricsTemplate = `
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadHistoryData(24);
|
||||
});
|
||||
|
||||
let refreshTimer;
|
||||
|
||||
function setupAutoRefresh() {
|
||||
const autoRefresh = document.getElementById('autoRefresh');
|
||||
const refreshInterval = document.getElementById('refreshInterval');
|
||||
|
||||
function updateRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
if (autoRefresh.checked) {
|
||||
refreshTimer = setInterval(refreshMetrics, parseInt(refreshInterval.value));
|
||||
}
|
||||
}
|
||||
|
||||
autoRefresh.addEventListener('change', updateRefreshTimer);
|
||||
refreshInterval.addEventListener('change', updateRefreshTimer);
|
||||
|
||||
updateRefreshTimer();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadHistoryData(24);
|
||||
setupAutoRefresh();
|
||||
});
|
||||
|
||||
function exportData() {
|
||||
const activeBtn = document.querySelector('.time-btn.active');
|
||||
const hours = parseInt(activeBtn.dataset.hours);
|
||||
|
||||
fetch('/metrics/history?hours=' + hours, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const csv = convertToCSV(data);
|
||||
downloadCSV(csv, 'metrics_' + hours + 'h_' + new Date().toISOString() + '.csv');
|
||||
})
|
||||
.catch(error => showError('导出失败: ' + error.message));
|
||||
}
|
||||
|
||||
function convertToCSV(data) {
|
||||
const headers = ['时间', '请求数', '错误数', '流量(MB)', '错误率(%)'];
|
||||
const rows = data.map(row => [
|
||||
row.timestamp,
|
||||
row.total_requests,
|
||||
row.total_errors,
|
||||
(row.total_bytes / (1024 * 1024)).toFixed(2),
|
||||
(row.error_rate * 100).toFixed(2)
|
||||
]);
|
||||
return [headers, ...rows].map(row => row.join(',')).join('\n');
|
||||
}
|
||||
|
||||
function downloadCSV(csv, filename) {
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 添加 Chart.js -->
|
||||
|
@ -24,6 +24,11 @@ type Collector struct {
|
||||
totalErrors int64
|
||||
totalBytes atomic.Int64
|
||||
latencySum atomic.Int64
|
||||
persistentStats struct {
|
||||
totalRequests atomic.Int64
|
||||
totalErrors atomic.Int64
|
||||
totalBytes atomic.Int64
|
||||
}
|
||||
pathStats sync.Map
|
||||
refererStats sync.Map
|
||||
statusStats [6]atomic.Int64
|
||||
@ -55,6 +60,15 @@ func InitCollector(dbPath string, config *config.Config) error {
|
||||
db: db,
|
||||
}
|
||||
|
||||
// 加载历史数据
|
||||
if lastMetrics, err := db.GetLastMetrics(); err == nil && lastMetrics != nil {
|
||||
globalCollector.persistentStats.totalRequests.Store(lastMetrics.TotalRequests)
|
||||
globalCollector.persistentStats.totalErrors.Store(lastMetrics.TotalErrors)
|
||||
globalCollector.persistentStats.totalBytes.Store(lastMetrics.TotalBytes)
|
||||
log.Printf("Loaded historical metrics: requests=%d, errors=%d, bytes=%d",
|
||||
lastMetrics.TotalRequests, lastMetrics.TotalErrors, lastMetrics.TotalBytes)
|
||||
}
|
||||
|
||||
globalCollector.cache = cache.NewCache(constants.CacheTTL)
|
||||
globalCollector.monitor = monitor.NewMonitor()
|
||||
|
||||
@ -95,13 +109,28 @@ func InitCollector(dbPath string, config *config.Config) error {
|
||||
// 设置程序退出时的处理
|
||||
utils.SetupCloseHandler(func() {
|
||||
log.Println("Saving final metrics before shutdown...")
|
||||
// 确保所有正在进行的操作完成
|
||||
time.Sleep(time.Second)
|
||||
|
||||
stats := globalCollector.GetStats()
|
||||
if err := db.SaveFullMetrics(stats); err != nil {
|
||||
if err := db.SaveMetrics(stats); err != nil {
|
||||
log.Printf("Error saving final metrics: %v", err)
|
||||
} else {
|
||||
log.Printf("Final metrics saved successfully")
|
||||
log.Printf("Basic metrics saved successfully")
|
||||
}
|
||||
|
||||
// 保存完整统计数据
|
||||
if err := db.SaveFullMetrics(stats); err != nil {
|
||||
log.Printf("Error saving full metrics: %v", err)
|
||||
} else {
|
||||
log.Printf("Full metrics saved successfully")
|
||||
}
|
||||
|
||||
// 等待数据写入完成
|
||||
time.Sleep(time.Second)
|
||||
|
||||
db.Close()
|
||||
log.Println("Database closed successfully")
|
||||
})
|
||||
|
||||
globalCollector.statsPool = sync.Pool{
|
||||
@ -220,20 +249,33 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
// 确保所有字段都被初始化
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// 基础指标
|
||||
// 基础指标 - 合并当前会话和持久化的数据
|
||||
currentRequests := atomic.LoadInt64(&c.totalRequests)
|
||||
currentErrors := atomic.LoadInt64(&c.totalErrors)
|
||||
currentBytes := c.totalBytes.Load()
|
||||
|
||||
totalRequests := currentRequests + c.persistentStats.totalRequests.Load()
|
||||
totalErrors := currentErrors + c.persistentStats.totalErrors.Load()
|
||||
totalBytes := currentBytes + c.persistentStats.totalBytes.Load()
|
||||
|
||||
// 计算每秒指标
|
||||
uptime := time.Since(c.startTime).Seconds()
|
||||
stats["requests_per_second"] = float64(currentRequests) / Max(uptime, 1)
|
||||
stats["bytes_per_second"] = float64(currentBytes) / Max(uptime, 1)
|
||||
|
||||
stats["active_requests"] = atomic.LoadInt64(&c.activeRequests)
|
||||
stats["total_requests"] = atomic.LoadInt64(&c.totalRequests)
|
||||
stats["total_errors"] = atomic.LoadInt64(&c.totalErrors)
|
||||
stats["total_bytes"] = c.totalBytes.Load()
|
||||
stats["total_requests"] = totalRequests
|
||||
stats["total_errors"] = totalErrors
|
||||
stats["total_bytes"] = totalBytes
|
||||
|
||||
// 系统指标
|
||||
stats["num_goroutine"] = runtime.NumGoroutine()
|
||||
stats["memory_usage"] = FormatBytes(m.Alloc)
|
||||
|
||||
// 延迟指标
|
||||
totalRequests := atomic.LoadInt64(&c.totalRequests)
|
||||
if totalRequests > 0 {
|
||||
stats["avg_latency"] = c.latencySum.Load() / totalRequests
|
||||
currentTotalRequests := atomic.LoadInt64(&c.totalRequests)
|
||||
if currentTotalRequests > 0 {
|
||||
stats["avg_latency"] = c.latencySum.Load() / currentTotalRequests
|
||||
} else {
|
||||
stats["avg_latency"] = int64(0)
|
||||
}
|
||||
@ -367,3 +409,35 @@ func Max(a, b float64) float64 {
|
||||
func (c *Collector) GetDB() *models.MetricsDB {
|
||||
return c.db
|
||||
}
|
||||
|
||||
func (c *Collector) SaveMetrics(stats map[string]interface{}) error {
|
||||
// 更新持久化数据
|
||||
c.persistentStats.totalRequests.Store(stats["total_requests"].(int64))
|
||||
c.persistentStats.totalErrors.Store(stats["total_errors"].(int64))
|
||||
c.persistentStats.totalBytes.Store(stats["total_bytes"].(int64))
|
||||
|
||||
// 重置当前会话计数器
|
||||
atomic.StoreInt64(&c.totalRequests, 0)
|
||||
atomic.StoreInt64(&c.totalErrors, 0)
|
||||
c.totalBytes.Store(0)
|
||||
c.latencySum.Store(0)
|
||||
|
||||
// 重置状态码统计
|
||||
for i := range c.statusStats {
|
||||
c.statusStats[i].Store(0)
|
||||
}
|
||||
|
||||
// 重置路径统计
|
||||
c.pathStats.Range(func(key, _ interface{}) bool {
|
||||
c.pathStats.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
// 重置引用来源统计
|
||||
c.refererStats.Range(func(key, _ interface{}) bool {
|
||||
c.refererStats.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
return c.db.SaveMetrics(stats)
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"proxy-go/internal/constants"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -47,12 +49,31 @@ type MetricsDB struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
type PerformanceMetrics struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
AvgResponseTime int64 `json:"avg_response_time"`
|
||||
RequestsPerSecond float64 `json:"requests_per_second"`
|
||||
BytesPerSecond float64 `json:"bytes_per_second"`
|
||||
}
|
||||
|
||||
func NewMetricsDB(dbPath string) (*MetricsDB, error) {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
db.SetMaxOpenConns(1) // SQLite 只支持一个写连接
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
// 设置数据库优化参数
|
||||
db.Exec("PRAGMA busy_timeout = 5000") // 设置忙等待超时
|
||||
db.Exec("PRAGMA journal_mode = WAL") // 使用 WAL 模式提高并发性能
|
||||
db.Exec("PRAGMA synchronous = NORMAL") // 在保证安全的前提下提高性能
|
||||
db.Exec("PRAGMA cache_size = -2000") // 使用2MB缓存
|
||||
db.Exec("PRAGMA temp_store = MEMORY") // 临时表使用内存
|
||||
|
||||
// 创建必要的表
|
||||
if err := initTables(db); err != nil {
|
||||
db.Close()
|
||||
@ -194,47 +215,82 @@ func initTables(db *sql.DB) error {
|
||||
|
||||
// 定期清理旧数据
|
||||
func cleanupRoutine(db *sql.DB) {
|
||||
// 避免在启动时就立即清理
|
||||
time.Sleep(5 * time.Minute)
|
||||
|
||||
ticker := time.NewTicker(constants.CleanupInterval)
|
||||
for range ticker.C {
|
||||
// 开始事务
|
||||
start := time.Now()
|
||||
var totalDeleted int64
|
||||
|
||||
// 检查数据库大小
|
||||
var dbSize int64
|
||||
row := db.QueryRow("SELECT page_count * page_size FROM pragma_page_count, pragma_page_size")
|
||||
row.Scan(&dbSize)
|
||||
log.Printf("Current database size: %s", FormatBytes(uint64(dbSize)))
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error starting cleanup transaction: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 删除超过保留期限的数据
|
||||
// 优化清理性能
|
||||
tx.Exec("PRAGMA synchronous = NORMAL")
|
||||
tx.Exec("PRAGMA journal_mode = WAL")
|
||||
tx.Exec("PRAGMA temp_store = MEMORY")
|
||||
tx.Exec("PRAGMA cache_size = -2000")
|
||||
|
||||
// 先清理索引
|
||||
tx.Exec("ANALYZE")
|
||||
tx.Exec("PRAGMA optimize")
|
||||
|
||||
cutoff := time.Now().Add(-constants.DataRetention)
|
||||
_, err = tx.Exec(`DELETE FROM metrics_history WHERE timestamp < ?`, cutoff)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Error cleaning metrics_history: %v", err)
|
||||
continue
|
||||
tables := []string{
|
||||
"metrics_history",
|
||||
"status_stats",
|
||||
"path_stats",
|
||||
"performance_metrics",
|
||||
"status_code_history",
|
||||
"popular_paths_history",
|
||||
"referer_history",
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`DELETE FROM status_stats WHERE timestamp < ?`, cutoff)
|
||||
for _, table := range tables {
|
||||
// 使用批量删除提高性能
|
||||
for {
|
||||
result, err := tx.Exec(`DELETE FROM `+table+` WHERE timestamp < ? LIMIT 1000`, cutoff)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Error cleaning status_stats: %v", err)
|
||||
continue
|
||||
log.Printf("Error cleaning %s: %v", table, err)
|
||||
break
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
totalDeleted += rows
|
||||
if rows < 1000 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`DELETE FROM path_stats WHERE timestamp < ?`, cutoff)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Error cleaning path_stats: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Printf("Error committing cleanup transaction: %v", err)
|
||||
} else {
|
||||
log.Printf("Successfully cleaned up old metrics data")
|
||||
log.Printf("Cleaned up %d old records in %v, freed %s",
|
||||
totalDeleted, time.Since(start),
|
||||
FormatBytes(uint64(dbSize-getDBSize(db))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据库大小
|
||||
func getDBSize(db *sql.DB) int64 {
|
||||
var size int64
|
||||
row := db.QueryRow("SELECT page_count * page_size FROM pragma_page_count, pragma_page_size")
|
||||
row.Scan(&size)
|
||||
return size
|
||||
}
|
||||
|
||||
func (db *MetricsDB) SaveMetrics(stats map[string]interface{}) error {
|
||||
tx, err := db.DB.Begin()
|
||||
if err != nil {
|
||||
@ -291,6 +347,21 @@ func (db *MetricsDB) SaveMetrics(stats map[string]interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 同时保存性能指标
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO performance_metrics (
|
||||
avg_response_time,
|
||||
requests_per_second,
|
||||
bytes_per_second
|
||||
) VALUES (?, ?, ?)`,
|
||||
stats["avg_latency"],
|
||||
stats["requests_per_second"],
|
||||
stats["bytes_per_second"],
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@ -299,9 +370,18 @@ func (db *MetricsDB) Close() error {
|
||||
}
|
||||
|
||||
func (db *MetricsDB) GetRecentMetrics(hours int) ([]HistoricalMetrics, error) {
|
||||
// 设置查询优化参数
|
||||
db.DB.Exec("PRAGMA temp_store = MEMORY")
|
||||
db.DB.Exec("PRAGMA cache_size = -4000") // 使用4MB缓存
|
||||
db.DB.Exec("PRAGMA mmap_size = 268435456") // 使用256MB内存映射
|
||||
|
||||
var interval string
|
||||
if hours <= 24 {
|
||||
if hours <= 1 {
|
||||
interval = "%Y-%m-%d %H:%M:00" // 按分钟分组
|
||||
} else {
|
||||
interval = "%Y-%m-%d %H:00:00" // 按小时分组
|
||||
}
|
||||
} else if hours <= 168 {
|
||||
interval = "%Y-%m-%d %H:00:00" // 按小时分组
|
||||
} else {
|
||||
@ -411,6 +491,26 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 开始时记录数据库大小
|
||||
startSize := getDBSize(db.DB)
|
||||
|
||||
// 优化写入性能
|
||||
tx.Exec("PRAGMA synchronous = NORMAL")
|
||||
tx.Exec("PRAGMA journal_mode = WAL")
|
||||
tx.Exec("PRAGMA temp_store = MEMORY")
|
||||
tx.Exec("PRAGMA cache_size = -2000") // 使用2MB缓存
|
||||
|
||||
// 使用批量插入提高性能
|
||||
const batchSize = 100
|
||||
|
||||
// 预分配语句
|
||||
stmts := make([]*sql.Stmt, 0, 4)
|
||||
defer func() {
|
||||
for _, stmt := range stmts {
|
||||
stmt.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// 保存性能指标
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO performance_metrics (
|
||||
@ -426,26 +526,30 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用事务提高写入性能
|
||||
tx.Exec("PRAGMA synchronous = OFF")
|
||||
tx.Exec("PRAGMA journal_mode = MEMORY")
|
||||
|
||||
// 保存状态码统计
|
||||
statusStats := stats["status_code_stats"].(map[string]int64)
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT INTO status_code_history (status_group, count)
|
||||
VALUES (?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
values := make([]string, 0, len(statusStats))
|
||||
args := make([]interface{}, 0, len(statusStats)*2)
|
||||
|
||||
for group, count := range statusStats {
|
||||
if _, err := stmt.Exec(group, count); err != nil {
|
||||
return err
|
||||
values = append(values, "(?, ?)")
|
||||
args = append(args, group, count)
|
||||
}
|
||||
|
||||
query := "INSERT INTO status_code_history (status_group, count) VALUES " +
|
||||
strings.Join(values, ",")
|
||||
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存热门路径
|
||||
pathStats := stats["top_paths"].([]PathMetrics)
|
||||
stmt, err = tx.Prepare(`
|
||||
pathStmt, err := tx.Prepare(`
|
||||
INSERT INTO popular_paths_history (
|
||||
path, request_count, error_count, avg_latency, bytes_transferred
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
@ -453,9 +557,10 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pathStmt.Close()
|
||||
|
||||
for _, p := range pathStats {
|
||||
if _, err := stmt.Exec(
|
||||
if _, err := pathStmt.Exec(
|
||||
p.Path, p.RequestCount, p.ErrorCount,
|
||||
p.AvgLatency, p.BytesTransferred,
|
||||
); err != nil {
|
||||
@ -465,19 +570,109 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
||||
|
||||
// 保存引用来源
|
||||
refererStats := stats["top_referers"].([]PathMetrics)
|
||||
stmt, err = tx.Prepare(`
|
||||
refererStmt, err := tx.Prepare(`
|
||||
INSERT INTO referer_history (referer, request_count)
|
||||
VALUES (?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer refererStmt.Close()
|
||||
|
||||
for _, r := range refererStats {
|
||||
if _, err := stmt.Exec(r.Path, r.RequestCount); err != nil {
|
||||
if _, err := refererStmt.Exec(r.Path, r.RequestCount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录写入的数据量
|
||||
endSize := getDBSize(db.DB)
|
||||
log.Printf("Saved metrics: wrote %s to database",
|
||||
FormatBytes(uint64(endSize-startSize)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *MetricsDB) GetLastMetrics() (*HistoricalMetrics, error) {
|
||||
row := db.DB.QueryRow(`
|
||||
SELECT
|
||||
total_requests,
|
||||
total_errors,
|
||||
total_bytes,
|
||||
avg_latency
|
||||
FROM metrics_history
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
`)
|
||||
|
||||
var metrics HistoricalMetrics
|
||||
err := row.Scan(
|
||||
&metrics.TotalRequests,
|
||||
&metrics.TotalErrors,
|
||||
&metrics.TotalBytes,
|
||||
&metrics.AvgLatency,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metrics, nil
|
||||
}
|
||||
|
||||
func (db *MetricsDB) GetRecentPerformanceMetrics(hours int) ([]PerformanceMetrics, error) {
|
||||
rows, err := db.DB.Query(`
|
||||
SELECT
|
||||
strftime('%Y-%m-%d %H:%M:00', timestamp, 'localtime') as ts,
|
||||
AVG(avg_response_time) as avg_response_time,
|
||||
AVG(requests_per_second) as requests_per_second,
|
||||
AVG(bytes_per_second) as bytes_per_second
|
||||
FROM performance_metrics
|
||||
WHERE timestamp >= datetime('now', '-' || ? || ' hours', 'localtime')
|
||||
GROUP BY ts
|
||||
ORDER BY ts DESC
|
||||
`, hours)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metrics []PerformanceMetrics
|
||||
for rows.Next() {
|
||||
var m PerformanceMetrics
|
||||
err := rows.Scan(
|
||||
&m.Timestamp,
|
||||
&m.AvgResponseTime,
|
||||
&m.RequestsPerSecond,
|
||||
&m.BytesPerSecond,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics = append(metrics, m)
|
||||
}
|
||||
|
||||
return metrics, rows.Err()
|
||||
}
|
||||
|
||||
// FormatBytes 格式化字节大小
|
||||
func FormatBytes(bytes uint64) string {
|
||||
const (
|
||||
MB = 1024 * 1024
|
||||
KB = 1024
|
||||
)
|
||||
|
||||
switch {
|
||||
case bytes >= MB:
|
||||
return fmt.Sprintf("%.2f MB", float64(bytes)/MB)
|
||||
case bytes >= KB:
|
||||
return fmt.Sprintf("%.2f KB", float64(bytes)/KB)
|
||||
default:
|
||||
return fmt.Sprintf("%d Bytes", bytes)
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,30 @@ package utils
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetupCloseHandler(callback func()) {
|
||||
c := make(chan os.Signal, 2)
|
||||
c := make(chan os.Signal, 1)
|
||||
done := make(chan bool, 1)
|
||||
var once sync.Once
|
||||
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
once.Do(func() {
|
||||
callback()
|
||||
done <- true
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
os.Exit(0)
|
||||
case <-c:
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user