feat(middleware,server): 实现访问日志中间件

- 新增 accesslog 中间件,记录请求方法、路径、状态码、响应大小和处理时间
- 集成到 Server 的 single 和 vhost 模式
- 支持 graceful shutdown 时关闭日志文件

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-03 10:54:53 +08:00
parent 9d24263918
commit 6f3ecb0a9f
3 changed files with 148 additions and 8 deletions

View File

@ -0,0 +1,42 @@
// Package accesslog 提供访问日志中间件,记录每个请求的详细信息。
package accesslog
import (
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
)
// AccessLog 访问日志中间件,记录请求方法、路径、状态码、响应大小和处理时间。
type AccessLog struct {
logger *logging.Logger
}
// New 创建访问日志中间件。
func New(cfg *config.LoggingConfig) *AccessLog {
return &AccessLog{
logger: logging.New(cfg),
}
}
// Name 返回中间件名称。
func (a *AccessLog) Name() string {
return "accesslog"
}
// Process 包装 handler在请求处理后记录访问日志。
func (a *AccessLog) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
start := time.Now()
next(ctx)
duration := time.Since(start)
a.logger.LogAccess(ctx, ctx.Response.StatusCode(), int64(len(ctx.Response.Body())), duration)
}
}
// Close 关闭日志文件。
func (a *AccessLog) Close() error {
return a.logger.Close()
}

View File

@ -0,0 +1,79 @@
package accesslog
import (
"bytes"
"testing"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
func TestAccessLog_Name(t *testing.T) {
al := New(&config.LoggingConfig{})
if al.Name() != "accesslog" {
t.Errorf("expected name 'accesslog', got '%s'", al.Name())
}
}
func TestAccessLog_Process(t *testing.T) {
al := New(&config.LoggingConfig{
Access: config.AccessLogConfig{Format: "json"},
})
// 创建一个简单的 handler
handler := func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(200)
ctx.SetBodyString("hello")
}
// 包装 handler
wrapped := al.Process(handler)
// 创建模拟请求上下文
var ctx fasthttp.RequestCtx
ctx.Init(&fasthttp.Request{}, nil, nil)
// 执行
wrapped(&ctx)
// 验证响应未被修改
if ctx.Response.StatusCode() != 200 {
t.Errorf("expected status 200, got %d", ctx.Response.StatusCode())
}
if !bytes.Equal(ctx.Response.Body(), []byte("hello")) {
t.Errorf("expected body 'hello', got '%s'", ctx.Response.Body())
}
// 清理
al.Close()
}
func TestAccessLog_ProcessWithDuration(t *testing.T) {
al := New(&config.LoggingConfig{
Access: config.AccessLogConfig{Format: "json"},
})
// 创建一个有延迟的 handler
handler := func(ctx *fasthttp.RequestCtx) {
time.Sleep(10 * time.Millisecond)
ctx.SetStatusCode(201)
ctx.SetBodyString("created")
}
wrapped := al.Process(handler)
var ctx fasthttp.RequestCtx
ctx.Init(&fasthttp.Request{}, nil, nil)
start := time.Now()
wrapped(&ctx)
elapsed := time.Since(start)
// 验证延迟被记录(至少 10ms
if elapsed < 10*time.Millisecond {
t.Errorf("expected duration >= 10ms, got %v", elapsed)
}
al.Close()
}

View File

@ -10,6 +10,7 @@ import (
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/middleware"
"rua.plus/lolly/internal/middleware/accesslog"
"rua.plus/lolly/internal/proxy"
)
@ -19,7 +20,8 @@ type Server struct {
fastServer *fasthttp.Server
handler fasthttp.RequestHandler
running bool
healthCheckers []*proxy.HealthChecker // 新增
healthCheckers []*proxy.HealthChecker
accessLogMiddleware *accesslog.AccessLog
}
// New 创建服务器
@ -52,7 +54,10 @@ func (s *Server) startSingleMode() error {
router.GET("/{filepath:*}", staticHandler.Handle)
router.HEAD("/{filepath:*}", staticHandler.Handle)
chain := middleware.NewChain()
// 创建访问日志中间件
s.accessLogMiddleware = accesslog.New(&s.config.Logging)
chain := middleware.NewChain(s.accessLogMiddleware)
s.handler = chain.Apply(router.Handler())
s.fastServer = &fasthttp.Server{
@ -73,6 +78,10 @@ func (s *Server) startSingleMode() error {
func (s *Server) startVHostMode() error {
vhostMgr := NewVHostManager()
// 创建访问日志中间件(共享给所有虚拟主机)
s.accessLogMiddleware = accesslog.New(&s.config.Logging)
chain := middleware.NewChain(s.accessLogMiddleware)
for i := range s.config.Servers {
router := handler.NewRouter()
s.registerProxyRoutes(router, &s.config.Servers[i])
@ -85,7 +94,7 @@ func (s *Server) startVHostMode() error {
router.GET("/{filepath:*}", staticHandler.Handle)
router.HEAD("/{filepath:*}", staticHandler.Handle)
vhostMgr.AddHost(s.config.Servers[i].Name, router.Handler())
vhostMgr.AddHost(s.config.Servers[i].Name, chain.Apply(router.Handler()))
}
// 默认主机
@ -97,7 +106,7 @@ func (s *Server) startVHostMode() error {
s.config.Server.Static.Index,
)
router.GET("/{filepath:*}", staticHandler.Handle)
vhostMgr.SetDefault(router.Handler())
vhostMgr.SetDefault(chain.Apply(router.Handler()))
}
s.handler = vhostMgr.Handler()
@ -161,6 +170,11 @@ func (s *Server) Stop() error {
hc.Stop()
}
// 关闭访问日志
if s.accessLogMiddleware != nil {
s.accessLogMiddleware.Close()
}
if s.fastServer != nil {
return s.fastServer.Shutdown()
}
@ -176,6 +190,11 @@ func (s *Server) GracefulStop(timeout time.Duration) error {
hc.Stop()
}
// 关闭访问日志
if s.accessLogMiddleware != nil {
s.accessLogMiddleware.Close()
}
if s.fastServer != nil {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()