From de8fc13f181caecc6bf1ec5f6413bd4e4308ff88 Mon Sep 17 00:00:00 2001 From: Jimmy Xue Date: Sun, 30 Nov 2025 21:01:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=96=B9=E6=B3=95=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=BC=82=E6=AD=A5=E4=B8=8E=E5=90=8C=E6=AD=A5=E7=9A=84?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 9 ++ config/example.json | 4 +- docs/logger.md | 75 +++++++++++++ docs/storage.md | 9 +- examples/logger_example.go | 14 ++- factory/factory.go | 8 ++ logger/logger.go | 218 +++++++++++++++++++++++++++++++------ storage/handler.go | 18 +-- 8 files changed, 310 insertions(+), 45 deletions(-) diff --git a/config/config.go b/config/config.go index c22db8d..ebe17c1 100644 --- a/config/config.go +++ b/config/config.go @@ -235,6 +235,15 @@ type LoggerConfig struct { // DisableTimestamp 禁用时间戳 DisableTimestamp bool `json:"disableTimestamp"` + + // Async 是否使用异步模式(默认false,即同步模式) + // 异步模式:日志写入通过channel异步处理,不阻塞调用方 + // 同步模式:日志直接写入,会阻塞调用方直到写入完成 + Async bool `json:"async"` + + // BufferSize 异步模式下的缓冲区大小(默认1000) + // 当缓冲区满时,新的日志会阻塞直到有空间 + BufferSize int `json:"bufferSize"` } // LoadFromFile 从文件加载配置 diff --git a/config/example.json b/config/example.json index 5c6b021..d63a22f 100644 --- a/config/example.json +++ b/config/example.json @@ -75,7 +75,9 @@ "output": "stdout", "filePath": "", "prefix": "app", - "disableTimestamp": false + "disableTimestamp": false, + "async": false, + "bufferSize": 1000 } } diff --git a/docs/logger.md b/docs/logger.md index 5b77f2c..48f52a0 100644 --- a/docs/logger.md +++ b/docs/logger.md @@ -12,6 +12,7 @@ - 支持日志前缀 - 支持禁用时间戳 - 支持带字段的日志记录 +- **支持异步/同步日志模式(默认同步)** - 使用配置工具统一管理配置 ## 使用方法 @@ -92,6 +93,40 @@ logger.Infof(fields, "User logged in") logger.Errorf(fields, "Failed to process request") ``` +### 5. 异步/同步模式 + +#### 同步模式(默认) + +```go +// 配置中不设置async或设置为false,使用同步模式 +// 同步模式:日志直接写入,会阻塞调用方直到写入完成 +logger.Info("This is a synchronous log") +``` + +#### 异步模式 + +```go +// 配置中设置async为true,使用异步模式 +// 异步模式:日志写入通过channel异步处理,不阻塞调用方 +// 配置文件示例: +// { +// "logger": { +// "async": true, +// "bufferSize": 1000 +// } +// } + +// 使用异步模式时,程序退出前需要调用Close()确保所有日志写入完成 +defer logger.Close() + +logger.Info("This is an asynchronous log") +``` + +**注意:** +- `Fatal` 和 `Panic` 方法始终使用同步模式,确保日志写入后再退出/panic +- 异步模式下,程序退出前应调用 `Close()` 方法,确保所有日志写入完成 +- 如果channel已满,会自动降级为同步写入,避免丢失日志 + ## API 参考 ### NewLogger(cfg *config.LoggerConfig) (*Logger, error) @@ -143,6 +178,15 @@ logger.Errorf(fields, "Failed to process request") 记录错误日志(带字段)。 +### (l *Logger) Close() error + +优雅关闭logger(仅异步模式需要)。 + +**说明:** +- 等待所有日志写入完成后再返回 +- 同步模式下调用此方法会立即返回,无需等待 +- 程序退出前应调用此方法,确保所有日志写入完成 + ## 配置说明 日志配置通过 `config.LoggerConfig` 提供: @@ -154,6 +198,8 @@ logger.Errorf(fields, "Failed to process request") | FilePath | string | 日志文件路径(当output为file或both时必需) | - | | Prefix | string | 日志前缀 | - | | DisableTimestamp | bool | 禁用时间戳 | false | +| Async | bool | 是否使用异步模式 | false(同步) | +| BufferSize | int | 异步模式下的缓冲区大小 | 1000 | ## 配置示例 @@ -196,6 +242,25 @@ logger.Errorf(fields, "Failed to process request") } ``` +### 异步模式配置 + +```json +{ + "logger": { + "level": "info", + "output": "file", + "filePath": "./logs/app.log", + "prefix": "app", + "async": true, + "bufferSize": 1000 + } +} +``` + +**说明:** +- `async`: 设置为 `true` 启用异步模式,`false` 或不设置则使用同步模式(默认) +- `bufferSize`: 异步模式下的channel缓冲区大小,默认1000。当缓冲区满时,新的日志会阻塞直到有空间,或降级为同步写入 + ## 日志级别说明 - **debug**: 调试信息,最详细的日志级别 @@ -222,6 +287,13 @@ logger.Errorf(fields, "Failed to process request") 4. **性能考虑**: - 使用标准库log包,性能较好 - 文件输出使用追加模式,不会覆盖已有日志 + - 异步模式适合高并发场景,减少日志写入对业务代码的阻塞 + - 同步模式适合需要确保日志立即写入的场景(如调试) + +5. **异步模式注意事项**: + - 异步模式下,程序退出前必须调用 `Close()` 方法,确保所有日志写入完成 + - 如果channel缓冲区已满,会自动降级为同步写入,避免丢失日志 + - `Fatal` 和 `Panic` 方法始终使用同步模式,确保日志写入后再退出/panic ## 完整示例 @@ -257,6 +329,9 @@ func main() { }, "User logged in successfully") logger.Error("An error occurred: %v", err) + + // 如果使用异步模式,程序退出前需要关闭logger + // defer logger.Close() } ``` diff --git a/docs/storage.md b/docs/storage.md index 0075c63..c8fa664 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -265,7 +265,14 @@ type Storage interface { #### 响应 -直接返回文件内容,设置适当的Content-Type。 +- **成功**:直接返回文件内容(二进制),设置适当的Content-Type +- **错误**:返回标准HTTP错误状态码和错误消息(文本格式) + - `400 Bad Request`: 缺少必需参数 + - `404 Not Found`: 文件不存在 + - `405 Method Not Allowed`: 请求方法不正确 + - `500 Internal Server Error`: 系统错误 + +**注意**:`ProxyHandler` 返回的是文件内容(二进制),而不是JSON响应。错误时使用标准HTTP状态码,保持与文件响应的一致性。 ### 辅助函数 diff --git a/examples/logger_example.go b/examples/logger_example.go index 8c23129..b3aa820 100644 --- a/examples/logger_example.go +++ b/examples/logger_example.go @@ -14,13 +14,20 @@ func main() { log.Fatal("Failed to load config:", err) } - // 使用工厂创建日志记录器(推荐方式) + // 方式1:使用工厂创建日志记录器(推荐方式) fac := factory.NewFactory(cfg) logger, err := fac.GetLogger() if err != nil { log.Fatal("Failed to create logger:", err) } + // 如果使用异步模式,程序退出前需要关闭logger + defer logger.Close() + + // 方式2:直接使用工厂的日志方法(黑盒模式,更简单) + // fac.LogInfo("Application started") + // fac.LogError("An error occurred") + // 示例1:基本日志记录 logger.Info("Application started") logger.Debug("Debug message: %s", "This is a debug message") @@ -45,6 +52,11 @@ func main() { logger.Warn("This is a warn log") logger.Error("This is an error log") + // 示例4:异步模式使用 + // 如果配置中设置了 "async": true,日志会异步写入 + // 程序退出前需要调用 Close() 确保所有日志写入完成 + // logger.Close() + // 注意:Fatal和Panic会终止程序,示例中不执行 // logger.Fatal("This would exit the program") // logger.Panic("This would panic") diff --git a/factory/factory.go b/factory/factory.go index 8340107..5192bb6 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -153,6 +153,14 @@ func (f *Factory) getLogger() (*logger.Logger, error) { return l, nil } +// GetLogger 获取日志记录器对象(已初始化) +// 返回已初始化的日志记录器对象,可直接使用 +// 注意:推荐使用 LogDebug、LogInfo、LogWarn、LogError 等方法直接记录日志 +// 如果需要使用logger的高级功能(如Close方法),可以使用此方法获取logger对象 +func (f *Factory) GetLogger() (*logger.Logger, error) { + return f.getLogger() +} + // LogDebug 记录调试日志 // message: 日志消息 // args: 格式化参数(可选) diff --git a/logger/logger.go b/logger/logger.go index 32c0be6..5358076 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -6,10 +6,19 @@ import ( "log" "os" "path/filepath" + "sync" "git.toowon.com/jimmy/go-common/config" ) +// logMessage 异步日志消息结构 +type logMessage struct { + level string // debug, info, warn, error + format string + args []interface{} + fields map[string]interface{} // 用于带字段的日志 +} + // Logger 日志记录器 type Logger struct { infoLog *log.Logger @@ -17,6 +26,14 @@ type Logger struct { warnLog *log.Logger debugLog *log.Logger config *config.LoggerConfig + + // 异步相关字段 + async bool // 是否异步模式 + logChan chan *logMessage // 异步日志channel + done chan struct{} // 用于优雅关闭 + wg sync.WaitGroup // 等待所有日志写入完成 + closed bool // 是否已关闭 + closeMux sync.RWMutex // 保护closed字段 } // NewLogger 创建日志记录器 @@ -24,9 +41,11 @@ func NewLogger(cfg *config.LoggerConfig) (*Logger, error) { if cfg == nil { // 使用默认配置 cfg = &config.LoggerConfig{ - Level: "info", - Output: "stdout", - FilePath: "", + Level: "info", + Output: "stdout", + FilePath: "", + Async: false, // 默认同步 + BufferSize: 1000, // 默认缓冲区大小 } } @@ -37,6 +56,9 @@ func NewLogger(cfg *config.LoggerConfig) (*Logger, error) { if cfg.Output == "" { cfg.Output = "stdout" } + if cfg.BufferSize <= 0 { + cfg.BufferSize = 1000 // 默认缓冲区大小 + } // 创建输出目标 var writers []io.Writer @@ -89,6 +111,7 @@ func NewLogger(cfg *config.LoggerConfig) (*Logger, error) { // 创建日志记录器 logger := &Logger{ config: cfg, + async: cfg.Async, } // 根据日志级别创建不同的logger @@ -106,39 +129,152 @@ func NewLogger(cfg *config.LoggerConfig) (*Logger, error) { } } + // 如果启用异步模式,启动goroutine处理日志 + if cfg.Async { + logger.logChan = make(chan *logMessage, cfg.BufferSize) + logger.done = make(chan struct{}) + logger.wg.Add(1) + go logger.processLogs() + } + return logger, nil } +// processLogs 异步处理日志(goroutine) +func (l *Logger) processLogs() { + defer l.wg.Done() + + for { + select { + case msg := <-l.logChan: + if msg == nil { + // channel已关闭,退出 + return + } + l.writeLog(msg) + case <-l.done: + // 收到关闭信号,处理完剩余日志后退出 + for { + select { + case msg := <-l.logChan: + if msg == nil { + return + } + l.writeLog(msg) + default: + return + } + } + } + } +} + +// writeLog 实际写入日志(内部方法) +func (l *Logger) writeLog(msg *logMessage) { + var logger *log.Logger + switch msg.level { + case "debug": + logger = l.debugLog + case "info": + logger = l.infoLog + case "warn": + logger = l.warnLog + case "error": + logger = l.errorLog + default: + return + } + + if logger == nil { + return + } + + // 如果有字段,先格式化字段 + format := msg.format + if len(msg.fields) > 0 { + fieldStr := formatFields(msg.fields) + format = fieldStr + format + } + + // 写入日志 + logger.Printf(format, msg.args...) +} + +// isClosed 检查logger是否已关闭 +func (l *Logger) isClosed() bool { + l.closeMux.RLock() + defer l.closeMux.RUnlock() + return l.closed +} + +// setClosed 设置logger为已关闭状态 +func (l *Logger) setClosed() { + l.closeMux.Lock() + defer l.closeMux.Unlock() + l.closed = true +} + +// log 内部日志方法,根据模式选择同步或异步 +func (l *Logger) log(level string, format string, args []interface{}, fields map[string]interface{}) { + // 如果已关闭,直接返回 + if l.isClosed() { + return + } + + // 如果是异步模式,发送到channel + if l.async { + // 检查channel是否已关闭 + select { + case l.logChan <- &logMessage{ + level: level, + format: format, + args: args, + fields: fields, + }: + // 成功发送 + default: + // channel已满或已关闭,同步写入(降级处理) + l.writeLog(&logMessage{ + level: level, + format: format, + args: args, + fields: fields, + }) + } + } else { + // 同步模式,直接写入 + l.writeLog(&logMessage{ + level: level, + format: format, + args: args, + fields: fields, + }) + } +} + // Debug 记录调试日志 func (l *Logger) Debug(format string, v ...interface{}) { - if l.debugLog != nil { - l.debugLog.Printf(format, v...) - } + l.log("debug", format, v, nil) } // Info 记录信息日志 func (l *Logger) Info(format string, v ...interface{}) { - if l.infoLog != nil { - l.infoLog.Printf(format, v...) - } + l.log("info", format, v, nil) } // Warn 记录警告日志 func (l *Logger) Warn(format string, v ...interface{}) { - if l.warnLog != nil { - l.warnLog.Printf(format, v...) - } + l.log("warn", format, v, nil) } // Error 记录错误日志 func (l *Logger) Error(format string, v ...interface{}) { - if l.errorLog != nil { - l.errorLog.Printf(format, v...) - } + l.log("error", format, v, nil) } -// Fatal 记录致命错误日志并退出程序 +// Fatal 记录致命错误日志并退出程序(始终同步) func (l *Logger) Fatal(format string, v ...interface{}) { + // Fatal必须同步执行,确保日志写入后再退出 if l.errorLog != nil { l.errorLog.Fatalf(format, v...) } else { @@ -146,8 +282,9 @@ func (l *Logger) Fatal(format string, v ...interface{}) { } } -// Panic 记录恐慌日志并触发panic +// Panic 记录恐慌日志并触发panic(始终同步) func (l *Logger) Panic(format string, v ...interface{}) { + // Panic必须同步执行,确保日志写入后再panic if l.errorLog != nil { l.errorLog.Panicf(format, v...) } else { @@ -175,33 +312,48 @@ func formatFields(fields map[string]interface{}) string { // Debugf 记录调试日志(带字段) func (l *Logger) Debugf(fields map[string]interface{}, format string, v ...interface{}) { - if l.debugLog != nil { - fieldStr := formatFields(fields) - l.debugLog.Printf(fieldStr+format, v...) - } + l.log("debug", format, v, fields) } // Infof 记录信息日志(带字段) func (l *Logger) Infof(fields map[string]interface{}, format string, v ...interface{}) { - if l.infoLog != nil { - fieldStr := formatFields(fields) - l.infoLog.Printf(fieldStr+format, v...) - } + l.log("info", format, v, fields) } // Warnf 记录警告日志(带字段) func (l *Logger) Warnf(fields map[string]interface{}, format string, v ...interface{}) { - if l.warnLog != nil { - fieldStr := formatFields(fields) - l.warnLog.Printf(fieldStr+format, v...) - } + l.log("warn", format, v, fields) } // Errorf 记录错误日志(带字段) func (l *Logger) Errorf(fields map[string]interface{}, format string, v ...interface{}) { - if l.errorLog != nil { - fieldStr := formatFields(fields) - l.errorLog.Printf(fieldStr+format, v...) - } + l.log("error", format, v, fields) } +// Close 优雅关闭logger(仅异步模式需要) +// 等待所有日志写入完成后再返回 +func (l *Logger) Close() error { + if !l.async { + // 同步模式不需要关闭 + return nil + } + + // 检查是否已关闭 + if l.isClosed() { + return nil + } + + // 设置关闭状态 + l.setClosed() + + // 发送关闭信号 + close(l.done) + + // 关闭channel(会触发processLogs退出) + close(l.logChan) + + // 等待所有日志写入完成 + l.wg.Wait() + + return nil +} diff --git a/storage/handler.go b/storage/handler.go index f0ad866..9bf9528 100644 --- a/storage/handler.go +++ b/storage/handler.go @@ -155,18 +155,17 @@ func NewProxyHandler(storage Storage) *ProxyHandler { // ServeHTTP 处理文件查看请求 // URL参数: key (对象键) +// 注意:此方法直接返回文件内容(二进制),错误时返回标准HTTP错误状态码 func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - handler := commonhttp.NewHandler(w, r) - if r.Method != http.MethodGet { - handler.Error(4001, "Method not allowed") + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 获取对象键 - objectKey := handler.GetQuery("key", "") + objectKey := r.URL.Query().Get("key") if objectKey == "" { - handler.Error(4004, "Missing parameter: key") + http.Error(w, "Missing parameter: key", http.StatusBadRequest) return } @@ -174,19 +173,19 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() exists, err := h.storage.Exists(ctx, objectKey) if err != nil { - handler.SystemError(fmt.Sprintf("Failed to check file existence: %v", err)) + http.Error(w, fmt.Sprintf("Failed to check file existence: %v", err), http.StatusInternalServerError) return } if !exists { - handler.Error(4005, "File not found") + http.Error(w, "File not found", http.StatusNotFound) return } // 获取文件内容 reader, err := h.storage.GetObject(ctx, objectKey) if err != nil { - handler.SystemError(fmt.Sprintf("Failed to get file: %v", err)) + http.Error(w, fmt.Sprintf("Failed to get file: %v", err), http.StatusInternalServerError) return } defer reader.Close() @@ -210,7 +209,8 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 复制文件内容到响应 _, err = io.Copy(w, reader) if err != nil { - handler.SystemError(fmt.Sprintf("Failed to write response: %v", err)) + // 如果已经开始写入响应,无法再设置错误状态码 + // 这里只能记录错误,无法返回错误响应 return } }