feat(handlers, monitoring, public): enhance API request handling and improve metrics logging

- Implemented detailed logging for API requests, including real IP, referer, and latency metrics.
- Added dynamic routing for handling requests to /pic/ and /video/ endpoints.
- Enhanced error handling for CSV content fetching and improved response messages.
- Updated the HTML and JavaScript to load and display statistics more efficiently, including filtering based on endpoint configuration.
- Introduced new metrics collection logic to only log relevant API requests, optimizing performance and clarity.
This commit is contained in:
wood chen 2024-11-30 23:52:31 +08:00
parent 44afb8cae9
commit 1b2fd3ea85
4 changed files with 235 additions and 123 deletions

View File

@ -1,9 +1,18 @@
package handlers package handlers
import ( import (
"encoding/json"
"fmt"
"log"
"net/http" "net/http"
"net/url"
"random-api-go/monitoring"
"random-api-go/router" "random-api-go/router"
"random-api-go/services"
"random-api-go/stats" "random-api-go/stats"
"random-api-go/utils"
"strings"
"time"
) )
type Router interface { type Router interface {
@ -15,24 +24,146 @@ type Handlers struct {
} }
func (h *Handlers) HandleAPIRequest(w http.ResponseWriter, r *http.Request) { func (h *Handlers) HandleAPIRequest(w http.ResponseWriter, r *http.Request) {
HandleAPIRequest(w, r) start := time.Now()
realIP := utils.GetRealIP(r)
// 获取并处理 referer
sourceInfo := "direct"
if referer := r.Referer(); referer != "" {
if parsedURL, err := url.Parse(referer); err == nil {
sourceInfo = parsedURL.Host + parsedURL.Path
if parsedURL.RawQuery != "" {
sourceInfo += "?" + parsedURL.RawQuery
}
}
}
path := strings.TrimPrefix(r.URL.Path, "/")
pathSegments := strings.Split(path, "/")
if len(pathSegments) < 2 {
monitoring.LogRequest(monitoring.RequestLog{
Time: time.Now(),
Path: r.URL.Path,
Method: r.Method,
StatusCode: http.StatusNotFound,
Latency: float64(time.Since(start).Microseconds()) / 1000,
IP: realIP,
Referer: sourceInfo,
})
http.NotFound(w, r)
return
}
prefix := pathSegments[0]
suffix := pathSegments[1]
services.Mu.RLock()
csvPath, ok := services.CSVPathsCache[prefix][suffix]
services.Mu.RUnlock()
if !ok {
monitoring.LogRequest(monitoring.RequestLog{
Time: time.Now(),
Path: r.URL.Path,
Method: r.Method,
StatusCode: http.StatusNotFound,
Latency: float64(time.Since(start).Microseconds()) / 1000,
IP: realIP,
Referer: sourceInfo,
})
http.NotFound(w, r)
return
}
selector, err := services.GetCSVContent(csvPath)
if err != nil {
log.Printf("Error fetching CSV content: %v", err)
monitoring.LogRequest(monitoring.RequestLog{
Time: time.Now(),
Path: r.URL.Path,
Method: r.Method,
StatusCode: http.StatusInternalServerError,
Latency: float64(time.Since(start).Microseconds()) / 1000,
IP: realIP,
Referer: sourceInfo,
})
http.Error(w, "Failed to fetch content", http.StatusInternalServerError)
return
}
if len(selector.URLs) == 0 {
monitoring.LogRequest(monitoring.RequestLog{
Time: time.Now(),
Path: r.URL.Path,
Method: r.Method,
StatusCode: http.StatusNotFound,
Latency: float64(time.Since(start).Microseconds()) / 1000,
IP: realIP,
Referer: sourceInfo,
})
http.Error(w, "No content available", http.StatusNotFound)
return
}
randomURL := selector.GetRandomURL()
endpoint := fmt.Sprintf("%s/%s", prefix, suffix)
h.Stats.IncrementCalls(endpoint)
duration := time.Since(start)
monitoring.LogRequest(monitoring.RequestLog{
Time: time.Now(),
Path: r.URL.Path,
Method: r.Method,
StatusCode: http.StatusFound,
Latency: float64(duration.Microseconds()) / 1000,
IP: realIP,
Referer: sourceInfo,
})
log.Printf(" %-12s | %-15s | %-6s | %-20s | %-20s | %-50s",
duration,
realIP,
r.Method,
r.URL.Path,
sourceInfo,
randomURL,
)
http.Redirect(w, r, randomURL, http.StatusFound)
} }
func (h *Handlers) HandleStats(w http.ResponseWriter, r *http.Request) { func (h *Handlers) HandleStats(w http.ResponseWriter, r *http.Request) {
HandleStats(w, r) w.Header().Set("Content-Type", "application/json")
stats := h.Stats.GetStats()
if err := json.NewEncoder(w).Encode(stats); err != nil {
http.Error(w, "Error encoding stats", http.StatusInternalServerError)
log.Printf("Error encoding stats: %v", err)
}
} }
func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) { func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) {
HandleURLStats(w, r) w.Header().Set("Content-Type", "application/json")
services.Mu.RLock()
response := map[string]interface{}{
"paths": services.CSVPathsCache,
}
services.Mu.RUnlock()
json.NewEncoder(w).Encode(response)
} }
func (h *Handlers) HandleMetrics(w http.ResponseWriter, r *http.Request) { func (h *Handlers) HandleMetrics(w http.ResponseWriter, r *http.Request) {
HandleMetrics(w, r) metrics := monitoring.CollectMetrics()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(metrics)
} }
func (h *Handlers) Setup(r *router.Router) { func (h *Handlers) Setup(r *router.Router) {
// 动态路由处理
r.HandleFunc("/pic/", h.HandleAPIRequest) r.HandleFunc("/pic/", h.HandleAPIRequest)
r.HandleFunc("/video/", h.HandleAPIRequest) r.HandleFunc("/video/", h.HandleAPIRequest)
// API 统计和监控
r.HandleFunc("/stats", h.HandleStats) r.HandleFunc("/stats", h.HandleStats)
r.HandleFunc("/urlstats", h.HandleURLStats) r.HandleFunc("/urlstats", h.HandleURLStats)
r.HandleFunc("/metrics", h.HandleMetrics) r.HandleFunc("/metrics", h.HandleMetrics)

View File

@ -2,6 +2,7 @@ package monitoring
import ( import (
"runtime" "runtime"
"strings"
"sync" "sync"
"time" "time"
) )
@ -95,6 +96,8 @@ func LogRequest(log RequestLog) {
metrics.StatusCodes[log.StatusCode]++ metrics.StatusCodes[log.StatusCode]++
metrics.TopReferers[log.Referer]++ metrics.TopReferers[log.Referer]++
// 只记录 API 请求
if strings.HasPrefix(log.Path, "/pic/") || strings.HasPrefix(log.Path, "/video/") {
// 更新路径延迟 // 更新路径延迟
if existing, ok := metrics.PathLatencies[log.Path]; ok { if existing, ok := metrics.PathLatencies[log.Path]; ok {
metrics.PathLatencies[log.Path] = (existing + log.Latency) / 2 metrics.PathLatencies[log.Path] = (existing + log.Latency) / 2
@ -107,4 +110,5 @@ func LogRequest(log RequestLog) {
if len(metrics.RecentRequests) > 100 { if len(metrics.RecentRequests) > 100 {
metrics.RecentRequests = metrics.RecentRequests[1:] metrics.RecentRequests = metrics.RecentRequests[1:]
} }
}
} }

View File

@ -0,0 +1,10 @@
{
"pic": {
"all": "随机图片",
"fj": "随机风景",
"loading": "随机加载图"
},
"video": {
"all": "随机视频"
}
}

View File

@ -34,9 +34,9 @@
// 用于存储配置的全局变量 // 用于存储配置的全局变量
let cachedEndpointConfig = null; let cachedEndpointConfig = null;
// 加载配置的函数 // 加载配置的函数
async function loadEndpointConfig() { async function loadEndpointConfig() {
// 如果已经有缓存的配置,直接返回
if (cachedEndpointConfig) { if (cachedEndpointConfig) {
return cachedEndpointConfig; return cachedEndpointConfig;
} }
@ -46,54 +46,50 @@
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// 保存配置到缓存
cachedEndpointConfig = await response.json(); cachedEndpointConfig = await response.json();
return cachedEndpointConfig; return cachedEndpointConfig;
} catch (error) { } catch (error) {
console.error('加载endpoint配置失败:', error); console.error('加载endpoint配置失败:', error);
return {}; // 返回空对象作为默认值 return {};
} }
} }
// 加载统计数据 // 加载统计数据
async function loadStats() { async function loadStats() {
try { try {
// 添加刷新动画 const [statsResponse, urlStatsResponse, endpointConfig] = await Promise.all([
const refreshIcon = document.querySelector('.refresh-icon'); fetch('/stats'),
const summaryElement = document.getElementById('stats-summary'); fetch('/urlstats'),
const detailElement = document.getElementById('stats-detail'); loadEndpointConfig()
]);
if (refreshIcon) { const stats = await statsResponse.json();
refreshIcon.classList.add('spinning'); const urlStats = await urlStatsResponse.json();
// 只显示 endpoint.json 中配置的路径
const filteredPaths = {};
for (const [category, types] of Object.entries(endpointConfig)) {
if (urlStats.paths[category]) {
filteredPaths[category] = {};
for (const [type, desc] of Object.entries(types)) {
if (urlStats.paths[category][type]) {
filteredPaths[category][type] = {
path: urlStats.paths[category][type],
description: desc
};
}
}
} }
if (summaryElement) summaryElement.classList.add('fade');
if (detailElement) detailElement.classList.add('fade');
// 获取数据
const response = await fetch('/stats');
const stats = await response.json();
// 更新统计
await updateStats(stats);
// 移除动画
setTimeout(() => {
if (refreshIcon) {
refreshIcon.classList.remove('spinning');
} }
if (summaryElement) summaryElement.classList.remove('fade');
if (detailElement) detailElement.classList.remove('fade');
}, 300);
await updateStats(stats, filteredPaths);
} catch (error) { } catch (error) {
console.error('Error loading stats:', error); console.error('Error loading stats:', error);
} }
} }
// 处理统计数据 // 更新统计显示
async function updateStats(stats) { async function updateStats(stats, paths) {
const endpointConfig = await loadEndpointConfig();
const startDate = new Date('2024-11-1'); const startDate = new Date('2024-11-1');
const today = new Date(); const today = new Date();
const daysSinceStart = Math.ceil((today - startDate) / (1000 * 60 * 60 * 24)); const daysSinceStart = Math.ceil((today - startDate) / (1000 * 60 * 60 * 24));
@ -101,15 +97,15 @@
let totalCalls = 0; let totalCalls = 0;
let todayCalls = 0; let todayCalls = 0;
// 计算总调用次数
Object.entries(stats).forEach(([endpoint, stat]) => { Object.entries(stats).forEach(([endpoint, stat]) => {
if (endpointConfig[endpoint]) {
totalCalls += stat.total_calls; totalCalls += stat.total_calls;
todayCalls += stat.today_calls; todayCalls += stat.today_calls;
}
}); });
const avgCallsPerDay = Math.round(totalCalls / daysSinceStart); const avgCallsPerDay = Math.round(totalCalls / daysSinceStart);
// 更新总览统计
const summaryHtml = ` const summaryHtml = `
<div class="stats-summary"> <div class="stats-summary">
<div class="stats-header"> <div class="stats-header">
@ -124,34 +120,32 @@
</div> </div>
`; `;
const sortedEndpoints = Object.entries(stats) // 获取 endpoint 配置
.filter(([endpoint]) => endpointConfig[endpoint]) const endpointConfig = await loadEndpointConfig();
.sort(([endpointA], [endpointB]) => {
const orderA = endpointConfig[endpointA].order;
const orderB = endpointConfig[endpointB].order;
return orderA - orderB;
});
// 生成详细统计表格
let detailHtml = ` let detailHtml = `
<table> <table>
<thead> <thead>
<tr> <tr>
<th>接口</th> <th>接口名称</th>
<th>今日调用</th> <th>今日调用</th>
<th>总调用</th> <th>总调用</th>
<th>URL数量</th> <th>URL数量</th>
<th>查看</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
`; `;
// 同时加载URL统计数据 // 按 order 排序并生成表格行
const urlStatsResponse = await fetch('/urlstats'); const endpoints = Object.entries(endpointConfig)
const urlStats = await urlStatsResponse.json(); .sort(([, a], [, b]) => a.order - b.order);
sortedEndpoints.forEach(([endpoint, stat]) => { for (const [endpoint, config] of endpoints) {
const stat = stats[endpoint] || { today_calls: 0, total_calls: 0 };
const urlCount = urlStats[endpoint]?.total_urls || 0; const urlCount = urlStats[endpoint]?.total_urls || 0;
detailHtml += ` detailHtml += `
<tr> <tr>
<td> <td>
@ -159,22 +153,26 @@
onclick="copyToClipboard('${endpoint}')" onclick="copyToClipboard('${endpoint}')"
class="endpoint-link" class="endpoint-link"
title="点击复制链接"> title="点击复制链接">
${getDisplayName(endpoint, endpointConfig)} ${config.name}
</a> </a>
</td> </td>
<td>${stat.today_calls}</td> <td>${stat.today_calls}</td>
<td>${stat.total_calls}</td> <td>${stat.total_calls}</td>
<td>${urlCount}</td> <td>${urlCount}</td>
<td><a href="${endpoint}" target="_blank" rel="noopener noreferrer">👀</a></td> <td>
<a href="/${endpoint}" target="_blank" rel="noopener noreferrer" title="测试接口">👀</a>
<a href="javascript:void(0)" onclick="copyToClipboard('${endpoint}')" title="复制链接">📋</a>
</td>
</tr> </tr>
`; `;
}); }
detailHtml += ` detailHtml += `
</tbody> </tbody>
</table> </table>
`; `;
// 更新 DOM
const summaryElement = document.getElementById('stats-summary'); const summaryElement = document.getElementById('stats-summary');
const detailElement = document.getElementById('stats-detail'); const detailElement = document.getElementById('stats-detail');
@ -182,46 +180,15 @@
if (detailElement) detailElement.innerHTML = detailHtml; if (detailElement) detailElement.innerHTML = detailHtml;
} }
// 获取显示名称的函数 // 复制链接功能
function getDisplayName(endpoint, endpointConfig) {
return endpointConfig[endpoint]?.name || endpoint;
}
// 修改事件处理函数来处理异步更新
async function refreshStats() {
try {
// 如果配置还没有加载,先加载配置
if (!cachedEndpointConfig) {
await loadEndpointConfig();
}
// 然后获取统计数据
const response = await fetch('/stats');
const stats = await response.json();
await updateStats(stats);
} catch (error) {
console.error('Error:', error);
}
}
// 初始加载
document.addEventListener('DOMContentLoaded', refreshStats);
// 添加复制功能
function copyToClipboard(endpoint) { function copyToClipboard(endpoint) {
const url = `https://random-api.czl.net/${endpoint}`; const url = `${window.location.protocol}//${window.location.host}/${endpoint}`;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
// 显示提示
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = '链接已复制到剪贴板!'; toast.textContent = '链接已复制到剪贴板!';
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
// 2秒后移除提示
setTimeout(() => {
toast.remove();
}, 2000);
}).catch(err => { }).catch(err => {
console.error('复制失败:', err); console.error('复制失败:', err);
}); });