// 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(). Bytes("method", ctx.Method()). Bytes("path", 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()). Bytes("request", append(append(ctx.Method(), ' '), 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() }