日志方法增加异步与同步的方法

This commit is contained in:
2025-11-30 21:01:47 +08:00
parent fd37c5c301
commit de8fc13f18
8 changed files with 310 additions and 45 deletions

View File

@@ -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 从文件加载配置

View File

@@ -75,7 +75,9 @@
"output": "stdout",
"filePath": "",
"prefix": "app",
"disableTimestamp": false
"disableTimestamp": false,
"async": false,
"bufferSize": 1000
}
}

View File

@@ -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()
}
```

View File

@@ -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状态码保持与文件响应的一致性。
### 辅助函数

View File

@@ -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")

View File

@@ -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: 格式化参数(可选)

View File

@@ -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 创建日志记录器
@@ -27,6 +44,8 @@ func NewLogger(cfg *config.LoggerConfig) (*Logger, error) {
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
}

View File

@@ -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
}
}