I’ve slowly but surely been learning Go over the past few
months, and I recently began working on a web service in my spare time. Go comes
with a great builtin package for building web services, net/http, and
in this post I’ll cover how to wrap the http.ResponseWriter
type to capture
the HTTP status code from the response. This is useful if you need to log the
status code after the response handler has completed. If you need to keep track
of any other part of the response, this approach can easily be extended.
Getting started
To get things started, let’s put together a very basic web service that responds
with a 404 Not Found
to all requests except requests for the root:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handleRoot)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleRoot(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
http.NotFound(w, req)
return
}
fmt.Fprintf(w, "Hello, World!")
}
After starting our service using go run main.go
, navigating to
http://localhost:8080/ should present us with "Hello, World!"
. If we navigate
to any other
path, we should get a 404.
Before we continue, take a moment to appreciate how easy it was to write that.
We didn’t even have to install a gem
or a pod
or an egg
!
Adding logging
Now, to make things more interesting, we decide that we should log all requests.
Our first idea is to create a function that takes a http.Handler
and wraps it,
returning a new handler that logs some information about the request before
invoking the wrapped handler:
func wrapHandlerWithLogging(wrappedHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
log.Printf("--> %s %s", req.Method, req.URL.Path)
wrapperHandler.ServeHTTP(w, req)
})
}
func main() {
rootHandler := wrapHandlerWithLogging(http.HandlerFunc(handleRoot))
http.Handle("/", rootHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
This works just as one would expect. If we make a few requests to our server, we will see something along the following lines in our terminal:
$ go run main.go
2016/03/02 21:44:42 --> GET /
2016/03/02 21:46:18 --> GET /foo
2016/03/02 21:46:20 --> GET /bar
2016/03/02 21:46:21 --> GET /baz
Pretty cool! However, it would be even cooler if we could log the response as
well. We figure we can just add another log.Printf
after the call to
wrappedHandler.ServeHTTP
to log the things we are interested in. An important
and informative part of the response is the HTTP status code, so we look through
the documentation for http.ResponseWriter to find out how to
retrieve the status code. But wait… we quickly realize that http.ResponseWriter
doesn’t expose the status code! Will this be where our adventure with net.http
ends?
Of course not! After a closer look, we see that ResponseWriter.WriteHeader
takes the status code as an argument. If we could somehow intercept that method,
then we could store the status code and use it for logging later.
This is in fact possible, and quite easy to do at that. By creating a new struct
that embeds an http.ResponseWriter
we can implement the
WriteHeader
method to store the status code in our new struct:
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{w, http.StatusOK}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
When we have our new loggingResponseWriter
type, we simply need to make sure
that our wrapped handler is passed a loggingResponseWriter
instead of the
standard http.ResponseWriter
:
func wrapHandlerWithLogging(wrappedHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
log.Printf("--> %s %s", req.Method, req.URL.Path)
lrw := NewLoggingResponseWriter(w)
wrappedHandler.ServeHTTP(lrw, req)
statusCode := lrw.statusCode
log.Printf("<-- %d %s", statusCode, http.StatusText(statusCode))
})
}
If we run our service again and make some requests we will see that the HTTP status code from the response is now logged:
$ go run main.go
2016/03/03 20:17:53 --> GET /
2016/03/03 20:17:53 <-- 200 OK
2016/03/03 20:17:56 --> GET /foo
2016/03/03 20:17:56 <-- 404 Not Found
2016/03/03 20:17:57 --> GET /bar
2016/03/03 20:17:57 <-- 404 Not Found
Just what we wanted!
Summary
When I began working on this problem I was afraid that the code required to solve the problem would be complex and difficult to understand. However, it turns out that capturing the HTTP status code (or any other data, for that matter) from the response is quite simple and easy to reason about.
The approach discussed in this post gives a brief glimpse into how powerful
wrapping an http.Handler
can be by making it easy to add functionality on top
of existing handlers.
Extending this approach to things like authorization is a natural next step, and
combining it with x/net/context
makes it even more powerful.
If you want to try out the code with minimal effort I’ve created a gist with the entirety of the code outlined in this post. It also includes some small comments about minor details that might not be entirely obvious from the post.
Until next time!