xfy 445401c40f perf(accesslog): add sample_rate for access log to reduce CPU and allocations
Add configurable access log sampling via :
- 0.0-1.0 range; defaults to 1.0 (record all) for backward compatibility
- Uses lock-free atomic counter for deterministic sampling
- Non-2xx responses always logged regardless of sample rate

Benchmark results (combined format, /dev/null):
  Full logging:    ~2245 ns/op, 1987 B/op, 17 allocs/op
  10% sampling:    ~1593 ns/op, 1633 B/op,  6 allocs/op
  Improvement:     -29% latency, -65% allocations/op

This addresses the top application-layer CPU hotspot identified
in the v0.4.0 profile (LogAccess at 16.36% cumulative CPU).
2026-06-11 13:53:41 +08:00

128 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package accesslog 提供访问日志中间件,记录每个请求的详细信息。
//
// 该文件包含访问日志相关的核心逻辑,包括:
// - 请求方法和路径记录
// - 响应状态码和大小记录
// - 请求处理耗时记录
// - 访问日志采样(按 sample_rate 比例记录成功请求)
//
// 使用示例:
//
// accessLog := accesslog.New(cfg.Logging)
// chain := middleware.NewChain(accessLog)
//
// 作者xfy
package accesslog
import (
"sync/atomic"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
)
// AccessLog 访问日志中间件,记录请求方法、路径、状态码、响应大小和处理时间。
type AccessLog struct {
// logger 日志记录器实例,用于输出访问日志
logger *logging.Logger
// sampleRate 采样率,范围 0.0-1.0
// 1.0 表示记录所有请求
sampleRate float64
// sampleCounter 原子计数器,用于确定性采样
// 每请求原子自增,与 1/sampleRate 取模决定是否记录
sampleCounter atomic.Uint64
// sampleInterval 采样间隔,由 sampleRate 计算得出
// 例如 sampleRate=0.1 时 interval=10每 10 个请求记录 1 个
sampleInterval uint64
}
// New 创建访问日志中间件。
//
// 参数:
// - cfg: 日志配置,包含输出路径、格式等设置
//
// 返回值:
// - *AccessLog: 访问日志中间件实例
func New(cfg *config.LoggingConfig) *AccessLog {
sampleRate := cfg.Access.SampleRate
if sampleRate <= 0.0 || sampleRate > 1.0 {
sampleRate = 1.0
}
var sampleInterval uint64 = 1
if sampleRate < 1.0 {
// 使用 1000 作为基数以提高精度,例如 0.123 -> 间隔约 8
sampleInterval = uint64((1.0 / sampleRate) + 0.5)
if sampleInterval < 1 {
sampleInterval = 1
}
}
return &AccessLog{
logger: logging.New(cfg),
sampleRate: sampleRate,
sampleInterval: sampleInterval,
}
}
// Name 返回中间件名称。
//
// 返回值:
// - string: 中间件名称 "accesslog"
func (a *AccessLog) Name() string {
return "accesslog"
}
// shouldLog 判断当前请求是否应记录访问日志。
//
// 规则:
// - 采样率为 1.0 时始终记录
// - 非 2xx 响应始终记录(便于排查错误)
// - 2xx 响应按采样率决定是否记录
//
// 使用原子计数器实现无锁、零分配采样。
func (a *AccessLog) shouldLog(status int) bool {
if a.sampleRate >= 1.0 {
return true
}
// 非成功响应始终记录
if status < 200 || status >= 300 {
return true
}
// 确定性采样:每 sampleInterval 个请求记录一个
return a.sampleCounter.Add(1)%a.sampleInterval == 1
}
// Process 包装 handler在请求处理后记录访问日志。
//
// 参数:
// - next: 下一个请求处理器
//
// 返回值:
// - fasthttp.RequestHandler: 包装后的请求处理器
func (a *AccessLog) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
start := time.Now()
next(ctx)
status := ctx.Response.StatusCode()
if !a.shouldLog(status) {
return
}
duration := time.Since(start)
a.logger.LogAccess(ctx, status, int64(len(ctx.Response.Body())), duration)
}
}
// Close 关闭日志文件。
//
// 返回值:
// - error: 关闭失败时返回错误,成功返回 nil
func (a *AccessLog) Close() error {
return a.logger.Close()
}