lolly/internal/logging/logging.go
xfy 9824ad5b57 perf(netutil): cache RemoteAddr string formatting
Add netutil.FormatRemoteAddr() to avoid repeated net.TCPAddr.String()
allocations on the hot logging path.

- IPv4 addresses use a zero-allocation fast path (custom uint8->ASCII).
- IPv6 falls back to addr.String() with a 1024-entry LRU cache.
- Update logging.LogAccess and variable $remote_addr/$remote_port
  getters to use the shared helper.

Eliminates net.JoinHostPort and net.IP.String from top allocators
when access logging is active.
2026-06-11 14:43:12 +08:00

380 lines
11 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 提供日志管理功能,支持访问日志和错误日志分离。
//
// 该文件包含日志相关的核心逻辑,包括:
// - 访问日志记录(请求方法、路径、状态码、耗时)
// - 错误日志记录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/netutil"
"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"
const formatText = "text"
// Init 初始化全局日志系统(兼容旧接口)。
//
// 该函数用于快速初始化全局 log 实例,支持 console、text 和 json 三种格式。
// 不指定输出路径时默认输出到标准输出。
//
// 参数:
// - level: 日志级别,支持 debug、info、warn、error不区分大小写
// - format: 日志格式,"console" 为带时间彩色、"text" 为纯文本、"json" 为结构化 JSON
func Init(level string, format string) {
l := parseLevel(level)
w := getOutput("") // stdout
switch format {
case "console":
log = zerolog.New(zerolog.ConsoleWriter{Out: w, TimeFormat: time.RFC3339}).Level(l).With().Timestamp().Logger()
case formatText:
// text 格式:使用 ConsoleWriter 但不带颜色
log = zerolog.New(zerolog.ConsoleWriter{Out: w, TimeFormat: time.RFC3339, NoColor: true}).Level(l).With().Timestamp().Logger()
default:
// json 或空格式:使用 JSON
log = zerolog.New(w).Level(l).With().Timestamp().Logger()
}
}
// New 创建日志管理器,支持访问/错误日志分离。
//
// 根据配置创建 Logger 实例,访问日志和错误日志可分别输出到不同路径。
// 配置为 nil 时使用默认设置(全部输出到标准输出)。
//
// 参数:
// - cfg: 日志配置,包含访问日志和错误日志的输出路径、级别、格式等
//
// 返回值:
// - *Logger: 初始化的日志管理器实例
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 记录访问日志(全局实例)。
//
// 使用全局 log 实例记录 HTTP 请求的基本信息,包括方法、路径、状态码、
// 响应大小、耗时和客户端地址。
//
// 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", netutil.FormatRemoteAddr(ctx)).
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 级别日志记录器(全局实例)。
//
// 用于记录错误信息,调用链式方法添加字段后需调用 Msg() 输出。
//
// 返回值:
// - *zerolog.Event: 错误级别日志事件,用于链式调用
func Error() *zerolog.Event {
return log.Error()
}
// Info 返回 Info 级别日志记录器(全局实例)。
//
// 用于记录一般信息日志,调用链式方法添加字段后需调用 Msg() 输出。
//
// 返回值:
// - *zerolog.Event: 信息级别日志事件,用于链式调用
func Info() *zerolog.Event {
return log.Info()
}
// Warn 返回 Warn 级别日志记录器(全局实例)。
//
// 用于记录警告信息,调用链式方法添加字段后需调用 Msg() 输出。
//
// 返回值:
// - *zerolog.Event: 警告级别日志事件,用于链式调用
func Warn() *zerolog.Event {
return log.Warn()
}
// Debug 返回 Debug 级别日志记录器(全局实例)。
//
// 用于记录调试信息,调用链式方法添加字段后需调用 Msg() 输出。
// 仅在日志级别设置为 debug 时才会实际输出。
//
// 返回值:
// - *zerolog.Event: 调试级别日志事件,用于链式调用
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 创建应用日志管理器。
//
// 根据配置创建 AppLogger 实例,用于统一管理应用生命周期日志
// (启动、停止、信号处理等)。默认使用 text 格式输出到错误日志路径。
//
// 参数:
// - cfg: 日志配置,包含输出路径、级别、格式等设置
//
// 返回值:
// - *AppLogger: 初始化的应用日志记录器实例
func NewAppLogger(cfg *config.LoggingConfig) *AppLogger {
if cfg == nil {
cfg = &config.LoggingConfig{}
}
format := cfg.Format
if format == "" {
format = "text" // 默认纯文本
}
writer := getOutput(cfg.Error.Path)
var errorLog zerolog.Logger
if format == "text" {
// text 格式:使用 ConsoleWriter无颜色
errorLog = zerolog.New(zerolog.ConsoleWriter{Out: writer, TimeFormat: time.RFC3339, NoColor: true}).Level(parseLevel(cfg.Error.Level)).With().Timestamp().Logger()
} else {
// json 格式
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) {
event := l.errorLog.Info()
for k, v := range fields {
event.Str(k, v)
}
event.Msg(msg)
}
// LogShutdown 记录停止消息。
func (l *AppLogger) LogShutdown(msg string) {
l.errorLog.Info().Msg(msg)
}
// LogSignal 记录信号处理消息。
func (l *AppLogger) LogSignal(sig string, action string) {
l.errorLog.Info().Str("signal", sig).Str("action", action).Msg("收到信号")
}
// Info 返回 Info 级别日志记录器。
func (l *AppLogger) Info() *zerolog.Event {
return l.errorLog.Info()
}
// Error 返回 Error 级别日志记录器。
func (l *AppLogger) Error() *zerolog.Event {
return l.errorLog.Error()
}