主要修复: - errcheck: defer Close 使用 //nolint:errcheck,类型断言改为 ok 检查 - govet fieldalignment: 调整结构体字段顺序优化内存布局 - revive unused-parameter: 将未使用参数改为 _ - exhaustive: 添加缺失的 switch case 或 default - goconst: 提取重复字符串为常量 (accessAllow, accessDeny 等) - staticcheck SA9003: 修复空分支逻辑 - gofmt: 运行 gofmt -w 格式化 - nolintlint: 修复 nolint 注释格式 其他改进: - 更新 .golangci.yml 配置,启用更严格的检查 - 移除未使用的代码和导入 - 简化测试辅助函数调用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
352 lines
9.3 KiB
Go
352 lines
9.3 KiB
Go
// Package logging 提供日志管理功能,支持访问日志和错误日志分离。
|
||
//
|
||
// 该文件包含日志相关的核心逻辑,包括:
|
||
// - 访问日志记录(请求方法、路径、状态码、耗时)
|
||
// - 错误日志记录(Debug、Info、Warn、Error 级别)
|
||
// - 日志格式配置(text 或 json)
|
||
// - 应用生命周期日志(启动、停止、信号)
|
||
//
|
||
// 主要用途:
|
||
//
|
||
// 用于记录服务器运行时的各类日志信息,便于监控和排查问题。
|
||
//
|
||
// 注意事项:
|
||
// - 支持 zerolog 高性能日志库
|
||
// - 访问日志和错误日志可分离输出到不同文件
|
||
//
|
||
// 作者:xfy
|
||
package logging
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/rs/zerolog"
|
||
"github.com/valyala/fasthttp"
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/variable"
|
||
)
|
||
|
||
// Logger 日志管理器,分离访问日志和错误日志。
|
||
type Logger struct {
|
||
accessLog zerolog.Logger
|
||
errorLog zerolog.Logger
|
||
accessWriter io.Writer
|
||
accessFile *os.File
|
||
errorFile *os.File
|
||
accessFormat string
|
||
}
|
||
|
||
// AppLogger 应用日志管理器,统一管理启动/停止日志。
|
||
type AppLogger struct {
|
||
errorLog zerolog.Logger
|
||
writer io.Writer
|
||
format string
|
||
}
|
||
|
||
var log zerolog.Logger
|
||
|
||
const formatJSON = "json"
|
||
|
||
// Init 初始化日志系统(兼容旧接口)。
|
||
func Init(level string, pretty bool) {
|
||
l := parseLevel(level)
|
||
if pretty {
|
||
log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).Level(l).With().Timestamp().Logger()
|
||
} else {
|
||
log = zerolog.New(os.Stdout).Level(l).With().Timestamp().Logger()
|
||
}
|
||
}
|
||
|
||
// New 创建日志管理器,支持访问/错误日志分离。
|
||
func New(cfg *config.LoggingConfig) *Logger {
|
||
if cfg == nil {
|
||
cfg = &config.LoggingConfig{}
|
||
}
|
||
|
||
accessWriter := getOutput(cfg.Access.Path)
|
||
|
||
logger := &Logger{
|
||
accessFormat: cfg.Access.Format,
|
||
accessWriter: accessWriter,
|
||
accessLog: zerolog.New(accessWriter).With().Timestamp().Logger(),
|
||
errorLog: zerolog.New(getOutput(cfg.Error.Path)).Level(parseLevel(cfg.Error.Level)).With().Timestamp().Logger(),
|
||
}
|
||
|
||
return logger
|
||
}
|
||
|
||
// getOutput 获取输出目标(stdout/stderr/文件)。
|
||
//
|
||
// 根据路径返回对应的输出 writer:
|
||
// - "stdout" 或空字符串:返回 os.Stdout
|
||
// - "stderr":返回 os.Stderr
|
||
// - 其他:尝试打开文件,失败返回 os.Stdout
|
||
//
|
||
// 参数:
|
||
// - path: 输出路径
|
||
//
|
||
// 返回值:
|
||
// - io.Writer: 输出 writer
|
||
func getOutput(path string) io.Writer {
|
||
path = strings.TrimSpace(path)
|
||
if path == "" || path == "stdout" {
|
||
return os.Stdout
|
||
}
|
||
if path == "stderr" {
|
||
return os.Stderr
|
||
}
|
||
|
||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||
if err != nil {
|
||
return os.Stdout
|
||
}
|
||
return f
|
||
}
|
||
|
||
// LogAccess 记录访问日志。
|
||
func LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, duration time.Duration) {
|
||
log.Info().
|
||
Str("method", string(ctx.Method())).
|
||
Str("path", string(ctx.Path())).
|
||
Int("status", status).
|
||
Int64("size", size).
|
||
Dur("duration", duration).
|
||
Str("remote_addr", ctx.RemoteAddr().String()).
|
||
Msg("request")
|
||
}
|
||
|
||
// LogAccess 记录访问日志,支持模板格式或 JSON。
|
||
func (l *Logger) LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, duration time.Duration) {
|
||
// JSON 格式或空格式:输出结构化 JSON
|
||
if l.accessFormat == formatJSON || l.accessFormat == "" {
|
||
l.accessLog.Info().
|
||
Str("remote_addr", ctx.RemoteAddr().String()).
|
||
Str("request", string(ctx.Method())+" "+string(ctx.Path())).
|
||
Int("status", status).
|
||
Int64("body_bytes_sent", size).
|
||
Dur("request_time", duration).
|
||
Str("http_referrer", string(ctx.Request.Header.Peek("Referer"))).
|
||
Str("http_user_agent", string(ctx.Request.Header.Peek("User-Agent"))).
|
||
Msg("")
|
||
return
|
||
}
|
||
|
||
// 模板格式:直接输出纯文本
|
||
output := l.formatAccessLog(ctx, status, size, duration)
|
||
_, _ = fmt.Fprintln(l.accessWriter, output)
|
||
}
|
||
|
||
// formatAccessLog 根据模板格式化访问日志。
|
||
//
|
||
// 使用变量系统展开模板字符串,支持以下变量:
|
||
// - $remote_addr: 客户端地址
|
||
// - $remote_user: 认证用户
|
||
// - $request: 请求方法和路径
|
||
// - $status: HTTP 状态码
|
||
// - $body_bytes_sent: 响应体大小
|
||
// - $request_time: 请求处理时间
|
||
// - $http_referer: Referer 头
|
||
// - $http_user_agent: User-Agent 头
|
||
// - $time_local, $time_iso8601: 时间
|
||
// - $host, $uri, $args: 请求信息
|
||
//
|
||
// 参数:
|
||
// - ctx: FastHTTP 请求上下文
|
||
// - status: HTTP 状态码
|
||
// - size: 响应体大小
|
||
// - duration: 请求处理时间
|
||
//
|
||
// 返回值:
|
||
// - string: 格式化后的日志字符串
|
||
func (l *Logger) formatAccessLog(ctx *fasthttp.RequestCtx, status int, size int64, duration time.Duration) string {
|
||
// 获取认证用户名,无认证时为 "-"
|
||
remoteUser := "-"
|
||
if user := ctx.UserValue("remote_user"); user != nil {
|
||
if username, ok := user.(string); ok && username != "" {
|
||
remoteUser = username
|
||
}
|
||
}
|
||
|
||
// 创建变量上下文
|
||
vc := variable.NewContext(ctx)
|
||
defer variable.ReleaseContext(vc)
|
||
|
||
// 设置响应信息(同时设置到 ctx 供 builtin getter 使用)
|
||
vc.SetResponseInfo(status, size, duration.Nanoseconds())
|
||
variable.SetResponseInfoInContext(ctx, status, size, duration.Nanoseconds())
|
||
|
||
// 设置自定义变量(用于兼容旧的变量名)
|
||
vc.Set("remote_user", remoteUser)
|
||
vc.Set("request", string(ctx.Method())+" "+string(ctx.Path())+" "+string(ctx.Request.Header.Protocol()))
|
||
vc.Set("http_referer", string(ctx.Request.Header.Peek("Referer")))
|
||
vc.Set("http_user_agent", string(ctx.Request.Header.Peek("User-Agent")))
|
||
// 添加 $time 别名(兼容旧格式)
|
||
vc.Set("time", time.Now().Format(time.RFC3339))
|
||
|
||
// 展开模板
|
||
return vc.Expand(l.accessFormat)
|
||
}
|
||
|
||
// Debug 返回 Debug 级别日志记录器。
|
||
func (l *Logger) Debug() *zerolog.Event {
|
||
return l.errorLog.Debug()
|
||
}
|
||
|
||
// Info 返回 Info 级别日志记录器。
|
||
func (l *Logger) Info() *zerolog.Event {
|
||
return l.errorLog.Info()
|
||
}
|
||
|
||
// Warn 返回 Warn 级别日志记录器。
|
||
func (l *Logger) Warn() *zerolog.Event {
|
||
return l.errorLog.Warn()
|
||
}
|
||
|
||
// Error 返回 Error 级别日志记录器。
|
||
func (l *Logger) Error() *zerolog.Event {
|
||
return l.errorLog.Error()
|
||
}
|
||
|
||
// Close 关闭日志文件。
|
||
func (l *Logger) Close() error {
|
||
var err error
|
||
if l.accessFile != nil {
|
||
err = l.accessFile.Close()
|
||
}
|
||
if l.errorFile != nil {
|
||
if closeErr := l.errorFile.Close(); closeErr != nil && err == nil {
|
||
err = closeErr
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
// Error 返回 Error 级别日志记录器(全局实例)。
|
||
func Error() *zerolog.Event {
|
||
return log.Error()
|
||
}
|
||
|
||
// Info 返回 Info 级别日志记录器(全局实例)。
|
||
func Info() *zerolog.Event {
|
||
return log.Info()
|
||
}
|
||
|
||
// Warn 返回 Warn 级别日志记录器(全局实例)。
|
||
func Warn() *zerolog.Event {
|
||
return log.Warn()
|
||
}
|
||
|
||
// Debug 返回 Debug 级别日志记录器(全局实例)。
|
||
func Debug() *zerolog.Event {
|
||
return log.Debug()
|
||
}
|
||
|
||
// parseLevel 解析日志级别。
|
||
//
|
||
// 将字符串级别转换为 zerolog.Level。
|
||
// 支持的级别:debug, info, warn, error(不区分大小写)。
|
||
// 未知级别默认返回 info。
|
||
//
|
||
// 参数:
|
||
// - level: 日志级别字符串
|
||
//
|
||
// 返回值:
|
||
// - zerolog.Level: 解析后的日志级别
|
||
func parseLevel(level string) zerolog.Level {
|
||
switch strings.ToLower(level) {
|
||
case "debug":
|
||
return zerolog.DebugLevel
|
||
case "info":
|
||
return zerolog.InfoLevel
|
||
case "warn":
|
||
return zerolog.WarnLevel
|
||
case "error":
|
||
return zerolog.ErrorLevel
|
||
default:
|
||
return zerolog.InfoLevel
|
||
}
|
||
}
|
||
|
||
// NewAppLogger 创建应用日志管理器。
|
||
func NewAppLogger(cfg *config.LoggingConfig) *AppLogger {
|
||
if cfg == nil {
|
||
cfg = &config.LoggingConfig{}
|
||
}
|
||
|
||
format := cfg.Format
|
||
if format == "" {
|
||
format = "text" // 默认纯文本
|
||
}
|
||
|
||
writer := getOutput(cfg.Error.Path)
|
||
errorLog := zerolog.New(writer).Level(parseLevel(cfg.Error.Level)).With().Timestamp().Logger()
|
||
|
||
return &AppLogger{
|
||
format: format,
|
||
errorLog: errorLog,
|
||
writer: writer,
|
||
}
|
||
}
|
||
|
||
// LogStartup 记录启动消息。
|
||
func (l *AppLogger) LogStartup(msg string, fields map[string]string) {
|
||
if l.format == formatJSON {
|
||
event := l.errorLog.Info()
|
||
for k, v := range fields {
|
||
event.Str(k, v)
|
||
}
|
||
event.Msg(msg)
|
||
return
|
||
}
|
||
|
||
// 纯文本格式
|
||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||
if len(fields) == 0 {
|
||
fmt.Fprintf(l.writer, "[%s] INFO %s\n", timestamp, msg)
|
||
return
|
||
}
|
||
|
||
// 带字段的文本格式
|
||
extra := ""
|
||
for k, v := range fields {
|
||
extra += fmt.Sprintf(" %s=%s", k, v)
|
||
}
|
||
fmt.Fprintf(l.writer, "[%s] INFO %s%s\n", timestamp, msg, extra)
|
||
}
|
||
|
||
// LogShutdown 记录停止消息。
|
||
func (l *AppLogger) LogShutdown(msg string) {
|
||
if l.format == formatJSON {
|
||
l.errorLog.Info().Msg(msg)
|
||
return
|
||
}
|
||
|
||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||
fmt.Fprintf(l.writer, "[%s] INFO %s\n", timestamp, msg)
|
||
}
|
||
|
||
// LogSignal 记录信号处理消息。
|
||
func (l *AppLogger) LogSignal(sig string, action string) {
|
||
if l.format == formatJSON {
|
||
l.errorLog.Info().Str("signal", sig).Str("action", action).Msg("")
|
||
return
|
||
}
|
||
|
||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||
fmt.Fprintf(l.writer, "[%s] INFO 收到 %s,%s\n", timestamp, sig, action)
|
||
}
|
||
|
||
// Info 返回 Info 级别日志记录器。
|
||
func (l *AppLogger) Info() *zerolog.Event {
|
||
return l.errorLog.Info()
|
||
}
|
||
|
||
// Error 返回 Error 级别日志记录器。
|
||
func (l *AppLogger) Error() *zerolog.Event {
|
||
return l.errorLog.Error()
|
||
}
|