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

Golang微服务日志包开发:响应对象日志记录方法求助

Go微服务日志包:完整记录HTTP请求/响应的实现方案

一、HTTP Handler场景:记录响应状态码、耗时、响应体

默认http.ResponseWriter不暴露响应体和状态码,需要自定义封装一个实现该接口的结构体,来捕获这些信息。结合gorilla/mux的中间件,可以实现全局日志记录,无需关心具体请求URL。

1. 自定义LoggingResponseWriter

import (
    "bytes"
    "net/http"
)

type LoggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
    body       bytes.Buffer
}

func NewLoggingResponseWriter(w http.ResponseWriter) *LoggingResponseWriter {
    return &LoggingResponseWriter{w, http.StatusOK, bytes.Buffer{}}
}

// 重写WriteHeader,记录状态码
func (lrw *LoggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

// 重写Write,记录响应体
func (lrw *LoggingResponseWriter) Write(b []byte) (int, error) {
    lrw.body.Write(b)
    return lrw.ResponseWriter.Write(b)
}

2. 实现日志中间件

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
    "time"
    "github.com/gorilla/mux"
)

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        lrw := NewLoggingResponseWriter(w)
        
        // 执行后续业务handler
        next.ServeHTTP(lrw, r)
        
        // 读取请求体(需重新放回,避免后续handler无法读取)
        reqBody := readRequestBody(r)
        
        // 组装日志数据
        logData := map[string]interface{}{
            "request_method": r.Method,
            "request_url":    r.URL.String(),
            "status_code":    lrw.statusCode,
            "duration_ms":    time.Since(start).Milliseconds(),
            "request_body":   reqBody,
            "response_body":  lrw.body.String(),
        }
        
        // 转为JSON并发送到Elasticsearch
        jsonBytes, err := json.Marshal(logData)
        if err != nil {
            log.Printf("日志序列化失败: %v", err)
            return
        }
        // 这里替换为你的ES发送逻辑,比如用ES客户端或HTTP POST
        // sendToES(jsonBytes)
    })
}

// 读取请求体并重置Body
func readRequestBody(r *http.Request) string {
    if r.Body == nil {
        return ""
    }
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        log.Printf("读取请求体失败: %v", err)
        return ""
    }
    r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
    return string(bodyBytes)
}

3. 接入gorilla/mux

func main() {
    r := mux.NewRouter()
    // 全局注册日志中间件
    r.Use(LoggingMiddleware)
    
    // 注册业务handler
    r.HandleFunc("/api/user", userHandler)
    
    http.ListenAndServe(":8080", r)
}

这个中间件会自动处理所有经过路由的请求,无需提前知晓具体URL即可记录完整响应信息。

二、主动发起HTTP请求场景(http.Get等):记录响应信息

使用http.Get/http.Post等方法时,返回的*http.Response包含了状态码、响应体等所有信息,只需读取响应体并备份即可。

import (
    "bytes"
    "encoding/json"
    "io"
    "log"
    "net/http"
    "time"
)

func logOutgoingRequest(url string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        log.Printf("请求失败: %v", err)
        return
    }
    defer resp.Body.Close()
    
    // 读取响应体并备份
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Printf("读取响应体失败: %v", err)
        return
    }
    // 重置Body,方便后续复用
    resp.Body = io.NopCloser(bytes.NewBuffer(respBody))
    
    // 组装日志数据
    logData := map[string]interface{}{
        "request_url":    url,
        "request_method": "GET",
        "status_code":    resp.StatusCode,
        "duration_ms":    time.Since(start).Milliseconds(),
        "response_body":  string(respBody),
    }
    
    // 转为JSON发送到ES
    jsonBytes, err := json.Marshal(logData)
    if err != nil {
        log.Printf("日志序列化失败: %v", err)
        return
    }
    // sendToES(jsonBytes)
}

如果是POST/PUT等带请求体的请求,需要在发起请求前提前备份请求体内容,避免发送后无法读取。

关键注意事项

  • 自定义LoggingResponseWriter时,若业务中用到http.ResponseWriter的其他方法(如Header()),需确保结构体正确转发调用。
  • 读取请求/响应体后必须重置Body,否则后续处理会因流已关闭而失败。
  • 发送日志到Elasticsearch建议用异步方式(如goroutine+缓冲channel),避免阻塞HTTP请求处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 16:01:15