mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 16:41:54 +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 {
|
.chart {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
margin-bottom: 50px;
|
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 {
|
#timeRange {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -354,6 +364,93 @@ var metricsTemplate = `
|
|||||||
color: white;
|
color: white;
|
||||||
border-color: #0056b3;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -474,17 +571,40 @@ var metricsTemplate = `
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>历史数据</h2>
|
<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-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="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="6">6小时</button>
|
||||||
<button class="time-btn" data-hours="12">12小时</button>
|
<button class="time-btn" data-hours="12">12小时</button>
|
||||||
<button class="time-btn active" data-hours="24">24小时</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="72">3天</button>
|
||||||
<button class="time-btn" data-hours="120">5天</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="168">7天</button>
|
||||||
<button class="time-btn" data-hours="360">15天</button>
|
<button class="time-btn" data-hours="360">15天</button>
|
||||||
<button class="time-btn" data-hours="720">30天</button>
|
<button class="time-btn" data-hours="720">30天</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="historyChart">
|
<div id="historyChart">
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
@ -506,6 +626,8 @@ var metricsTemplate = `
|
|||||||
<span id="lastUpdate"></span>
|
<span id="lastUpdate"></span>
|
||||||
<button class="refresh" onclick="refreshMetrics()">刷新</button>
|
<button class="refresh" onclick="refreshMetrics()">刷新</button>
|
||||||
|
|
||||||
|
<div id="errorMessage" class="error-message"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 检查登录状态
|
// 检查登录状态
|
||||||
const token = localStorage.getItem('metricsToken');
|
const token = localStorage.getItem('metricsToken');
|
||||||
@ -554,15 +676,13 @@ var metricsTemplate = `
|
|||||||
document.getElementById('totalBytes').textContent = formatBytes(data.total_bytes);
|
document.getElementById('totalBytes').textContent = formatBytes(data.total_bytes);
|
||||||
document.getElementById('bytesPerSecond').textContent = formatBytes(data.bytes_per_second) + '/s';
|
document.getElementById('bytesPerSecond').textContent = formatBytes(data.bytes_per_second) + '/s';
|
||||||
|
|
||||||
// 更新状态码统计
|
// 更新状态码计
|
||||||
const statusCodesHtml = Object.entries(data.status_code_stats)
|
const statusCodesHtml = Object.entries(data.status_code_stats)
|
||||||
.map(([status, count]) => {
|
.map(([status, count]) => {
|
||||||
const statusClass = 'status-' + status.charAt(0) + 'xx';
|
const statusClass = 'status-' + status.charAt(0) + 'xx';
|
||||||
return '<div class="metric">' +
|
return '<div class="metric">' +
|
||||||
'<span class="metric-label">' +
|
|
||||||
'<span class="status-badge ' + statusClass + '">' + status + '</span>' +
|
'<span class="status-badge ' + statusClass + '">' + status + '</span>' +
|
||||||
'</span>' +
|
'<span class="metric-value">' + count.toLocaleString() + '</span>' +
|
||||||
'<span class="metric-value">' + count + '</span>' +
|
|
||||||
'</div>';
|
'</div>';
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
@ -605,6 +725,15 @@ var metricsTemplate = `
|
|||||||
document.getElementById('lastUpdate').textContent = '最后更新: ' + new Date().toLocaleTimeString();
|
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() {
|
function refreshMetrics() {
|
||||||
fetch('/metrics', {
|
fetch('/metrics', {
|
||||||
headers: {
|
headers: {
|
||||||
@ -613,17 +742,19 @@ var metricsTemplate = `
|
|||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// token 无效,跳转到登录页
|
|
||||||
localStorage.removeItem('metricsToken');
|
localStorage.removeItem('metricsToken');
|
||||||
window.location.href = '/metrics/ui';
|
window.location.href = '/metrics/ui';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取数据失败');
|
||||||
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data) updateMetrics(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) {
|
function loadHistoryData(hours) {
|
||||||
|
const chartContainer = document.getElementById('historyChart');
|
||||||
|
chartContainer.classList.add('loading');
|
||||||
|
|
||||||
fetch('/metrics/history?hours=' + hours, {
|
fetch('/metrics/history?hours=' + hours, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + token
|
'Authorization': 'Bearer ' + token
|
||||||
@ -672,6 +806,30 @@ var metricsTemplate = `
|
|||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
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: {
|
scales: {
|
||||||
@ -679,12 +837,26 @@ var metricsTemplate = `
|
|||||||
display: true,
|
display: true,
|
||||||
grid: {
|
grid: {
|
||||||
display: false
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: {
|
grid: {
|
||||||
drawBorder: false
|
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
|
tension: 0.4
|
||||||
},
|
},
|
||||||
point: {
|
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)',
|
updateChart('bytesChart', 'bytes', labels, data, '流量 (MB)',
|
||||||
m => m.total_bytes / (1024 * 1024), '#28a745', commonOptions);
|
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) {
|
function updateChart(canvasId, chartKey, labels, data, label, valueGetter, color, options) {
|
||||||
@ -756,6 +937,69 @@ var metricsTemplate = `
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadHistoryData(24);
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- 添加 Chart.js -->
|
<!-- 添加 Chart.js -->
|
||||||
|
@ -24,6 +24,11 @@ type Collector struct {
|
|||||||
totalErrors int64
|
totalErrors int64
|
||||||
totalBytes atomic.Int64
|
totalBytes atomic.Int64
|
||||||
latencySum atomic.Int64
|
latencySum atomic.Int64
|
||||||
|
persistentStats struct {
|
||||||
|
totalRequests atomic.Int64
|
||||||
|
totalErrors atomic.Int64
|
||||||
|
totalBytes atomic.Int64
|
||||||
|
}
|
||||||
pathStats sync.Map
|
pathStats sync.Map
|
||||||
refererStats sync.Map
|
refererStats sync.Map
|
||||||
statusStats [6]atomic.Int64
|
statusStats [6]atomic.Int64
|
||||||
@ -55,6 +60,15 @@ func InitCollector(dbPath string, config *config.Config) error {
|
|||||||
db: db,
|
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.cache = cache.NewCache(constants.CacheTTL)
|
||||||
globalCollector.monitor = monitor.NewMonitor()
|
globalCollector.monitor = monitor.NewMonitor()
|
||||||
|
|
||||||
@ -95,13 +109,28 @@ func InitCollector(dbPath string, config *config.Config) error {
|
|||||||
// 设置程序退出时的处理
|
// 设置程序退出时的处理
|
||||||
utils.SetupCloseHandler(func() {
|
utils.SetupCloseHandler(func() {
|
||||||
log.Println("Saving final metrics before shutdown...")
|
log.Println("Saving final metrics before shutdown...")
|
||||||
|
// 确保所有正在进行的操作完成
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
stats := globalCollector.GetStats()
|
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)
|
log.Printf("Error saving final metrics: %v", err)
|
||||||
} else {
|
} 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()
|
db.Close()
|
||||||
|
log.Println("Database closed successfully")
|
||||||
})
|
})
|
||||||
|
|
||||||
globalCollector.statsPool = sync.Pool{
|
globalCollector.statsPool = sync.Pool{
|
||||||
@ -220,20 +249,33 @@ func (c *Collector) GetStats() map[string]interface{} {
|
|||||||
// 确保所有字段都被初始化
|
// 确保所有字段都被初始化
|
||||||
stats := make(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["active_requests"] = atomic.LoadInt64(&c.activeRequests)
|
||||||
stats["total_requests"] = atomic.LoadInt64(&c.totalRequests)
|
stats["total_requests"] = totalRequests
|
||||||
stats["total_errors"] = atomic.LoadInt64(&c.totalErrors)
|
stats["total_errors"] = totalErrors
|
||||||
stats["total_bytes"] = c.totalBytes.Load()
|
stats["total_bytes"] = totalBytes
|
||||||
|
|
||||||
// 系统指标
|
// 系统指标
|
||||||
stats["num_goroutine"] = runtime.NumGoroutine()
|
stats["num_goroutine"] = runtime.NumGoroutine()
|
||||||
stats["memory_usage"] = FormatBytes(m.Alloc)
|
stats["memory_usage"] = FormatBytes(m.Alloc)
|
||||||
|
|
||||||
// 延迟指标
|
// 延迟指标
|
||||||
totalRequests := atomic.LoadInt64(&c.totalRequests)
|
currentTotalRequests := atomic.LoadInt64(&c.totalRequests)
|
||||||
if totalRequests > 0 {
|
if currentTotalRequests > 0 {
|
||||||
stats["avg_latency"] = c.latencySum.Load() / totalRequests
|
stats["avg_latency"] = c.latencySum.Load() / currentTotalRequests
|
||||||
} else {
|
} else {
|
||||||
stats["avg_latency"] = int64(0)
|
stats["avg_latency"] = int64(0)
|
||||||
}
|
}
|
||||||
@ -367,3 +409,35 @@ func Max(a, b float64) float64 {
|
|||||||
func (c *Collector) GetDB() *models.MetricsDB {
|
func (c *Collector) GetDB() *models.MetricsDB {
|
||||||
return c.db
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"proxy-go/internal/constants"
|
"proxy-go/internal/constants"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -47,12 +49,31 @@ type MetricsDB struct {
|
|||||||
DB *sql.DB
|
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) {
|
func NewMetricsDB(dbPath string) (*MetricsDB, error) {
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err := initTables(db); err != nil {
|
||||||
db.Close()
|
db.Close()
|
||||||
@ -194,47 +215,82 @@ func initTables(db *sql.DB) error {
|
|||||||
|
|
||||||
// 定期清理旧数据
|
// 定期清理旧数据
|
||||||
func cleanupRoutine(db *sql.DB) {
|
func cleanupRoutine(db *sql.DB) {
|
||||||
|
// 避免在启动时就立即清理
|
||||||
|
time.Sleep(5 * time.Minute)
|
||||||
|
|
||||||
ticker := time.NewTicker(constants.CleanupInterval)
|
ticker := time.NewTicker(constants.CleanupInterval)
|
||||||
for range ticker.C {
|
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()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error starting cleanup transaction: %v", err)
|
log.Printf("Error starting cleanup transaction: %v", err)
|
||||||
continue
|
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)
|
cutoff := time.Now().Add(-constants.DataRetention)
|
||||||
_, err = tx.Exec(`DELETE FROM metrics_history WHERE timestamp < ?`, cutoff)
|
tables := []string{
|
||||||
if err != nil {
|
"metrics_history",
|
||||||
tx.Rollback()
|
"status_stats",
|
||||||
log.Printf("Error cleaning metrics_history: %v", err)
|
"path_stats",
|
||||||
continue
|
"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 {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
log.Printf("Error cleaning status_stats: %v", err)
|
log.Printf("Error cleaning %s: %v", table, err)
|
||||||
continue
|
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 {
|
if err := tx.Commit(); err != nil {
|
||||||
log.Printf("Error committing cleanup transaction: %v", err)
|
log.Printf("Error committing cleanup transaction: %v", err)
|
||||||
} else {
|
} 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 {
|
func (db *MetricsDB) SaveMetrics(stats map[string]interface{}) error {
|
||||||
tx, err := db.DB.Begin()
|
tx, err := db.DB.Begin()
|
||||||
if err != nil {
|
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()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,9 +370,18 @@ func (db *MetricsDB) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *MetricsDB) GetRecentMetrics(hours int) ([]HistoricalMetrics, 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
|
var interval string
|
||||||
if hours <= 24 {
|
if hours <= 24 {
|
||||||
|
if hours <= 1 {
|
||||||
interval = "%Y-%m-%d %H:%M:00" // 按分钟分组
|
interval = "%Y-%m-%d %H:%M:00" // 按分钟分组
|
||||||
|
} else {
|
||||||
|
interval = "%Y-%m-%d %H:00:00" // 按小时分组
|
||||||
|
}
|
||||||
} else if hours <= 168 {
|
} else if hours <= 168 {
|
||||||
interval = "%Y-%m-%d %H:00:00" // 按小时分组
|
interval = "%Y-%m-%d %H:00:00" // 按小时分组
|
||||||
} else {
|
} else {
|
||||||
@ -411,6 +491,26 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
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(`
|
_, err = tx.Exec(`
|
||||||
INSERT INTO performance_metrics (
|
INSERT INTO performance_metrics (
|
||||||
@ -426,26 +526,30 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用事务提高写入性能
|
||||||
|
tx.Exec("PRAGMA synchronous = OFF")
|
||||||
|
tx.Exec("PRAGMA journal_mode = MEMORY")
|
||||||
|
|
||||||
// 保存状态码统计
|
// 保存状态码统计
|
||||||
statusStats := stats["status_code_stats"].(map[string]int64)
|
statusStats := stats["status_code_stats"].(map[string]int64)
|
||||||
stmt, err := tx.Prepare(`
|
values := make([]string, 0, len(statusStats))
|
||||||
INSERT INTO status_code_history (status_group, count)
|
args := make([]interface{}, 0, len(statusStats)*2)
|
||||||
VALUES (?, ?)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
for group, count := range statusStats {
|
for group, count := range statusStats {
|
||||||
if _, err := stmt.Exec(group, count); err != nil {
|
values = append(values, "(?, ?)")
|
||||||
return err
|
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)
|
pathStats := stats["top_paths"].([]PathMetrics)
|
||||||
stmt, err = tx.Prepare(`
|
pathStmt, err := tx.Prepare(`
|
||||||
INSERT INTO popular_paths_history (
|
INSERT INTO popular_paths_history (
|
||||||
path, request_count, error_count, avg_latency, bytes_transferred
|
path, request_count, error_count, avg_latency, bytes_transferred
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
@ -453,9 +557,10 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer pathStmt.Close()
|
||||||
|
|
||||||
for _, p := range pathStats {
|
for _, p := range pathStats {
|
||||||
if _, err := stmt.Exec(
|
if _, err := pathStmt.Exec(
|
||||||
p.Path, p.RequestCount, p.ErrorCount,
|
p.Path, p.RequestCount, p.ErrorCount,
|
||||||
p.AvgLatency, p.BytesTransferred,
|
p.AvgLatency, p.BytesTransferred,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@ -465,19 +570,109 @@ func (db *MetricsDB) SaveFullMetrics(stats map[string]interface{}) error {
|
|||||||
|
|
||||||
// 保存引用来源
|
// 保存引用来源
|
||||||
refererStats := stats["top_referers"].([]PathMetrics)
|
refererStats := stats["top_referers"].([]PathMetrics)
|
||||||
stmt, err = tx.Prepare(`
|
refererStmt, err := tx.Prepare(`
|
||||||
INSERT INTO referer_history (referer, request_count)
|
INSERT INTO referer_history (referer, request_count)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer refererStmt.Close()
|
||||||
|
|
||||||
for _, r := range refererStats {
|
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 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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupCloseHandler(callback func()) {
|
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)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
|
once.Do(func() {
|
||||||
callback()
|
callback()
|
||||||
|
done <- true
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case <-c:
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user