mirror of
https://github.com/woodchen-ink/random-api-go.git
synced 2025-07-18 13:52:02 +08:00
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:
parent
44afb8cae9
commit
1b2fd3ea85
@ -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)
|
||||||
|
@ -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:]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
public/config/endpoint.json
Normal file
10
public/config/endpoint.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"pic": {
|
||||||
|
"all": "随机图片",
|
||||||
|
"fj": "随机风景",
|
||||||
|
"loading": "随机加载图"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"all": "随机视频"
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user