feat(config,logging): 增强日志配置,支持全局格式和 nginx combined 格式
- 新增全局日志格式配置 (text/json),控制启动/停止日志输出格式 - 访问日志默认格式改为近似 nginx combined 格式 - 支持 $remote_user 变量,BasicAuth 认证成功后存储用户名到上下文 - 扩展 config.example.yaml 虚拟主机配置说明 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ac9153f09d
commit
94a2ae7113
@ -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,级别越高日志越少)
|
||||
|
||||
@ -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 访问日志配置。
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -204,7 +204,10 @@ func (ba *BasicAuth) Process(next fasthttp.RequestHandler) fasthttp.RequestHandl
|
||||
return
|
||||
}
|
||||
|
||||
// 认证成功,继续执行下一个处理器
|
||||
// 认证成功,存储用户名到上下文(用于访问日志 $remote_user)
|
||||
ctx.SetUserValue("remote_user", username)
|
||||
|
||||
// 继续执行下一个处理器
|
||||
next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user