xfy 047e033af5 feat(accesslog): add deterministic sampling with sample_rate config
Add logging.access.sample_rate config (0.0-1.0) for deterministic
request sampling. 5xx errors are always logged; 2xx/3xx/4xx follow
the configured rate. Uses atomic.Uint64 counter for lock-free,
zero-allocation sampling decisions.

Includes test updates to verify:
- sample_rate=1.0 logs all requests
- sample_rate=0.0 logs only 5xx
- 5xx are always logged regardless of rate
2026-06-11 14:42:55 +08:00

134 lines
3.5 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
// sampleRate=0 明确表示禁用访问日志
// sampleRate<0 或 >1 修正为 1.0(全量记录)
if sampleRate < 0.0 || sampleRate > 1.0 {
sampleRate = 1.0
}
var sampleInterval uint64 = 1
if sampleRate > 0.0 && 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 判断当前请求是否应记录访问日志。
//
// 规则:
// - 5xx 服务器错误始终记录(便于排查错误)
// - sampleRate=0 时不记录 2xx/3xx/4xx
// - 采样率为 1.0 时始终记录
// - 其他情况按 sampleRate 采样
//
// 使用原子计数器实现无锁、零分配采样。
func (a *AccessLog) shouldLog(status int) bool {
// 5xx 服务器错误始终记录
if status >= 500 {
return true
}
if a.sampleRate == 0.0 {
return false
}
if a.sampleRate >= 1.0 {
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()
}