diff --git a/config.example.yaml b/config.example.yaml index 004e76c..3378c48 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -109,17 +109,54 @@ server: - "application/json" - "application/javascript" -# 多虚拟主机模式(可选) +# 多虚拟主机模式(可选,每个虚拟主机支持完整的 server 配置) # servers: -# - listen: ":8080" -# name: "api.example.com" -# proxy: +# - listen: ":8080" # 监听地址 +# name: "api.example.com" # 服务器名称(用于虚拟主机匹配) +# read_timeout: 30s # 读取超时(0 表示不限制) +# write_timeout: 30s # 写入超时(0 表示不限制) +# idle_timeout: 120s # 空闲超时(0 表示不限制) +# max_conns_per_ip: 1000 # 每 IP 最大连接数(0 表示不限制) +# max_requests_per_conn: 10000 # 每连接最大请求数(0 表示不限制) +# static: # 静态文件配置 +# root: /var/www/api +# index: [index.html] +# proxy: # 反向代理配置 # - path: /api -# targets: [http://backend:8080] -# - listen: ":8443" +# targets: +# - url: http://backend:8080 +# load_balance: round_robin +# ssl: # SSL/TLS 配置 +# cert: /path/to/api.cert.pem +# key: /path/to/api.key.pem +# protocols: [TLSv1.2, TLSv1.3] +# hsts: +# max_age: 31536000 +# include_sub_domains: true +# security: # 安全配置 +# access: +# default: allow +# rate_limit: +# request_rate: 100 +# headers: +# x_frame_options: DENY +# compression: # 响应压缩配置 +# type: gzip +# level: 6 +# - listen: ":8443" # 另一个虚拟主机 # name: "static.example.com" # static: # root: /var/www/static +# index: [index.html, index.htm] +# ssl: +# cert: /path/to/static.cert.pem +# key: /path/to/static.key.pem +# compression: +# type: gzip + +# SSL/TLS 默认配置说明(未配置证书时不启用) +# 默认 TLS 协议: TLSv1.2, TLSv1.3(不支持 TLSv1.0/1.1) +# 默认 HSTS 配置: max_age=31536000(1年), include_sub_domains=true # TCP/UDP Stream 代理配置(可选) # stream: @@ -135,9 +172,12 @@ server: # 日志配置 logging: + format: "text" # 全局日志格式(有效值: text, json),控制启动/停止日志格式 access: path: "" # 日志文件路径(空表示输出到 stdout) - format: "$remote_addr - $request - $status - $body_bytes_sent" # 日志格式(支持变量: $remote_addr, $request, $status, $body_bytes_sent, $request_time, $http_referer, $http_user_agent) + format: "$remote_addr - $remote_user [$time] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"" # 访问日志格式,近似 nginx combined + # 支持变量: $remote_addr, $remote_user, $request, $status, $body_bytes_sent, $request_time, $http_referer, $http_user_agent, $time + # 特殊值 "json" 输出结构化 JSON error: path: "" # 日志文件路径(空表示输出到 stderr) level: "info" # 日志级别(有效值: debug, info, warn, error,级别越高日志越少) diff --git a/internal/config/config.go b/internal/config/config.go index 7d003b7..f5d164c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -189,8 +189,9 @@ type CompressionConfig struct { // LoggingConfig 日志配置。 type LoggingConfig struct { - Access AccessLogConfig `yaml:"access"` // 访问日志 - Error ErrorLogConfig `yaml:"error"` // 错误日志 + Format string `yaml:"format"` // 全局格式:text(默认)或 json,控制启动/停止日志 + Access AccessLogConfig `yaml:"access"` // 访问日志 + Error ErrorLogConfig `yaml:"error"` // 错误日志 } // AccessLogConfig 访问日志配置。 diff --git a/internal/config/defaults.go b/internal/config/defaults.go index d798a60..e35a00e 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -96,8 +96,12 @@ func DefaultConfig() *Config { }, }, Logging: LoggingConfig{ + Format: "text", Access: AccessLogConfig{ - Format: "$remote_addr - $request - $status - $body_bytes_sent", + // 近似 nginx combined 格式 + // nginx: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" + // lolly: $request 不含 HTTP 版本,$time 为 RFC3339 格式 + Format: "$remote_addr - $remote_user [$time] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\"", }, Error: ErrorLogConfig{ Level: "info", @@ -348,9 +352,12 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { // logging 配置 buf.WriteString("# 日志配置\n") buf.WriteString("logging:\n") + buf.WriteString(fmt.Sprintf(" format: \"%s\" # 全局日志格式(有效值: text, json),控制启动/停止日志格式\n", cfg.Logging.Format)) buf.WriteString(" access:\n") buf.WriteString(" path: \"\" # 日志文件路径(空表示输出到 stdout)\n") - buf.WriteString(fmt.Sprintf(" format: \"%s\" # 日志格式(支持变量: $remote_addr, $request, $status, $body_bytes_sent, $request_time, $http_referer, $http_user_agent)\n", cfg.Logging.Access.Format)) + buf.WriteString(fmt.Sprintf(" format: \"%s\" # 访问日志格式,近似 nginx combined\n", cfg.Logging.Access.Format)) + buf.WriteString(" # 支持变量: $remote_addr, $remote_user, $request, $status, $body_bytes_sent, $request_time, $http_referer, $http_user_agent, $time\n") + buf.WriteString(" # 特殊值 \"json\" 输出结构化 JSON\n") buf.WriteString(" error:\n") buf.WriteString(" path: \"\" # 日志文件路径(空表示输出到 stderr)\n") buf.WriteString(fmt.Sprintf(" level: \"%s\" # 日志级别(有效值: debug, info, warn, error,级别越高日志越少)\n", cfg.Logging.Error.Level)) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 65ac35a..8a91889 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -1,8 +1,10 @@ package logging import ( + "fmt" "io" "os" + "strconv" "strings" "time" @@ -13,10 +15,19 @@ import ( // Logger 日志管理器,分离访问日志和错误日志。 type Logger struct { - accessLog zerolog.Logger - errorLog zerolog.Logger - accessFile *os.File - errorFile *os.File + accessLog zerolog.Logger + errorLog zerolog.Logger + accessFormat string // 访问日志格式模板 + accessWriter io.Writer // 访问日志输出目标 + accessFile *os.File + errorFile *os.File +} + +// AppLogger 应用日志管理器,统一管理启动/停止日志。 +type AppLogger struct { + format string // "text" 或 "json" + errorLog zerolog.Logger + writer io.Writer } var log zerolog.Logger @@ -37,9 +48,13 @@ func New(cfg *config.LoggingConfig) *Logger { cfg = &config.LoggingConfig{} } + accessWriter := getOutput(cfg.Access.Path) + 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(), + 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 @@ -74,17 +89,54 @@ func LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, duration time.D Msg("request") } -// LogAccessWithLogger 使用 Logger 实例记录访问日志(nginx 格式变量)。 +// LogAccess 记录访问日志,支持模板格式或 JSON。 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("") + // JSON 格式或空格式:输出结构化 JSON + if l.accessFormat == "json" || 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 根据模板格式化访问日志。 +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 + } + } + + replacements := map[string]string{ + "$remote_addr": ctx.RemoteAddr().String(), + "$remote_user": remoteUser, + "$request": string(ctx.Method()) + " " + string(ctx.Path()) + " " + string(ctx.Request.Header.Protocol()), + "$status": strconv.Itoa(status), + "$body_bytes_sent": strconv.FormatInt(size, 10), + "$request_time": fmt.Sprintf("%.6f", duration.Seconds()), + "$http_referer": string(ctx.Request.Header.Peek("Referer")), + "$http_user_agent": string(ctx.Request.Header.Peek("User-Agent")), + "$time": time.Now().Format(time.RFC3339), + } + + result := l.accessFormat + for varName, value := range replacements { + result = strings.ReplaceAll(result, varName, value) + } + return result } // Debug 返回 Debug 级别日志记录器。 @@ -138,3 +190,82 @@ func parseLevel(level string) zerolog.Level { 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 == "json" { + 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 == "json" { + 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 == "json" { + 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() +} diff --git a/internal/middleware/security/auth.go b/internal/middleware/security/auth.go index 4012f40..e4bfb31 100644 --- a/internal/middleware/security/auth.go +++ b/internal/middleware/security/auth.go @@ -204,7 +204,10 @@ func (ba *BasicAuth) Process(next fasthttp.RequestHandler) fasthttp.RequestHandl return } - // 认证成功,继续执行下一个处理器 + // 认证成功,存储用户名到上下文(用于访问日志 $remote_user) + ctx.SetUserValue("remote_user", username) + + // 继续执行下一个处理器 next(ctx) } }