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
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
// 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()
|
||
}
|