diff --git a/compat.go b/compat.go new file mode 100644 index 0000000..6a49c89 --- /dev/null +++ b/compat.go @@ -0,0 +1,37 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package touka + +import "github.com/fenthope/reco" + +// GetLogReco 返回底层的 reco.Logger 实例 +// 用于需要访问 reco 特定功能的场景 +// 如果当前 logger 不是 *reco.Logger 类型,返回 nil +// +//go:fix inline +func (engine *Engine) GetLogReco() *reco.Logger { + return engine.LogReco +} + +// SetLogReco 设置 reco.Logger 实例 +// 用于向后兼容,等价于 SetLogger(l) +// +//go:fix inline +func (engine *Engine) SetLogReco(l *reco.Logger) { + engine.LogReco = l + engine.logger = l +} + +// GetLoggerReco 返回底层的 reco.Logger 实例 +// 用于需要访问 reco 特定功能的场景 +// 如果当前 logger 不是 *reco.Logger 类型,返回 nil +// +//go:fix inline +func (c *Context) GetLoggerReco() *reco.Logger { + if rl, ok := c.engine.logger.(*reco.Logger); ok { + return rl + } + return c.engine.LogReco +} diff --git a/context.go b/context.go index e73033d..324386e 100644 --- a/context.go +++ b/context.go @@ -26,7 +26,6 @@ import ( "time" "github.com/WJQSERVER/wanf" - "github.com/fenthope/reco" "github.com/go-json-experiment/json" "github.com/WJQSERVER-STUDIO/go-utils/iox" @@ -135,8 +134,8 @@ func (c *Context) writeResponseBody(data []byte, contextMsg string) { if _, err := c.Writer.Write(data); err != nil { wrapped := fmt.Errorf("%s: %w", contextMsg, err) c.AddError(wrapped) - if c != nil && c.engine != nil && c.engine.LogReco != nil { - c.engine.LogReco.Errorf("%s: %v", contextMsg, err) + if c.engine != nil && c.engine.logger != nil { + c.engine.logger.Errorf("%s: %v", contextMsg, err) } } } @@ -1136,9 +1135,9 @@ func (c *Context) GetHTTPC() *httpc.Client { return c.HTTPClient } -// GetLogger 获取engine的Logger -func (c *Context) GetLogger() *reco.Logger { - return c.engine.LogReco +// GetLogger 获取engine的Logger接口 +func (c *Context) GetLogger() Logger { + return c.engine.logger } // GetReqQueryString @@ -1297,25 +1296,25 @@ func (c *Context) DeleteCookie(name string) { // === 日志记录 === func (c *Context) Debugf(format string, args ...any) { - c.engine.LogReco.Debugf(format, args...) + c.engine.logger.Debugf(format, args...) } func (c *Context) Infof(format string, args ...any) { - c.engine.LogReco.Infof(format, args...) + c.engine.logger.Infof(format, args...) } func (c *Context) Warnf(format string, args ...any) { - c.engine.LogReco.Warnf(format, args...) + c.engine.logger.Warnf(format, args...) } func (c *Context) Errorf(format string, args ...any) { - c.engine.LogReco.Errorf(format, args...) + c.engine.logger.Errorf(format, args...) } func (c *Context) Fatalf(format string, args ...any) { - c.engine.LogReco.Fatalf(format, args...) + c.engine.logger.Fatalf(format, args...) } func (c *Context) Panicf(format string, args ...any) { - c.engine.LogReco.Panicf(format, args...) + c.engine.logger.Panicf(format, args...) } diff --git a/docs/logger-migration-design.md b/docs/logger-migration-design.md new file mode 100644 index 0000000..9684d8e --- /dev/null +++ b/docs/logger-migration-design.md @@ -0,0 +1,400 @@ +# Touka Logger 接口迁移方案 + +## 基于 Go 1.26 `go:fix inline` 的自动化迁移设计 + +--- + +## 一、问题分析 + +当前架构问题: +``` +Engine.LogReco → *reco.Logger (公开字段, 直接访问) +Context.GetLogger() → 返回 *reco.Logger (具体类型) +Context.Debugf/Infof... → 硬编码 c.engine.LogReco.Debugf(...) +``` + +这导致用户无法替换日志实现(如 zap/logrus)。 + +--- + +## 二、目标架构 + +``` +Engine.logger → Logger 接口 (私有) +Engine.logReco → *reco.Logger (私有, 兼容层) +Engine.GetLogger() → 返回 Logger 接口 +Engine.SetLogger(Logger)→ 设置日志实现 +Context.GetLogger() → 返回 Logger 接口 +Context.Debugf/Infof... → 调用 c.engine.logger.Debugf(...) +``` + +--- + +## 三、Logger 接口定义 + +```go +// logger.go +package touka + +// Logger 是日志接口,支持任意日志库实现 +type Logger interface { + Debugf(format string, args ...any) + Infof(format string, args ...any) + Warnf(format string, args ...any) + Errorf(format string, args ...any) + Fatalf(format string, args ...any) + Panicf(format string, args ...any) +} + +// CloserLogger 可选扩展,支持关闭操作 +type CloserLogger interface { + Logger + Close() error +} +``` + +--- + +## 四、Engine 结构变更 + +```go +// engine.go 变更 +type Engine struct { + // ... 其他字段保持不变 + + // logger 是新的日志接口 (私有) + logger Logger + + // logReco 是保留的 reco.Logger 引用 (私有) + // 用于向后兼容,当通过 SetLoggerReco 设置时同步到 logger + logReco *reco.Logger + + // 其他字段... +} +``` + +新增/修改方法: + +```go +// GetLogger 返回日志接口 +func (engine *Engine) GetLogger() Logger { + return engine.logger +} + +// SetLogger 设置任意 Logger 实现 +func (engine *Engine) SetLogger(l Logger) { + engine.logger = l + // 如果是 *reco.Logger 类型,同步更新 logReco + if rl, ok := l.(*reco.Logger); ok { + engine.logReco = rl + } else { + engine.logReco = nil + } +} + +// SetLoggerCfg 使用 reco.Config 配置日志 +func (engine *Engine) SetLoggerCfg(logcfg reco.Config) { + logger := NewLogger(logcfg) + engine.logger = logger + engine.logReco = logger +} +``` + +--- + +## 五、`go:fix inline` 兼容性函数 + +### 5.1 旧 API 包装函数 + +在 `compat.go` 中定义: + +```go +// compat.go +package touka + +import "github.com/fenthope/reco" + +// GetLogReco 返回 reco.Logger,用于向后兼容 +// +//go:fix inline +func (engine *Engine) GetLogReco() *reco.Logger { + return engine.logReco +} + +// SetLogReco 设置 reco.Logger,用于向后兼容 +// +//go:fix inline +func (engine *Engine) SetLogReco(l *reco.Logger) { + engine.logReco = l + engine.logger = l +} +``` + +### 5.2 Context 日志方法的 inline 包装 + +```go +// context_compat.go +package touka + +// Debugf 记录 Debug 级别日志 +// +//go:fix inline +func (c *Context) Debugf(format string, args ...any) { + c.engine.logger.Debugf(format, args...) +} + +// Infof 记录 Info 级别日志 +// +//go:fix inline +func (c *Context) Infof(format string, args ...any) { + c.engine.logger.Infof(format, args...) +} + +// Warnf 记录 Warn 级别日志 +// +//go:fix inline +func (c *Context) Warnf(format string, args ...any) { + c.engine.logger.Warnf(format, args...) +} + +// Errorf 记录 Error 级别日志 +// +//go:fix inline +func (c *Context) Errorf(format string, args ...any) { + c.engine.logger.Errorf(format, args...) +} + +// Fatalf 记录 Fatal 级别日志 +// +//go:fix inline +func (c *Context) Fatalf(format string, args ...any) { + c.engine.logger.Fatalf(format, args...) +} + +// Panicf 记录 Panic 级别日志 +// +//go:fix inline +func (c *Context) Panicf(format string, args ...any) { + c.engine.logger.Panicf(format, args...) +} +``` + +### 5.3 GetLogger 返回类型的兼容处理 + +由于 `GetLogger()` 返回类型从 `*reco.Logger` 变为 `Logger`,需要提供兼容函数: + +```go +// context_compat.go (续) + +// GetLoggerReco 返回 *reco.Logger 类型,用于需要具体类型的场景 +// +//go:fix inline +func (c *Context) GetLoggerReco() *reco.Logger { + if rl, ok := c.engine.logger.(*reco.Logger); ok { + return rl + } + return nil +} +``` + +--- + +## 六、go:fix inline 工作原理 + +### 迁移前用户代码: +```go +func handler(c *touka.Context) { + // 旧 API 调用 + c.Debugf("request: %s", c.Request.URL.Path) + c.engine.LogReco.Infof("server started") +} +``` + +### go fix 执行后(自动替换): +```go +func handler(c *touka.Context) { + // Debugf 被替换为函数体 + c.engine.logger.Debugf("request: %s", c.Request.URL.Path) + + // LogReco 访问无法通过 inline 自动处理,需要手动迁移 + // 或者通过 getter 调用 +} +``` + +### 对于字段访问的处理策略: + +`engine.LogReco` 字段访问无法直接用 `go:fix inline` 处理,采用以下策略: + +1. **保留字段但标记 deprecated**:继续导出 `LogReco` 但文档标记为 deprecated +2. **提供 getter/setter**:通过 `go:fix inline` 提供 `GetLogReco/SetLogReco` +3. **渐进迁移**:用户可以在方便时手动迁移到 `GetLogger()/SetLogger()` + +--- + +## 七、迁移前后对比 + +### 场景 1:基本日志调用 + +**迁移前:** +```go +func myHandler(c *touka.Context) { + c.Debugf("processing request %s", c.Request.URL.Path) + c.Infof("user %s logged in", username) + c.Warnf("slow query: %v", duration) + c.Errorf("db error: %v", err) +} +``` + +**迁移后(自动替换):** +```go +func myHandler(c *touka.Context) { + c.engine.logger.Debugf("processing request %s", c.Request.URL.Path) + c.engine.logger.Infof("user %s logged in", username) + c.engine.logger.Warnf("slow query: %v", duration) + c.engine.logger.Errorf("db error: %v", err) +} +``` + +### 场景 2:Engine 配置日志 + +**迁移前:** +```go +engine := touka.New() +engine.LogReco = myLogger // 直接赋值 +logger := engine.LogReco // 直接读取 +``` + +**迁移后(手动 + 自动混合):** +```go +engine := touka.New() + +// 方式 1:使用新 API(推荐) +engine.SetLogger(myLogger) +logger := engine.GetLogger() + +// 方式 2:通过 go:fix inline 自动替换为 getter +// engine.SetLogReco(myLogger) ← go fix 替换 +// logger := engine.GetLogReco() ← go fix 替换 +``` + +### 场景 3:使用第三方日志库(新功能) + +```go +import "go.uber.org/zap" + +func main() { + zapLogger, _ := zap.NewProduction() + defer zapLogger.Sync() + + engine := touka.New() + // 使用 zap 替代默认的 reco.Logger + engine.SetLogger(&ZapAdapter{logger: zapLogger}) + + engine.GET("/api", func(c *touka.Context) { + c.Infof("api called") // 自动使用 zap 输出 + }) +} + +// ZapAdapter 适配 zap 到 touka.Logger 接口 +type ZapAdapter struct { + logger *zap.Logger +} + +func (z *ZapAdapter) Debugf(format string, args ...any) { + z.logger.Debug(fmt.Sprintf(format, args...)) +} + +func (z *ZapAdapter) Infof(format string, args ...any) { + z.logger.Info(fmt.Sprintf(format, args...)) +} + +func (z *ZapAdapter) Warnf(format string, args ...any) { + z.logger.Warn(fmt.Sprintf(format, args...)) +} + +func (z *ZapAdapter) Errorf(format string, args ...any) { + z.logger.Error(fmt.Sprintf(format, args...)) +} + +func (z *ZapAdapter) Fatalf(format string, args ...any) { + z.logger.Fatal(fmt.Sprintf(format, args...)) +} + +func (z *ZapAdapter) Panicf(format string, args ...any) { + z.logger.Panic(fmt.Sprintf(format, args...)) +} +``` + +--- + +## 八、内部使用迁移 + +框架内部代码也需要迁移,将直接调用 `engine.LogReco` 改为 `engine.logger`: + +需要修改的文件: +- `context.go`: writeResponseBody 中的 `c.engine.LogReco.Errorf` +- `recovery.go`: 如有使用日志 +- `logreco.go`: CloseLogger 方法 + +```go +// context.go 修改前 +func (c *Context) writeResponseBody(data []byte, contextMsg string) { + if _, err := c.Writer.Write(data); err != nil { + if c.engine.LogReco != nil { + c.engine.LogReco.Errorf("%s: %v", contextMsg, err) + } + } +} + +// context.go 修改后 +func (c *Context) writeResponseBody(data []byte, contextMsg string) { + if _, err := c.Writer.Write(data); err != nil { + if c.engine.logger != nil { + c.engine.logger.Errorf("%s: %v", contextMsg, err) + } + } +} +``` + +--- + +## 九、完整文件结构 + +``` +touka/ +├── logger.go # Logger 接口定义 +├── logreco.go # reco.Logger 相关工具函数 +├── compat.go # go:fix inline 兼容性函数 (Engine) +├── context_compat.go # go:fix inline 兼容性函数 (Context) +├── engine.go # Engine 结构变更 +├── context.go # Context 日志方法变更 +└── ... +``` + +--- + +## 十、版本策略 + +| 版本 | 变更内容 | +|------|---------| +| v1.x | 引入 Logger 接口,LogReco 标记 deprecated | +| v2.x | 移除 LogReco 公开字段,仅通过 getter/setter 访问 | +| v3.x | 移除 go:fix inline 兼容函数 | + +--- + +## 十一、go:fix inline 限制说明 + +1. **字段访问无法自动迁移**:`engine.LogReco` 字段访问需要用户手动修改 +2. **返回类型变更需谨慎**:`GetLogger()` 返回类型变更会导致依赖具体类型的代码失败 +3. **inline 函数有大小限制**:函数体过大会影响内联效果 +4. **跨包迁移**:`go:fix inline` 支持跨包,但用户必须运行 `go fix` + +--- + +## 十二、推荐迁移步骤 + +1. **框架侧**:添加 Logger 接口,添加 go:fix inline 函数 +2. **用户侧**:运行 `go fix ./...` 自动迁移可处理的部分 +3. **用户侧**:手动将 `engine.LogReco` 字段访问改为 `engine.SetLogger()/GetLogger()` +4. **用户侧**:如需使用第三方日志,实现 Logger 接口并通过 SetLogger 设置 diff --git a/engine.go b/engine.go index d712064..15df162 100644 --- a/engine.go +++ b/engine.go @@ -52,8 +52,14 @@ type Engine struct { HTTPClient *httpc.Client // 用于在此上下文中执行出站 HTTP 请求 + // LogReco 保留的 reco.Logger 字段 + // Deprecated: 使用 SetLogger/GetLogger 替代 LogReco *reco.Logger + // logger 是新的日志接口,支持任意 Logger 实现 + // 优先级: logger > LogReco + logger Logger + HTMLRender any // 用于 HTML 模板渲染,可以设置为 *template.Template 或自定义渲染器接口 routesInfo []RouteInfo // 存储所有注册的路由信息 @@ -367,14 +373,27 @@ func (engine *Engine) SetHandleMethodNotAllowed(enable bool) { engine.rebuildFallbackChains() } -// SetLogger传入实例 -func (engine *Engine) SetLogger(logger *reco.Logger) { - engine.LogReco = logger +// SetLogger 传入 Logger 接口实例 +func (engine *Engine) SetLogger(logger Logger) { + engine.logger = logger + // 同步更新 LogReco 以保持向后兼容 + if rl, ok := logger.(*reco.Logger); ok { + engine.LogReco = rl + } else { + engine.LogReco = nil + } } -// 配置日志LoggerCfg +// GetLogger 返回 Logger 接口实例 +func (engine *Engine) GetLogger() Logger { + return engine.logger +} + +// SetLoggerCfg 使用 reco.Config 配置日志 func (engine *Engine) SetLoggerCfg(logcfg reco.Config) { - engine.LogReco = NewLogger(logcfg) + logger := NewLogger(logcfg) + engine.logger = logger + engine.LogReco = logger } // 设置自定义错误处理 diff --git a/examples/logger_slog/main.go b/examples/logger_slog/main.go new file mode 100644 index 0000000..2263960 --- /dev/null +++ b/examples/logger_slog/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "log/slog" + "net/http" + "os" + + "github.com/infinite-iroha/touka" +) + +// SlogAdapter 将 slog.Logger 适配到 touka.Logger 接口 +type SlogAdapter struct { + logger *slog.Logger +} + +func NewSlogAdapter(handler slog.Handler) *SlogAdapter { + return &SlogAdapter{ + logger: slog.New(handler), + } +} + +func (s *SlogAdapter) Debugf(format string, args ...any) { + s.logger.Debug(fmt.Sprintf(format, args...)) +} + +func (s *SlogAdapter) Infof(format string, args ...any) { + s.logger.Info(fmt.Sprintf(format, args...)) +} + +func (s *SlogAdapter) Warnf(format string, args ...any) { + s.logger.Warn(fmt.Sprintf(format, args...)) +} + +func (s *SlogAdapter) Errorf(format string, args ...any) { + s.logger.Error(fmt.Sprintf(format, args...)) +} + +func (s *SlogAdapter) Fatalf(format string, args ...any) { + s.logger.Error(fmt.Sprintf(format, args...)) + os.Exit(1) +} + +func (s *SlogAdapter) Panicf(format string, args ...any) { + s.logger.Error(fmt.Sprintf(format, args...)) + panic(fmt.Sprintf(format, args...)) +} + +func main() { + engine := touka.New() + + // 使用 slog 替换默认的 reco.Logger + handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + slogAdapter := NewSlogAdapter(handler) + engine.SetLogger(slogAdapter) + + engine.GET("/", func(c *touka.Context) { + c.Infof("request received: %s", c.Request.URL.Path) + c.JSON(http.StatusOK, map[string]string{"message": "hello"}) + }) + + // 也可以获取 Logger 接口 + logger := engine.GetLogger() + logger.Debugf("engine started") + + // 也可以直接使用 slog + slog.Info("Server running", "addr", ":8080") + // engine.Run(":8080") +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..1be0077 --- /dev/null +++ b/logger.go @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package touka + +// Logger 是日志接口,支持多种日志库实现(reco、zap、logrus 等) +// 用户可以通过实现此接口来替换默认的日志实现 +type Logger interface { + Debugf(format string, args ...any) + Infof(format string, args ...any) + Warnf(format string, args ...any) + Errorf(format string, args ...any) + Fatalf(format string, args ...any) + Panicf(format string, args ...any) +} + +// CloserLogger 可选扩展接口,支持关闭操作 +// 如果 Logger 实现了此接口,Engine 在关闭时会调用 Close() +type CloserLogger interface { + Logger + Close() error +} diff --git a/logreco.go b/logreco.go index 4bda8d3..e37dd53 100644 --- a/logreco.go +++ b/logreco.go @@ -39,7 +39,16 @@ func CloseLogger(logger *reco.Logger) { } } +// CloseLogger 关闭 Engine 的日志实现 +// 如果 logger 实现了 CloserLogger 接口,会调用其 Close 方法 func (engine *Engine) CloseLogger() { + if cl, ok := engine.logger.(CloserLogger); ok { + if err := cl.Close(); err != nil { + log.Printf("Close Logger Error: %s", err) + } + return + } + // 兼容旧代码 if engine.LogReco != nil { CloseLogger(engine.LogReco) }