lolly/internal/logging/logging.go
xfy 575de90398 feat(logging): 增强日志模块,支持访问/错误日志分离
- 新增 Logger 结构体,分离访问日志和错误日志
- 支持 stdout/stderr/文件输出目标
- 实现 nginx 格式变量日志记录
- 添加日志文件关闭方法
- 修复 parseLevel 大小写处理
- 新增完整单元测试覆盖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 10:11:23 +08:00

140 lines
3.4 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 logging
import (
"io"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// Logger 日志管理器,分离访问日志和错误日志。
type Logger struct {
accessLog zerolog.Logger
errorLog zerolog.Logger
accessFile *os.File
errorFile *os.File
}
var log zerolog.Logger
// 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{}
}
logger := &Logger{
accessLog: zerolog.New(getOutput(cfg.Access.Path)).With().Timestamp().Logger(),
errorLog: zerolog.New(getOutput(cfg.Error.Path)).Level(parseLevel(cfg.Error.Level)).With().Timestamp().Logger(),
}
return logger
}
// getOutput 获取输出目标stdout/stderr/文件)。
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, 0644)
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")
}
// LogAccessWithLogger 使用 Logger 实例记录访问日志nginx 格式变量)。
func (l *Logger) LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, duration time.Duration) {
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("")
}
// 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 {
if l.accessFile != nil {
l.accessFile.Close()
}
if l.errorFile != nil {
l.errorFile.Close()
}
return nil
}
// Error 返回 Error 级别日志记录器(全局实例)。
func Error() *zerolog.Event {
return log.Error()
}
// parseLevel 解析日志级别。
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
}
}