DEV Community

loading...

Logging the status code of a HTTP Handler in Go

Julien
Fullstack dev, interested in developer experience, k8s, go, js
・3 min read

This post shows how to write a logging middleware for HTTP handlers that can log the status code of the response.

HTTP Handlers

First we need to understand the http.Handler interface in Go. This interface implements a single method ServeHTTP and allows us to respond to a HTTP request.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
Enter fullscreen mode Exit fullscreen mode

In our implementation of the ServeHTTP method we write headers and data to the ResponseWriter and return when we have finished dealing with the request.

type GreetingHandler struct{}

func (g GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s", r.RemoteAddr)
}

myHandler := GreetingHandler{}
Enter fullscreen mode Exit fullscreen mode

ResponseWriter implements the io.Writer interface, allowing us to use usual methods from the fmt or io packages to write the response body. To return a status code other than 200, we can call the ResponseWriter. WriteHeader(statusCode int) method.

Since it is cumbersome to always create a new type to attach our ServeHTTP method to, Go provides us with a shortcut: http.HandlerFunc.

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
Enter fullscreen mode Exit fullscreen mode

Note that this is a function type with a method that calls the function itself! This allows us to define a http.Handler without creating a type ourselves:

myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s", r.RemoteAddr)
})
Enter fullscreen mode Exit fullscreen mode

HTTP Middleware

If we want to log every request being handled, we do not want to add logging code inside each of our handlers, instead we can use a middleware. A middleware is a function that wraps a handler to augment it with some extra functionality. The function signature of a middleware is func(http.Handler) http.Handler.

func WithLogging(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        h.ServeHTTP(w, r)
        log.Printf("Handling request for %s from %s", r.URL.Path, r.RemoteAddr)
    }
}
Enter fullscreen mode Exit fullscreen mode

WithLogging takes a http.Handler as its only argument and returns a new http.Handler, augmented with logging functionality.

myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s", r.RemoteAddr)
})
handlerWithLogging := WithLogging(myHandler)
Enter fullscreen mode Exit fullscreen mode

Logging the status code of a request

What if we want to log the status code of each request? Inside our logging middleware, we do not have access to the status code, as it is set inside the wrapped handler and there is no way to retrieve it.

To work around this, we can create a new type that implements ResponseWriter and records the status code, so that we can read it in our logging middleware:

type StatusRecorder struct {
    http.ResponseWriter
    Status int
}

func (r *StatusRecorder) WriteHeader(status int) {
    r.Status = status
    r.ResponseWriter.WriteHeader(status)
}
Enter fullscreen mode Exit fullscreen mode

Our StatusRecorder delegates to an embedded http.ResponseWriter for writing the actual response body and status code, but additionally stores the status code.

This now allows us to read it back inside our logging middleware:

func WithLogging(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        recorder := &Recorder{
            ResponseWriter: w,
            Status:         200,
        }
        h.ServeHTTP(recorder, r)
        log.Printf("Handling request for %s from %s, status: %d", r.URL.Path, r.RemoteAddr, recorder.Status)
    })
}
Enter fullscreen mode Exit fullscreen mode

Complete Example

package main

import (
    "fmt"
    "log"                                                                                                    
    "net/http"
)

type StatusRecorder struct {
    http.ResponseWriter
    Status int
}

func (r *StatusRecorder) WriteHeader(status int) {
    r.Status = status
    r.ResponseWriter.WriteHeader(status)
}

func WithLogging(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        recorder := &StatusRecorder{
            ResponseWriter: w,
            Status:         200,
        }
        h.ServeHTTP(recorder, r)
        log.Printf("Handling request for %s from %s, status: %d", r.URL.Path, r.RemoteAddr, recorder.Status)
    })
}

func main() {
    myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s", r.RemoteAddr)
    })
    handlerWithLogging := WithLogging(myHandler)
    http.Handle("/", handlerWithLogging)
    http.ListenAndServe(":8000", nil)                                                                        
}
Enter fullscreen mode Exit fullscreen mode

And here is a package that provides a ready to use logging middleware to gorilla/mux and sirupsen/logrus: https://github.com/julienp/httplog. The logged field might be a bit specific for my usage, but it should be adaptable for your needs.

Discussion (0)