You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go开发RESTful API:如何实现规范的结构化请求日志最佳实践?

Go + Echo 实现结构化访问日志(Beautiful Logger)

核心思路

利用Echo的全局中间件拦截所有HTTP请求,在请求前后捕获所需的上下文信息,结合Logrus输出符合要求的结构化日志。中间件是实现统一日志的最佳方式,能覆盖所有HTTP方法(GET/POST/PUT/DELETE等)的端点。

分步实现示例

1. 自定义响应写入器(捕获响应内容)

import (
    "bytes"
    "io"
    "strings"
    "time"

    "github.com/labstack/echo/v4"
    "github.com/sirupsen/logrus"
    "github.com/spf13/viper"
)

// 自定义响应记录器,用于捕获响应体
type responseRecorder struct {
    echo.ResponseWriter
    body *bytes.Buffer
}

func (rr *responseRecorder) Write(b []byte) (int, error) {
    rr.body.Write(b)
    return rr.ResponseWriter.Write(b)
}

2. 实现全局访问日志中间件

// AccessLogMiddleware 生成全局访问日志中间件
func AccessLogMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 1. 基础环境与时间信息
            env := viper.GetString("env")
            timestamp := time.Now().UTC().Format("2006-01-02 15:04:05 UTC")
            
            // 2. 请求基础信息
            httpMethod := c.Request().Method
            endpoint := c.Request().URL.String()
            deviceIP := c.RealIP()
            
            // 3. 解析客户端操作系统(从User-Agent提取)
            deviceOS := "unknown"
            userAgent := c.Request().UserAgent()
            switch {
            case strings.Contains(userAgent, "Windows"):
                deviceOS = "windows"
            case strings.Contains(userAgent, "Ubuntu"):
                deviceOS = "ubuntu"
            case strings.Contains(userAgent, "Mac OS"):
                deviceOS = "macos"
            case strings.Contains(userAgent, "Android"):
                deviceOS = "android"
            case strings.Contains(userAgent, "iOS"):
                deviceOS = "ios"
            }

            // 4. 捕获请求体(注意重置请求体,避免后续Handler无法读取)
            requestBody := ""
            if c.Request().Body != nil && c.Request().ContentLength > 0 {
                bodyBytes, err := io.ReadAll(c.Request().Body)
                if err == nil {
                    requestBody = string(bodyBytes)
                    // 重置请求体
                    c.Request().Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
                }
            }

            // 5. 获取授权用户信息(需结合你的认证逻辑,示例假设上下文存了user对象)
            authEmail := ""
            authRole := ""
            if user, ok := c.Get("auth_user").(YourUserModel); ok {
                authEmail = user.Email
                authRole = user.Role
            }

            // 6. 替换响应写入器,捕获响应内容
            rr := &responseRecorder{
                ResponseWriter: c.Response().Writer,
                body:           bytes.NewBufferString(""),
            }
            c.Response().Writer = rr

            // 7. 执行后续业务Handler
            err := next(c)
            if err != nil {
                c.Error(err)
            }

            // 8. 收集响应状态与内容
            statusCode := c.Response().Status
            responseBody := rr.body.String()

            // 9. Logrus输出结构化日志
            logrus.WithFields(logrus.Fields{
                "env":                env,
                "timestamp":          timestamp,
                "auth_email":         authEmail,
                "auth_role":          authRole,
                "endpoint":           endpoint,
                "http_method":        httpMethod,
                "http_request_body":  requestBody,
                "http_status":        statusCode,
                "http_response":      responseBody,
                "device_ip_address":  deviceIP,
                "device_os":          deviceOS,
            }).Info("access_log")

            return err
        }
    }
}

3. 注册中间件到Echo实例

func main() {
    e := echo.New()

    // 加载Viper配置(示例从.env文件读取)
    viper.SetConfigFile(".env")
    viper.ReadInConfig()

    // 注册全局访问日志中间件(所有请求都会被拦截记录)
    e.Use(AccessLogMiddleware())

    // 注册你的API路由示例
    e.GET("/users", func(c echo.Context) error {
        return c.JSON(200, map[string]any{"data": []string{"user1", "user2"}})
    })
    e.POST("/users", func(c echo.Context) error {
        return c.JSON(201, map[string]string{"message": "user created"})
    })

    e.Start(":8080")
}

关键注意事项

  • 请求体重置:读取请求体后必须用io.NopCloser重置,否则后续业务Handler无法读取请求体,导致逻辑异常。
  • 敏感信息脱敏:如果请求/响应体包含密码、Token等敏感数据,需在日志输出前做脱敏处理(比如替换为***)。
  • 性能优化:高流量场景下,可将日志发送到异步队列(如Redis)批量写入,避免同步IO阻塞请求。
  • 认证信息适配:示例中从上下文auth_user获取用户信息,需根据你的认证中间件(如JWT解析)调整对应逻辑。

最佳实践补充

  • 统一日志字段:通过Logrus的Fields保证所有日志字段结构一致,方便后续日志分析工具(如ELK、Grafana Loki)解析。
  • 时区统一:强制使用UTC时间记录,避免多时区部署时的日志时间混乱。
  • 环境标记:通过Viper加载不同环境配置,日志中明确标记env字段,便于区分开发/测试/生产环境的日志。
  • 错误日志增强:若Handler返回错误,可在日志Fields中增加error_msg字段,记录具体错误信息。

内容的提问来源于stack exchange,提问作者ramadoiranedar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 05:35:23