跳到主要内容

Logger 中间件

Logger 中间件用于记录每个请求的开始和结束,包括请求信息、响应状态、处理时间等有用的数据。

使用方法

基础用法

import (
"go-slim.dev/slim"
"go-slim.dev/slim/middleware"
)

func main() {
s := slim.New()

// 配置错误处理器以记录错误日志
s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err) // 确保错误也被记录
return err
}

// Logger 应该在 Recovery 之前
s.Use(middleware.Logger())
s.Use(slim.Recovery())

s.GET("/", func(c slim.Context) error {
return c.String(200, "Hello, World!")
})

s.Start(":8080")
}

自定义配置

s := slim.New()

s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
TimeLayout: "2006-01-02 15:04:05",
NewEntry: func(c slim.Context) middleware.LogEntry {
return middleware.NewLogEntry(os.Stdout)
},
}))

配置选项

LoggerConfig

type LoggerConfig struct {
// TimeLayout 定义时间格式
// 可选。默认值 "2006/01/02 15:04:05.000"
TimeLayout string

// NewEntry 为每个请求创建日志条目
// 可选。默认使用标准输出
NewEntry func(slim.Context) LogEntry
}

LogEntry 接口

type LogEntry interface {
Begin(c slim.Context) map[string]any
End(c slim.Context, err error) map[string]any
SetTimeLayout(layout string)
SetColorable(colorable bool)
Print(c slim.Context, p LogPayload)
Panic(v any, stack []byte)
}

LogPayload 结构

type LogPayload struct {
StartTime time.Time // 请求开始时间
Proto string // HTTP 协议版本
RequestURI string // 请求 URI
RawQuery string // 查询字符串
Method string // HTTP 方法
RemoteAddr string // 客户端地址
Extra map[string]any // 额外数据
Error error // 错误信息
StatusCode int // 响应状态码
Written int // 响应字节数
Elapsed time.Duration // 处理耗时
}

日志输出格式

默认日志输出包含以下信息:

2006/01/02 15:04:05.000 [request-id] "GET /api/users HTTP/1.1" from 127.0.0.1:54321 - 200 1234B in 45ms

日志格式说明

  • 时间戳(可配置格式)
  • 请求 ID(如果存在)
  • HTTP 方法和请求路径
  • 客户端地址
  • 响应状态码(带颜色标识)
  • 响应大小
  • 处理耗时(带颜色标识)

使用示例

1. 基本日志记录

s := slim.New()

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)
return err
}

s.Use(middleware.Logger())

s.GET("/api/users", func(c slim.Context) error {
return c.JSON(200, map[string]string{"message": "success"})
})

输出示例

2024/11/08 10:30:15.123 "GET /api/users HTTP/1.1" from 127.0.0.1:54321 - 200 28B in 2.5ms

2. 自定义时间格式

s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
TimeLayout: "2006-01-02 15:04:05",
}))

输出示例

2024-11-08 10:30:15 "GET /api/users HTTP/1.1" from 127.0.0.1:54321 - 200 28B in 2.5ms

3. 自定义日志条目

type customLogEntry struct {
*middleware.defaultLogEntry
logger *log.Logger
}

func (c *customLogEntry) Begin(ctx slim.Context) map[string]any {
// 在请求开始时记录额外信息
return map[string]any{
"user_agent": ctx.Request().UserAgent(),
}
}

func (c *customLogEntry) End(ctx slim.Context, err error) map[string]any {
// 在请求结束时记录额外信息
extra := make(map[string]any)
if userID := ctx.Get("user_id"); userID != nil {
extra["user_id"] = userID
}
return extra
}

s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
NewEntry: func(c slim.Context) middleware.LogEntry {
return &customLogEntry{
logger: log.New(os.Stdout, "", 0),
}
},
}))

4. 添加请求 ID

s.Use(middleware.RequestID()) // 添加请求 ID 中间件
s.Use(middleware.Logger()) // Logger 会自动记录请求 ID

s.GET("/api/users", func(c slim.Context) error {
return c.JSON(200, map[string]string{"message": "success"})
})

输出示例

2024/11/08 10:30:15.123 [abc123-def456] "GET /api/users HTTP/1.1" from 127.0.0.1:54321 - 200 28B in 2.5ms

5. 记录错误信息

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err) // 记录错误
return c.JSON(500, map[string]string{"error": err.Error()})
}

s.Use(middleware.Logger())

s.GET("/api/error", func(c slim.Context) error {
return errors.New("something went wrong")
})

输出示例

2024/11/08 10:30:15.123 "GET /api/error HTTP/1.1" from 127.0.0.1:54321 - 500 35B in 1.2ms

Spot a mistake:
something went wrong

6. 记录额外数据

s.Use(middleware.Logger())

s.GET("/api/users/:id", func(c slim.Context) error {
// 在日志中添加额外信息
payload, _ := middleware.GetLogPayload(c)
if payload.Extra == nil {
payload.Extra = make(map[string]any)
}
payload.Extra["user_id"] = c.Param("id")
middleware.ProvideLogPayload(c, payload)

return c.JSON(200, map[string]string{"id": c.Param("id")})
})

输出示例

2024/11/08 10:30:15.123 "GET /api/users/123 HTTP/1.1" from 127.0.0.1:54321 - 200 15B in 3ms

Additional data:
{
"user_id": "123"
}

颜色输出

Logger 中间件在 TTY 环境下会自动使用彩色输出:

  • 状态码颜色

    • 1xx(信息):蓝色
    • 2xx(成功):绿色
    • 3xx(重定向):青色
    • 4xx(客户端错误):黄色
    • 5xx(服务器错误):红色
  • 耗时颜色

    • < 500ms:绿色
    • 500ms - 5s:黄色
    • 5s:红色

工作原理

  1. 请求开始Logger 中间件在 LogBegin() 中记录请求开始时间和基本信息
  2. 处理请求:调用下一个处理器处理请求
  3. 请求结束
    • 如果没有错误,在中间件中调用 LogEnd(c, nil) 记录日志
    • 如果有错误,需要在 ErrorHandler 中调用 LogEnd(c, err) 记录日志
  4. 输出日志Print() 方法格式化并输出日志

中间件顺序

重要:Logger 应该在 Recovery 之前,这样可以确保 panic 也被正确记录:

s.ErrorHandler = func (c slim.Context, err error) error {
middleware.LogEnd(c, err) // 在错误处理器中记录日志
return err
}
s.Use(middleware.Logger()) // Logger 在前
s.Use(slim.Recovery()) // Recovery 在后

最佳实践

1. 配置错误处理器

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)

// 根据错误类型返回不同的响应
if he, ok := err.(*slim.HTTPError); ok {
return c.JSON(he.Code, he)
}
return c.JSON(500, map[string]string{"error": "Internal Server Error"})
}

2. 使用结构化日志

type structuredLogEntry struct {
logger *slog.Logger
}

func (s *structuredLogEntry) Print(c slim.Context, p middleware.LogPayload) {
s.logger.Info("request completed",
slog.String("method", p.Method),
slog.String("uri", p.RequestURI),
slog.Int("status", p.StatusCode),
slog.Duration("elapsed", p.Elapsed),
slog.String("remote_addr", p.RemoteAddr),
)
}

s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
NewEntry: func(c slim.Context) middleware.LogEntry {
return &structuredLogEntry{
logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)),
}
},
}))

3. 根据环境调整日志级别

func newLogger() slim.MiddlewareFunc {
if os.Getenv("ENV") == "production" {
// 生产环境使用结构化日志
return middleware.LoggerWithConfig(middleware.LoggerConfig{
NewEntry: func(c slim.Context) middleware.LogEntry {
return newStructuredLogger()
},
})
}
// 开发环境使用默认彩色日志
return middleware.Logger()
}

s.Use(newLogger())

4. 记录慢请求

s.Use(middleware.Logger())

s.Use(func(c slim.Context, next slim.HandlerFunc) error {
start := time.Now()
err := next(c)
elapsed := time.Since(start)

// 记录超过 1 秒的慢请求
if elapsed > time.Second {
log.Printf("SLOW REQUEST: %s %s took %v",
c.Request().Method,
c.Request().URL.Path,
elapsed)
}
return err
})

高级功能

LogBegin 和 LogEnd

您可以手动控制日志记录:

// 在处理器中手动记录日志
s.GET("/api/custom", func(c slim.Context) error {
// 开始记录
middleware.LogBegin(c)

// 处理请求
result := doSomething()

// 结束记录
middleware.LogEnd(c, nil)

return c.JSON(200, result)
})

获取和设置日志数据

s.Use(middleware.Logger())

s.GET("/api/users", func(c slim.Context) error {
// 获取日志负载
payload, ok := middleware.GetLogPayload(c)
if ok {
// 添加自定义数据
if payload.Extra == nil {
payload.Extra = make(map[string]any)
}
payload.Extra["custom_field"] = "custom_value"

// 更新日志负载
middleware.ProvideLogPayload(c, payload)
}

return c.JSON(200, map[string]string{"message": "success"})
})

与其他日志库集成

与 slog 集成

import (
"log/slog"
"go-slim.dev/slim/middleware"
)

type slogEntry struct {
logger *slog.Logger
}

func (s *slogEntry) Begin(c slim.Context) map[string]any { return nil }
func (s *slogEntry) End(c slim.Context, err error) map[string]any { return nil }
func (s *slogEntry) SetTimeLayout(layout string) {}
func (s *slogEntry) SetColorable(colorable bool) {}

func (s *slogEntry) Print(c slim.Context, p middleware.LogPayload) {
attrs := []slog.Attr{
slog.String("method", p.Method),
slog.String("uri", p.RequestURI),
slog.Int("status", p.StatusCode),
slog.Duration("elapsed", p.Elapsed),
}

if p.Error != nil {
s.logger.LogAttrs(context.Background(), slog.LevelError, "request failed", attrs...)
} else {
s.logger.LogAttrs(context.Background(), slog.LevelInfo, "request completed", attrs...)
}
}

func (s *slogEntry) Panic(v any, stack []byte) {
s.logger.Error("panic recovered",
slog.Any("panic", v),
slog.String("stack", string(stack)))
}

参考资料