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
相关产品推荐
相关产品推荐

