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

huangapple go评论72阅读模式
英文:

Logging responses to incoming HTTP requests inside http.HandleFunc

问题

这是一个关于如何在Go中检查写入到http.ResponseWriter的HTTP响应的后续问题,原问题链接为https://stackoverflow.com/questions/27983893/in-go-how-to-inspect-the-http-response-that-is-written-to-http-responsewriter/27988044,因为那里的解决方案需要伪造一个请求,在单元测试中效果很好,但在实际服务器上不适用。

我想将我的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()

    // 服务器在这里做一些工作
    // ...

    // 在这里插入调试代码,类似于
    //
    // dump = http.DumpResponseFromWriter(w)
    // fmt.Printf("%s\n", dump)
})
http.Handle("/path", r)
英文:

This is a follow-up question to https://stackoverflow.com/questions/27983893/in-go-how-to-inspect-the-http-response-that-is-written-to-http-responsewriter/27988044 since the solution there requires faking a request, which works great for a unit test but not on a live server.

I would like to dump out HTTP response that my web service is returning in response to requests it receives from users into a log file (or to console). The output should tell me what the headers are and the JSON payload.

How does one go about that?

If there were a httputil.DumpResponse equivalent that takes a http.ResponseWriter as argument rather than http.Response it would be perfect, but currently I can only access the Header from http.ResponseWriter

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)

答案1

得分: 8

中间件链

解决这个问题的常见方法是所谓的“中间件链”。有几个库提供了这个功能,比如 negroni

它是一种延续传递风格,你可以像这样编写你的“中间件”函数(摘自 negroni 的自述文件):

func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  // 在之前做一些事情
  next(rw, r)
  // 在之后做一些事情
}

然后 negroni 会给你一个调用中间件的 HTTP 处理程序,按照正确的顺序调用它们。

我们可以稍微不同地实现这个解决方案,以更少的魔法和更多的函数式(即“函数式编程”)方法。定义如下的处理程序组合器:

func NewFooHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 在之前做一些事情
        next(r,w)
        // 在之后做一些事情
    }
}

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

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) {

        // 将响应写入器替换为记录器
        // 对于所有后续的处理程序
        c := httptest.NewRecorder()
        next(c, r)

        // 将响应记录器中的所有内容
        // 复制到实际的响应写入器
        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 中定义的所有默认内容。

英文:

Middleware Chaining

A common solution to this problem is the so called middleware chain. There are several libraries that provide this functionality e.g. negroni.

It's a form of continuation-passing style where you write your middleware functions like this (taken from negroni's readme):

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

And then negroni gives you an HTTP handler that calls your middlewares in the right order.

We could implement this solution slightly differently to a less magical and more functional (as in functional programming) approach. Define handler combinators as follows:

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
    }
}

Then define your chain as a combination:

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

Now h is an http.HandlerFunc that does foo, then bar, then baz. Sink is just an empty last handler, that does nothing (to "finish" the chain.)

Applying this solution to your problem

Define a handler combinator:

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)

    }
}

Now the problem boils down to handler management. You'll probably want this handler applied to all chains in a certain category. For this, you can use combinators again (this is somewhat equivalent to negroni's Classic() method):

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

After this, whenever you start a chain like this:

h := NewDefaultHandler(...)

It will automatically include response logging and all the default stuff that you defined in NewDefaultHandler.

答案2

得分: 6

这可以通过使用自定义的ServerMux来实现,它不进行路由,而是替换响应写入器,然后将请求转发给普通的mux。由于ResponseWriter只是一个接口,我们可以很容易地模拟它。

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

type DumpResponseWriter struct {
    // 底层的写入器
    w http.ResponseWriter
    // 在这里添加更多用于记录上下文(IP、头部等)的内容
}

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

func (w *DumpResponseWriter) Write(b []byte) (int, error) {
    // 在初始化写入器时,可以添加有关连接的更多上下文,并在此处记录
    log.Println("Writing <更多上下文细节>", string(b))
    return w.w.Write(b)
}

func (w *DumpResponseWriter) WriteHeader(h int) {
    log.Println("Writing Header<更多上下文细节>", h)
    w.w.WriteHeader(h)
}

这样我们的处理函数与之前一样,对于使用“Fake”写入器的事实是不知情的:

func MyHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello world"))
}

然后,我们只需用我们自己的代理mux替换默认的mux,替换写入器并让常规的ServeMux继续处理:

func main() {
    // 我们不使用默认的mux,而是使用自定义的mux
    mux := http.NewServeMux()
    mux.HandleFunc("/", MyHandler)

    // 现在我们拦截每个请求并将其转发给mux进行处理和路由到处理程序
    err := http.ListenAndServe(":1337", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 我们用自己的写入器包装响应写入器。如果需要,可以在这里添加更多上下文到写入器的实例中
        writer := &DumpResponseWriter{w}

        // 然后让我们的普通mux继续处理剩下的事情
        mux.ServeHTTP(writer, r)

        // 在处理程序完成后,我们还可以输出头部信息。不过,它不会打印标准头部
        log.Printf("Response headers: %#v", w.Header())
    }))
    if err != nil {
        panic(err)
    }
}

你可以在这里查看完整的代码示例:http://play.golang.org/p/hT1PCNxI-V

英文:

This can be achieved by using a custom ServerMux that does no routing, but replaces the response writer, and then forwards the request to a normal mux. Since ResponseWriter is just an interface we can fake it easily.

First, we wrap the ResponseWriter interface with our own response writer, that will log everything and pass all functionality to a real response writer:

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(&quot;Writing &lt; more context details here&gt; &quot;, string(b) )
        return w.w.Write(b)
}

       
func (w *DumpResponseWriter)WriteHeader(h int) {
    log.Println(&quot;Writing Header&lt; more context details here&gt; &quot;, h)
    w.w.WriteHeader(h)
}

This leaves our handler func the same as before, and agnostic to the fact that we're using a "Fake" writer...

func MyHandler(w http.ResponseWriter, r *http.Request) {
    
    w.Write([]byte(&quot;Hello world&quot;))
}

And then we simply replace the default mux with our own proxy mux, that replaces the writer and lets a regular ServeMux do its thing:

func main(){

    // we don&#39;t use the default mux, but a custom one
    mux := http.NewServeMux()
    mux.HandleFunc(&quot;/&quot;, MyHandler)
	
    // now we intercept each request and forward it to the mux to do    the routing to the handlers.
    err := http.ListenAndServe(&quot;:1337&quot;, 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&#39;s instance
        writer := &amp;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(&quot;Response headers: %#v&quot;, w.Header())
    
    }))
    if err != nil {
        panic(err)
    }
}

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

答案3

得分: 2

使用httptest.ResponseRecorder实现Mat Ryer的方法,基于请求ID记录日志。

使用httptest.ResponseRecorder的缺点:

  • 仅支持HTTP/1.1
  • 不支持ReadFrom()、Hijack()、Flush()等方法
  • Recorder中不可用的头部字段,如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(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)
		})
	}
}
英文:

Implementing Mat Ryer's approach with logging request id based on httptest.ResponseRecorder

Disadvantages of using httptest.ResponseRecorder:

  • HTTP/1.1 only
  • Doesn't supports ReadFrom(), Hijack(), Flush() maybe something else
  • Headers like Content-Length and Date are not available in recorder

Code:

import (
&quot;fmt&quot;
&quot;github.com/google/uuid&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;net/http/httptest&quot;
&quot;net/http/httputil&quot;
&quot;strings&quot;
)
func main() {
logger := log.New(os.Stdout, &quot;server: &quot;, log.Lshortfile)
http.HandleFunc(&quot;/api/smth&quot;, Adapt(smth, httpLogger(quips.logger)))
panic(http.ListenAndServe(&quot;:8080&quot;, 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 &gt; 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(&quot;&lt;&lt;&lt;&lt;&lt; Request %s\n%s\n&lt;&lt;&lt;&lt;&lt;&quot;, reqId.String(), string(dump))
recorder := httptest.NewRecorder()
defer func() {
var sb strings.Builder
fmt.Fprintf(&amp;sb, &quot;%s %d\n&quot;, recorder.Result().Proto, recorder.Result().StatusCode)
for h, v := range recorder.Result().Header {
w.Header()[h] = v
for _, headerValue := range v {
fmt.Fprintf(&amp;sb, &quot;%s: %s\n&quot;, h, headerValue)
}
}
w.Header().Set(&quot;X-Request-Id&quot;, reqId.String())
fmt.Fprintf(&amp;sb, &quot;X-Request-Id: %s\n&quot;, reqId.String())
fmt.Fprintf(&amp;sb, &quot;Content-Length: %d\n&quot;, recorder.Body.Len())
fmt.Fprint(&amp;sb, &quot;\n&quot;)
sb.Write(recorder.Body.Bytes())
logger.Printf(&quot;&gt;&gt;&gt;&gt;&gt; Response %s\n%s\n&gt;&gt;&gt;&gt;&gt;&quot;, reqId.String(), sb.String())
w.WriteHeader(recorder.Result().StatusCode)
recorder.Body.WriteTo(w)
}()
h.ServeHTTP(recorder, r)
})
}
}

huangapple
  • 本文由 发表于 2015年3月29日 00:45:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/29319783.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定