Skip to content

Commit

Permalink
feat(docker, config, api): update docker-compose for logging, enhance…
Browse files Browse the repository at this point in the history
… app structure, and add system metrics display

- Updated docker-compose.yml to mount logs directory.
- Changed BASE_URL environment variable to point to the new API endpoint.
- Refactored main.go to implement a structured App type, improving initialization and graceful shutdown.
- Enhanced config management with JSON loading and environment variable support.
- Added monitoring capabilities in api_handler for logging request metrics.
- Introduced new metrics display in index.html with corresponding CSS styles for better visualization.
  • Loading branch information
woodchen-ink committed Nov 30, 2024
1 parent f31ff5c commit e70ca4c
Show file tree
Hide file tree
Showing 15 changed files with 683 additions and 43 deletions.
17 changes: 17 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
50 changes: 48 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions data/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 21 additions & 2 deletions handlers/api_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 14 additions & 0 deletions handlers/metrics_handler.go
Original file line number Diff line number Diff line change
@@ -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)
}
118 changes: 80 additions & 38 deletions main.go
Original file line number Diff line number Diff line change
@@ -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))
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)
}

// 初始化配置
if err := config.Load("/root/data/config.json"); err != nil {
return err
}

// 初始化日志
logging.SetupLogging()
statsManager := stats.NewStatsManager("data/stats.json")

// 设置优雅关闭
setupGracefulShutdown(statsManager)
// 初始化统计管理器
a.Stats = stats.NewStatsManager(config.Get().Storage.StatsFile)

// 初始化handlers
if err := handlers.InitializeHandlers(statsManager); err != nil {
log.Fatal("Failed to initialize handlers:", err)
// 初始化服务
if err := services.InitializeCSVService(); err != nil {
return err
}

// 初始化加载所有CSV内容
if err := services.InitializeCSVService(); err != nil {
log.Fatal("Failed to initialize CSV Service:", 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 {
log.Fatal(err)
// 创建 HTTP 服务器
cfg := config.Get().Server
a.server = &http.Server{
Addr: cfg.Port,
Handler: a.router,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
MaxHeaderBytes: cfg.MaxHeaderBytes,
}
}

func setupGracefulShutdown(statsManager *stats.StatsManager) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
return nil
}

func (a *App) Run() error {
// 启动服务器
go func() {
<-c
log.Println("Server is shutting down...")
statsManager.Shutdown()
log.Println("Stats manager shutdown completed")
os.Exit(0)
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 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)
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)
}
}
Loading

0 comments on commit e70ca4c

Please sign in to comment.