【问题标题】:Logging responses to incoming HTTP requests inside http.HandleFunc在 http.HandleFunc 中记录对传入 HTTP 请求的响应
【发布时间】:2015-03-28 16:45:03
【问题描述】:

这是In go, how to inspect the http response that is written to http.ResponseWriter? 的后续问题,因为那里的解决方案需要伪造请求,这对于单元测试非常有效,但不适用于实时服务器。

我想将我的 Web 服务为响应它从用户接收到的请求而返回的 HTTP 响应转储到日志文件(或控制台)中。输出应该告诉我标头是什么以及 JSON 有效负载。

如何做到这一点?

如果有一个以 http.ResponseWriter 而不是 http.Response 作为参数的 httputil.DumpResponse 等价物,那将是完美的,但目前我只能从 http.ResponseWriter 访问 Header

r = mux.NewRouter()
r.HandleFunc("/path", func (w http.ResponseWriter, r *http.Request) {

    fmt.Printf("r.HandleFunc /path\n")

    resp := server.NewResponse()
    defer resp.Close()

    r.ParseForm()

    // Server does some work here
    // ...

    // Insert debug code here, something like
    //
    // dump = http.DumpResponseFromWriter(w)
    // fmt.Printf("%s\n", dump)
});
http.Handle("/path", r)

【问题讨论】:

  • 如果您可以将net.Conn 包装在发送到网络但也写入日志文件或缓冲区的内容中,那就太好了。但是我看不到您可以在net/http 管道中插入它。如果本周末没有人提出任何建议,这可能是 golang-nuts 的问题。

标签: http go


【解决方案1】:

中间件链接

这个问题的一个常见解决方案是所谓的中间件链。有几个库提供此功能,例如negroni.

这是一种延续传递样式,您可以在其中编写像这样的 中间件 函数(取自 negroni 的自述文件):

func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  // do some stuff before
  next(rw, r)
  // do some stuff after
}

然后 negroni 会为您提供一个 HTTP 处理程序,以正确的顺序调用您的中间件。

我们可以以稍微不同的方式来实现这个解决方案,以使用一种不那么神奇但功能更强大(如函数式编程)的方法。如下定义处理程序组合器:

func NewFooHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // do some stuff before
        next(r,w)
        // do some stuff after
    }
}

然后将你的链定义为一个组合:

h := NewFooHandler(NewBarHandler(NewBazHandler(Sink)))

现在h 是一个http.HandlerFunc,它执行 foo,然后是 bar,然后是 baz。 Sink 只是一个空的最后一个处理程序,它什么都不做(“完成”链。)

将此解决方案应用于您的问题

定义处理程序组合器:

func NewResponseLoggingHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {

        // switch out response writer for a recorder
        // for all subsequent handlers
        c := httptest.NewRecorder()
        next(c, r)

        // copy everything from response recorder
        // to actual response writer
        for k, v := range c.HeaderMap {
            w.Header()[k] = v
        }
        w.WriteHeader(c.Code)
        c.Body.WriteTo(w)

    }
}

现在问题归结为处理程序管理。您可能希望将此处理程序应用于某个类别中的所有链。为此,您可以再次使用组合器(这在某种程度上相当于 negroni 的 Classic() 方法):

func NewDefaultHandler(next http.HandlerFunc) http.HandlerFunc {
    return NewResponseLoggingHandler(NewOtherStuffHandler(next))
}

在此之后,每当您启动这样的链时:

h := NewDefaultHandler(...)

它将自动包含响应日志和您在NewDefaultHandler 中定义的所有默认内容。

【讨论】:

  • 谢谢,这是我最终采用的技术,因为它允许我挑选特定的 URL 进行监控,减去使用 negroni。虽然我需要做一些调整:next(w, c) ––> next(c, r)for k, v := c.HeaderMap {...} –––> for k, v := range c.HeaderMap {...}。干杯!
  • 哦,对不起。我对我的答案进行了更正。谢谢!
【解决方案2】:

这可以通过使用自定义ServerMux 来实现,该ServerMux 不进行路由,而是替换响应编写器,然后将请求转发到普通多路复用器。由于 ResponseWriter 只是一个接口,我们可以轻松地伪造它。

首先,我们用我们自己的响应编写器包装 ResponseWriter 接口,它将记录所有内容并将所有功能传递给真正的响应编写器:

type DumpResponseWriter struct {
    // the underlying writer
    w http.ResponseWriter
    // more stuff you want to use for logging context (ip, headers, etc) here
}


func (w *DumpResponseWriter)Header() http.Header {
    return w.w.Header()
}

func (w *DumpResponseWriter)Write(b []byte) (int, error) {
        // You can add more context about the connection when initializing the writer and log it here
        log.Println("Writing < more context details here> ", string(b) )
        return w.w.Write(b)
}


func (w *DumpResponseWriter)WriteHeader(h int) {
    log.Println("Writing Header< more context details here> ", h)
    w.w.WriteHeader(h)
}

这使我们的处理程序功能与以前相同,并且不知道我们正在使用“假”编写器这一事实......

func MyHandler(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("Hello world"))
}

然后我们只需用我们自己的代理多路复用器替换默认多路复用器,它会替换编写器并让常规的 ServeMux 做它的事情:

func main(){

    // we don't use the default mux, but a custom one
    mux := http.NewServeMux()
    mux.HandleFunc("/", MyHandler)

    // now we intercept each request and forward it to the mux to do    the routing to the handlers.
    err := http.ListenAndServe(":1337", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // we wrap the response writer with our own. Add more context here if you want to the writer's instance
        writer := &DumpResponseWriter{w}

        // and we let our ordinary mux take care of things from here
        mux.ServeHTTP(writer, r)

        // We can also dump the headers after the handler is done. It will not print the standard headers though
        log.Printf("Response headers: %#v", w.Header())

    }))
    if err != nil {
        panic(err)
    }
}

http://play.golang.org/p/hT1PCNxI-V

【讨论】:

    【解决方案3】:

    使用基于 httptest.ResponseRecorder 的日志记录请求 ID 实现 Mat Ryer 的 approach

    使用httptest.ResponseRecorder的缺点:

    • 仅限 HTTP/1.1
    • 不支持 ReadFrom()、Hijack()、Flush() 可能还有其他功能
    • Content-LengthDate 等标题在记录器中不可用

    代码:

    import (
        "fmt"
        "github.com/google/uuid"
        "log"
        "net/http"
        "net/http/httptest"
        "net/http/httputil"
        "strings"
    )
    
    func main() {
        logger := log.New(os.Stdout, "server: ", log.Lshortfile)
        http.HandleFunc("/api/smth", Adapt(smth, httpLogger(quips.logger)))
        panic(http.ListenAndServe(":8080", nil))
    }
    
    type Adapter func(http.HandlerFunc) http.HandlerFunc
    
    func Adapt(h http.HandlerFunc, adapters ...Adapter) http.HandlerFunc {
        for _, adapter := range adapters {
            h = adapter(h)
        }
        return h
    }
    
    func httpLogger(logger *log.Logger) Adapter {
        return func(h http.HandlerFunc) http.HandlerFunc {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                dumpBody := true
                if r.ContentLength > 1024 {
                    dumpBody = false
                }
                dump, err := httputil.DumpRequest(r, dumpBody)
                if err != nil {
                    http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
                    return
                }
    
                reqId, err := uuid.NewRandom()
                if err != nil {
                    http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
                    return
                }
    
                logger.Printf("<<<<< Request %s\n%s\n<<<<<", reqId.String(), string(dump))
    
                recorder := httptest.NewRecorder()
                defer func() {
                    var sb strings.Builder
                    fmt.Fprintf(&sb, "%s %d\n", recorder.Result().Proto, recorder.Result().StatusCode)
    
                    for h, v := range recorder.Result().Header {
                        w.Header()[h] = v
                        for _, headerValue := range v {
                            fmt.Fprintf(&sb, "%s: %s\n", h, headerValue)
                        }
                    }
                    w.Header().Set("X-Request-Id", reqId.String())
                    fmt.Fprintf(&sb, "X-Request-Id: %s\n", reqId.String())
                    fmt.Fprintf(&sb, "Content-Length: %d\n", recorder.Body.Len())
                    fmt.Fprint(&sb, "\n")
                    sb.Write(recorder.Body.Bytes())
    
                    logger.Printf(">>>>> Response %s\n%s\n>>>>>", reqId.String(), sb.String())
    
                    w.WriteHeader(recorder.Result().StatusCode)
                    recorder.Body.WriteTo(w)
                }()
                h.ServeHTTP(recorder, r)
            })
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2017-02-12
      • 2016-11-21
      • 2018-10-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-25
      • 2013-02-11
      • 1970-01-01
      相关资源
      最近更新 更多