Go - 在http.HandleFunc中记录对传入HTTP请求的响应

时间:2015-03-28 16:45:04

标签: http go

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

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

如何做到这一点?

如果有一个httputil.DumpResponse等效,它将http.ResponseWriter作为参数而不是http.Response,那将是完美的,但目前我只能从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)

3 个答案:

答案 0 :(得分:7)

中间件链接

此问题的常见解决方案是所谓的中间件链。有几个库提供此功能,例如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是foo的http.HandlerFunc,然后是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中定义的所有默认内容。

答案 1 :(得分:6)

这可以通过使用不进行路由的自定义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)
}

这使得我们的处理程序功能与以前一样,并且不知道我们正在使用&#34; Fake&#34;作家...

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

答案 2 :(得分:0)

使用基于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)
        })
    }
}