在Golang的Web开发场景中,管理Web日志并完整记录请求和响应信息,是排查接口异常、统计服务性能的重要手段。通过合理的日志设计,我们可以在不侵入业务代码的前提下,获取每一次请求的全链路数据。

核心需求分析
记录Web请求和响应信息的日志,通常需要包含以下核心内容:
- 请求基础信息:请求方法、请求路径、请求头、请求参数、客户端IP
- 响应基础信息:响应状态码、响应头、响应内容长度
- 链路信息:请求处理耗时、请求唯一标识、 TraceID
- 异常信息:请求处理过程中的错误信息
实现方案:基于中间件拦截
Golang的Web框架大多支持中间件机制,我们可以通过自定义中间件,在请求进入业务逻辑前和响应返回后分别采集数据,避免在每个接口中重复编写日志代码。
1. 定义日志结构体
首先定义存储日志信息的结构体,方便后续统一处理日志内容:
package middleware
import (
"net/http"
"time"
)
// WebLog 存储Web请求响应日志的结构体
type WebLog struct {
RequestID string // 请求唯一标识
Method string // 请求方法
Path string // 请求路径
ClientIP string // 客户端IP
StatusCode int // 响应状态码
Latency time.Duration // 请求处理耗时
ReqHeader http.Header // 请求头
RespHeader http.Header // 响应头
ErrorMsg string // 错误信息
}
2. 实现响应写入包装器
默认情况下,我们无法直接获取响应写入的内容,需要自定义一个实现http.ResponseWriter接口的包装器,拦截写入的响应状态码和响应头:
// responseWriterWrapper 包装http.ResponseWriter,用于捕获响应状态码和响应头
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
header http.Header
}
// WriteHeader 重写WriteHeader方法,记录响应状态码
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
// Header 重写Header方法,记录响应头
func (w *responseWriterWrapper) Header() http.Header {
if w.header == nil {
w.header = make(http.Header)
}
// 复制原始响应头到自定义header中
for k, v := range w.ResponseWriter.Header() {
w.header[k] = v
}
return w.header
}
3. 编写日志中间件
接下来实现核心的日志中间件,完成请求信息的采集、业务逻辑执行、响应信息采集和日志输出:
package middleware
import (
"context"
"log"
"net/http"
"time"
"github.com/google/uuid"
)
// WebLogMiddleware Web日志中间件
func WebLogMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 生成请求唯一标识
requestID := uuid.New().String()
ctx := context.WithValue(r.Context(), "request_id", requestID)
r = r.WithContext(ctx)
// 记录请求开始时间
startTime := time.Now()
// 包装响应写入器
wrapper := &responseWriterWrapper{
ResponseWriter: w,
statusCode: http.StatusOK, // 默认状态码为200
}
// 执行后续业务逻辑
next.ServeHTTP(wrapper, r)
// 计算请求耗时
latency := time.Since(startTime)
// 构建日志对象
webLog := WebLog{
RequestID: requestID,
Method: r.Method,
Path: r.URL.Path,
ClientIP: r.RemoteAddr,
StatusCode: wrapper.statusCode,
Latency: latency,
ReqHeader: r.Header,
RespHeader: wrapper.Header(),
}
// 输出日志,实际项目中可替换为写入文件、发送到日志系统
log.Printf("request_id: %s, method: %s, path: %s, client_ip: %s, status: %d, latency: %v",
webLog.RequestID,
webLog.Method,
webLog.Path,
webLog.ClientIP,
webLog.StatusCode,
webLog.Latency,
)
})
}
4. 中间件使用示例
以标准库的net/http为例,演示如何注册中间件使用:
package main
import (
"fmt"
"log"
"net/http"
"your_project_path/middleware" // 替换为实际的中间件包路径
)
func main() {
// 注册路由
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
})
// 使用Web日志中间件
handler := middleware.WebLogMiddleware(mux)
// 启动服务
log.Println("server start at :8080")
if err := http.ListenAndServe(":8080", handler); err != nil {
log.Fatal(err)
}
}
优化建议
上述实现是基础版本,实际生产环境中可以根据需求做进一步优化:
- 日志输出可以替换为结构化日志,比如使用zap、logrus等日志库,方便后续日志检索和分析
- 对于大流量的服务,可以采用异步写日志的方式,避免日志写入阻塞请求处理
- 可以配置日志采样规则,对高频请求进行采样记录,减少日志存储压力
- 可以结合Trace系统,将请求ID和TraceID关联,实现全链路追踪
注意事项
需要注意响应体的内容如果过大,不建议直接记录到日志中,避免日志文件快速膨胀,同时也要注意不要记录敏感信息,比如用户密码、token等内容,防止信息泄露。
通过上述方案,我们可以在Golang的Web服务中高效管理日志,完整记录每一次请求和响应的关键信息,为服务运维和问题排查提供有力支撑。