diff --git a/config.json b/config.json new file mode 100644 index 0000000..4991003 --- /dev/null +++ b/config.json @@ -0,0 +1,17 @@ +{ + "server": { + "port": ":5003", + "read_timeout": "30s", + "write_timeout": "30s", + "max_header_bytes": 1048576 + }, + "storage": { + "data_dir": "/root/data", + "stats_file": "/root/data/stats.json", + "log_file": "/var/log/random-api/server.log" + }, + "api": { + "base_url": "", + "request_timeout": "10s" + } +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index ab9ab58..62e33ec 100644 --- a/config/config.go +++ b/config/config.go @@ -1,20 +1,66 @@ package config import ( + "encoding/json" "math/rand" + "os" "time" ) const ( - Port = ":5003" - RequestTimeout = 10 * time.Second EnvBaseURL = "BASE_URL" + DefaultPort = ":5003" + RequestTimeout = 10 * time.Second ) +type Config struct { + Server struct { + Port string `json:"port"` + ReadTimeout time.Duration `json:"read_timeout"` + WriteTimeout time.Duration `json:"write_timeout"` + MaxHeaderBytes int `json:"max_header_bytes"` + } `json:"server"` + + Storage struct { + DataDir string `json:"data_dir"` + StatsFile string `json:"stats_file"` + LogFile string `json:"log_file"` + } `json:"storage"` + + API struct { + BaseURL string `json:"base_url"` + RequestTimeout time.Duration `json:"request_timeout"` + } `json:"api"` +} + var ( + cfg Config RNG *rand.Rand ) +func Load(configFile string) error { + file, err := os.Open(configFile) + if err != nil { + return err + } + defer file.Close() + + decoder := json.NewDecoder(file) + if err := decoder.Decode(&cfg); err != nil { + return err + } + + if envBaseURL := os.Getenv(EnvBaseURL); envBaseURL != "" { + cfg.API.BaseURL = envBaseURL + } + + return nil +} + +func Get() *Config { + return &cfg +} + func InitRNG(r *rand.Rand) { RNG = r } diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000..4991003 --- /dev/null +++ b/data/config.json @@ -0,0 +1,17 @@ +{ + "server": { + "port": ":5003", + "read_timeout": "30s", + "write_timeout": "30s", + "max_header_bytes": 1048576 + }, + "storage": { + "data_dir": "/root/data", + "stats_file": "/root/data/stats.json", + "log_file": "/var/log/random-api/server.log" + }, + "api": { + "base_url": "", + "request_timeout": "10s" + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f71eb73..285d68a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,9 @@ services: - "5003:5003" volumes: - ./public:/root/public + - ./logs:/var/log/random-api - ./data:/root/data environment: - TZ=Asia/Shanghai - - BASE_URL=https://example.com/csvfile + - BASE_URL=https://github-file.czl.net/random-api.czl.net restart: unless-stopped diff --git a/handlers/api_handler.go b/handlers/api_handler.go index 4905e23..d5c8a25 100644 --- a/handlers/api_handler.go +++ b/handlers/api_handler.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "net/url" + "random-api-go/monitoring" "random-api-go/services" "random-api-go/stats" "random-api-go/utils" @@ -31,9 +32,7 @@ func HandleAPIRequest(w http.ResponseWriter, r *http.Request) { sourceInfo := "direct" if referer != "" { if parsedURL, err := url.Parse(referer); err == nil { - // 包含主机名和路径 sourceInfo = parsedURL.Host + parsedURL.Path - // 如果有查询参数,也可以加上 if parsedURL.RawQuery != "" { sourceInfo += "?" + parsedURL.RawQuery } @@ -44,6 +43,15 @@ func HandleAPIRequest(w http.ResponseWriter, r *http.Request) { 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 } @@ -80,6 +88,17 @@ func HandleAPIRequest(w http.ResponseWriter, r *http.Request) { 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, // 真实IP diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..34d4bb2 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "net/http" + "random-api-go/router" + "random-api-go/stats" +) + +type Router interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) +} + +type Handlers struct { + Stats *stats.StatsManager +} + +func (h *Handlers) HandleAPIRequest(w http.ResponseWriter, r *http.Request) { + HandleAPIRequest(w, r) +} + +func (h *Handlers) HandleStats(w http.ResponseWriter, r *http.Request) { + HandleStats(w, r) +} + +func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) { + HandleURLStats(w, r) +} + +func (h *Handlers) HandleMetrics(w http.ResponseWriter, r *http.Request) { + HandleMetrics(w, r) +} + +func (h *Handlers) Setup(r *router.Router) { + r.HandleFunc("/pic/", h.HandleAPIRequest) + r.HandleFunc("/video/", h.HandleAPIRequest) + r.HandleFunc("/stats", h.HandleStats) + r.HandleFunc("/urlstats", h.HandleURLStats) + r.HandleFunc("/metrics", h.HandleMetrics) +} diff --git a/handlers/metrics_handler.go b/handlers/metrics_handler.go new file mode 100644 index 0000000..2028a4d --- /dev/null +++ b/handlers/metrics_handler.go @@ -0,0 +1,14 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "random-api-go/monitoring" +) + +func HandleMetrics(w http.ResponseWriter, r *http.Request) { + metrics := monitoring.CollectMetrics() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metrics) +} diff --git a/main.go b/main.go index 5db7963..470f19d 100644 --- a/main.go +++ b/main.go @@ -1,75 +1,117 @@ package main import ( + "context" + "fmt" "log" - "math/rand" "net/http" "os" "os/signal" "random-api-go/config" "random-api-go/handlers" "random-api-go/logging" + "random-api-go/router" "random-api-go/services" "random-api-go/stats" - "syscall" "time" ) -func init() { - if err := os.MkdirAll("data", 0755); err != nil { - log.Fatal("Failed to create data directory:", err) +type App struct { + server *http.Server + router *router.Router + Stats *stats.StatsManager +} + +func NewApp() *App { + return &App{ + router: router.New(), } } -func main() { - source := rand.NewSource(time.Now().UnixNano()) - config.InitRNG(rand.New(source)) - - logging.SetupLogging() - statsManager := stats.NewStatsManager("data/stats.json") - - // 设置优雅关闭 - setupGracefulShutdown(statsManager) - - // 初始化handlers - if err := handlers.InitializeHandlers(statsManager); err != nil { - log.Fatal("Failed to initialize handlers:", err) +func (a *App) Initialize() error { + // 创建必要的目录 + if err := os.MkdirAll(config.Get().Storage.DataDir, 0755); err != nil { + return fmt.Errorf("failed to create data directory: %w", err) } - // 初始化加载所有CSV内容 + // 初始化配置 + if err := config.Load("/root/data/config.json"); err != nil { + return err + } + + // 初始化日志 + logging.SetupLogging() + + // 初始化统计管理器 + a.Stats = stats.NewStatsManager(config.Get().Storage.StatsFile) + + // 初始化服务 if err := services.InitializeCSVService(); err != nil { - log.Fatal("Failed to initialize CSV Service:", err) + return err + } + + // 创建 handlers + handlers := &handlers.Handlers{ + Stats: a.Stats, } // 设置路由 - setupRoutes() + a.router.Setup(handlers) - log.Printf("Server starting on %s...\n", config.Port) - if err := http.ListenAndServe(config.Port, nil); err != nil { + // 创建 HTTP 服务器 + cfg := config.Get().Server + a.server = &http.Server{ + Addr: cfg.Port, + Handler: a.router, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + MaxHeaderBytes: cfg.MaxHeaderBytes, + } + + return nil +} + +func (a *App) Run() error { + // 启动服务器 + go func() { + log.Printf("Server starting on %s...\n", a.server.Addr) + if err := a.server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + // 优雅关闭 + return a.gracefulShutdown() +} + +func (a *App) gracefulShutdown() error { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Server is shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + a.Stats.Shutdown() + + if err := a.server.Shutdown(ctx); err != nil { + return err + } + + log.Println("Server shutdown completed") + return nil +} + +func main() { + app := NewApp() + if err := app.Initialize(); err != nil { + log.Fatal(err) + } + + if err := app.Run(); err != nil { log.Fatal(err) } } - -func setupGracefulShutdown(statsManager *stats.StatsManager) { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - - go func() { - <-c - log.Println("Server is shutting down...") - statsManager.Shutdown() - log.Println("Stats manager shutdown completed") - os.Exit(0) - }() -} - -func setupRoutes() { - fs := http.FileServer(http.Dir("./public")) - http.Handle("/", fs) - http.HandleFunc("/pic/", handlers.HandleAPIRequest) - http.HandleFunc("/video/", handlers.HandleAPIRequest) - http.HandleFunc("/stats", handlers.HandleStats) - // 添加URL统计接口 - http.HandleFunc("/urlstats", handlers.HandleURLStats) -} diff --git a/middleware/metrics.go b/middleware/metrics.go new file mode 100644 index 0000000..bba8e06 --- /dev/null +++ b/middleware/metrics.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "net/http" + "random-api-go/monitoring" + "random-api-go/utils" + "time" +) + +func MetricsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // 创建自定义的ResponseWriter来捕获状态码 + rw := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + // 处理请求 + next.ServeHTTP(rw, r) + + // 记录请求数据 + duration := time.Since(start) + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: rw.statusCode, + Latency: float64(duration.Microseconds()) / 1000, + IP: utils.GetRealIP(r), + Referer: r.Referer(), + }) + }) +} + +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(statusCode int) { + rw.statusCode = statusCode + rw.ResponseWriter.WriteHeader(statusCode) +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..8cd5a49 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,25 @@ +package middleware + +import "net/http" + +// Chain 用于组合多个中间件 +func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } +} + +// Recovery 中间件用于捕获 panic +func Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/monitoring/metrics.go b/monitoring/metrics.go new file mode 100644 index 0000000..1b1a6c5 --- /dev/null +++ b/monitoring/metrics.go @@ -0,0 +1,110 @@ +package monitoring + +import ( + "runtime" + "sync" + "time" +) + +type SystemMetrics struct { + // 基础指标 + Uptime time.Duration `json:"uptime"` + StartTime time.Time `json:"start_time"` + + // 系统指标 + NumCPU int `json:"num_cpu"` + NumGoroutine int `json:"num_goroutine"` + MemoryStats struct { + Alloc uint64 `json:"alloc"` + TotalAlloc uint64 `json:"total_alloc"` + Sys uint64 `json:"sys"` + HeapAlloc uint64 `json:"heap_alloc"` + HeapSys uint64 `json:"heap_sys"` + } `json:"memory_stats"` + + // 性能指标 + RequestCount int64 `json:"request_count"` + AverageLatency float64 `json:"average_latency"` + + // 流量统计 + TotalBytesIn int64 `json:"total_bytes_in"` + TotalBytesOut int64 `json:"total_bytes_out"` + + // 状态码统计 + StatusCodes map[int]int64 `json:"status_codes"` + + // 路径延迟统计 + PathLatencies map[string]float64 `json:"path_latencies"` + + // 最近请求 + RecentRequests []RequestLog `json:"recent_requests"` + + // 热门引用来源 + TopReferers map[string]int64 `json:"top_referers"` +} + +type RequestLog struct { + Time time.Time `json:"time"` + Path string `json:"path"` + Method string `json:"method"` + StatusCode int `json:"status_code"` + Latency float64 `json:"latency"` + IP string `json:"ip"` + Referer string `json:"referer"` +} + +var ( + metrics SystemMetrics + mu sync.RWMutex + startTime = time.Now() +) + +func init() { + metrics.StatusCodes = make(map[int]int64) + metrics.PathLatencies = make(map[string]float64) + metrics.TopReferers = make(map[string]int64) + metrics.RecentRequests = make([]RequestLog, 0, 100) +} + +func CollectMetrics() *SystemMetrics { + mu.Lock() + defer mu.Unlock() + + var m runtime.MemStats + runtime.ReadMemStats(&m) + + metrics.Uptime = time.Since(startTime) + metrics.StartTime = startTime + metrics.NumCPU = runtime.NumCPU() + metrics.NumGoroutine = runtime.NumGoroutine() + + metrics.MemoryStats.Alloc = m.Alloc + metrics.MemoryStats.TotalAlloc = m.TotalAlloc + metrics.MemoryStats.Sys = m.Sys + metrics.MemoryStats.HeapAlloc = m.HeapAlloc + metrics.MemoryStats.HeapSys = m.HeapSys + + return &metrics +} + +func LogRequest(log RequestLog) { + mu.Lock() + defer mu.Unlock() + + metrics.RequestCount++ + metrics.StatusCodes[log.StatusCode]++ + metrics.TopReferers[log.Referer]++ + + // 更新路径延迟 + if existing, ok := metrics.PathLatencies[log.Path]; ok { + metrics.PathLatencies[log.Path] = (existing + log.Latency) / 2 + } else { + metrics.PathLatencies[log.Path] = log.Latency + } + + // 保存最近请求记录 + metrics.RecentRequests = append(metrics.RecentRequests, log) + if len(metrics.RecentRequests) > 100 { + metrics.RecentRequests = metrics.RecentRequests[1:] + } +} diff --git a/public/css/main.css b/public/css/main.css index 64c4e08..bf6c0a2 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -173,3 +173,95 @@ table { transform: translate(-50%, -20px); } } + +/* 系统监控样式 */ +.metrics-container { + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + padding: 20px; + margin: 20px 0; +} + +.metrics-section { + margin-bottom: 20px; +} + +.metrics-section h3 { + color: #2196f3; + margin-bottom: 15px; + font-size: 1.1em; + border-bottom: 1px solid rgba(33, 150, 243, 0.2); + padding-bottom: 5px; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.metric-item { + background: rgba(255, 255, 255, 0.1); + padding: 12px; + border-radius: 6px; + font-size: 0.9em; +} + +.status-codes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 10px; +} + +.status-code-item { + background: rgba(255, 255, 255, 0.1); + padding: 8px 12px; + border-radius: 6px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.recent-requests table { + width: 100%; + border-collapse: collapse; + font-size: 0.9em; +} + +.recent-requests th, +.recent-requests td { + padding: 8px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.recent-requests th { + color: #2196f3; + font-weight: 500; +} + +.top-referers { + display: grid; + gap: 8px; +} + +.referer-item { + background: rgba(255, 255, 255, 0.1); + padding: 8px 12px; + border-radius: 6px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.referer { + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.count { + color: #2196f3; + font-weight: 500; +} diff --git a/public/index.html b/public/index.html index ef63a70..2bc02a7 100644 --- a/public/index.html +++ b/public/index.html @@ -239,6 +239,137 @@ // 定期更新统计数据 setInterval(loadStats, 5 * 1000); + + async function loadMetrics() { + try { + const response = await fetch('/metrics'); + const metrics = await response.json(); + updateMetricsDisplay(metrics); + } catch (error) { + console.error('Error loading metrics:', error); + } + } + + function updateMetricsDisplay(metrics) { + const metricsHtml = ` +
+
+

基础指标

+
+
运行时间:${formatDuration(metrics.uptime)}
+
启动时间:${new Date(metrics.start_time).toLocaleString()}
+
+
+ +
+

系统指标

+
+
CPU核心数:${metrics.num_cpu}
+
Goroutine数:${metrics.num_goroutine}
+
内存使用:${formatBytes(metrics.memory_stats.heap_alloc)}
+
系统内存:${formatBytes(metrics.memory_stats.heap_sys)}
+
+
+ +
+

性能指标

+
+
总请求数:${metrics.request_count}
+
平均延迟:${metrics.average_latency.toFixed(2)}ms
+
+
+ +
+

状态码统计

+
+ ${Object.entries(metrics.status_codes) + .map(([code, count]) => ` +
+ ${code} + ${count} +
+ `).join('')} +
+
+ +
+

最近请求

+
+ + + + + + + + + + + + ${metrics.recent_requests.slice(0, 10).map(req => ` + + + + + + + + `).join('')} + +
时间路径方法状态延迟
${new Date(req.time).toLocaleString()}${req.path}${req.method}${req.status_code}${req.latency.toFixed(2)}ms
+
+
+ +
+

热门引用来源

+
+ ${Object.entries(metrics.top_referers) + .sort(([,a], [,b]) => b - a) + .slice(0, 10) + .map(([referer, count]) => ` +
+ ${referer || '直接访问'} + ${count} +
+ `).join('')} +
+
+
+ `; + + const metricsElement = document.getElementById('system-metrics'); + if (metricsElement) { + metricsElement.innerHTML = metricsHtml; + } + } + + function formatBytes(bytes) { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${size.toFixed(2)} ${units[unitIndex]}`; + } + + function formatDuration(ns) { + const duration = ns / 1e9; // 转换为秒 + const days = Math.floor(duration / 86400); + const hours = Math.floor((duration % 86400) / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = Math.floor(duration % 60); + return `${days}天 ${hours}时 ${minutes}分 ${seconds}秒`; + } + + // 定期更新监控数据 + setInterval(loadMetrics, 5000); + + // 初始加载 + document.addEventListener('DOMContentLoaded', () => { + loadMetrics(); + }); diff --git a/public/index.md b/public/index.md index 5272be2..6600484 100644 --- a/public/index.md +++ b/public/index.md @@ -5,6 +5,8 @@
+
+ --- diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..60e43fe --- /dev/null +++ b/router/router.go @@ -0,0 +1,40 @@ +package router + +import ( + "net/http" + "random-api-go/middleware" +) + +type Router struct { + mux *http.ServeMux +} + +type Handler interface { + Setup(r *Router) +} + +func New() *Router { + return &Router{ + mux: http.NewServeMux(), + } +} + +func (r *Router) Setup(h Handler) { + // 静态文件服务 + fileServer := http.FileServer(http.Dir("./public")) + r.mux.Handle("/", middleware.Chain( + middleware.Recovery, + middleware.MetricsMiddleware, + )(fileServer)) + + // 设置API路由 + h.Setup(r) +} + +func (r *Router) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { + r.mux.HandleFunc(pattern, handler) +} + +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + r.mux.ServeHTTP(w, req) +}