Loading...
Loading...
Marco Franssen

In a previous post, I showed how to create a Go web server with graceful shutdown. This time, let's make that boilerplate easier to reuse as a drop-in server.go file, with chi for routing and zap for structured logging.
In the earlier example, I put everything in main.go. There's nothing wrong with that, but after building microservices for a while, I found it more convenient to keep main.go clean and make the server boilerplate easier to copy into a new project. Since each microservice has different setup needs, I now use main.go only to bootstrap packages and components such as the web server.
That leaves us with a small main.go that delegates the work to other files and packages and focuses on bootstrapping the application. Here's an example.
Loading improved-graceful-webserver/main.go…
If you're new to Go, start with this post, where I explain how to set up a Go development environment and initialize a project. If you're already familiar with Go, keep reading.
Now let's look at implementing the web server in server.go, so we can drop it into a new project as boilerplate. You don't necessarily need third-party libraries for a simple web server. For convenience and development speed, though, I chose two libraries with strong benchmark results and supportive communities.
The first is chi, a lightweight routing library for web servers and RESTful APIs. It ships with middleware and makes it easy to add your own. Later, I'll show you the zap.Logger middleware I created to log requests through chi.
That brings us to the second library: zap, created by Uber. It's a high-performance logger with logging levels and configurable backends for structured output. You can use those logs with tools such as Elasticsearch and Kibana.
I've included links to both libraries at the end of this post.
Let's start with NewServer, the function that returns our server struct.
package main
// Server provides an http.Server
type Server struct {
l *zap.Logger
*http.Server
}
// NewServer creates and configures a server serving all application routes.
//
// The server implements a graceful shutdown and utilizes zap.Logger for logging purposes.
// chi.Mux is used for registering some convenient middlewares and easy configuration of
// routes using different http verbs.
func NewServer(listenAddr string) (*Server, error) {
logger, err := zap.NewDevelopment(zap.AddStacktrace(zapcore.FatalLevel))
if err != nil {
log.Fatalf("Can't initialize zap logger: %v", err)
}
defer logger.Sync()
logger.Info("Configure API")
api := newAPI(logger)
errorLog, _ := zap.NewStdLogAt(logger, zap.ErrorLevel)
srv := http.Server{
Addr: listenAddr,
Handler: api,
ErrorLog: errorLog,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
return &Server{logger, &srv}, nil
}Here, we create a zap.Logger with the NewDevelopment factory function. Zap supports more complex, fine-grained configuration, but I generally use this simple setup. We also call newAPI(logger) to create a chi.Mux, where we define our web server's endpoints, such as those for a RESTful API. Here's that function.
func newAPI(logger *zap.Logger) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(zapLogger(logger))
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
// register more routes over here...
logRoutes(r, logger)
return r
}Here, we register / to respond to a GET request with HTTP status OK (200), and /ping to return “pong”. We also register middleware that ships with chi, along with my zapLogger middleware for logging requests with zap.Logger. Finally, we log the registered routes at DEBUG level. That's helpful when working on larger APIs with many routes and HTTP verbs.
Below is my zapLogger middleware. It uses the RequestID middleware to include the request ID in each log entry. I'm also using a handy defer trick to calculate request latency. The deferred function runs after next.ServeHTTP(ww, r) returns, once execution has passed through the rest of the middleware stack and your endpoint code. That lets us log how long the downstream handler took to process the request. You can read more about defer in Go in this post.
func zapLogger(l *zap.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
l.Info("Served",
zap.String("proto", r.Proto),
zap.String("path", r.URL.Path),
zap.Duration("lat", time.Since(t1)),
zap.Int("status", ww.Status()),
zap.Int("size", ww.BytesWritten()),
zap.String("reqId", middleware.GetReqID(r.Context())),
)
}()
next.ServeHTTP(ww, r)
})
}
}
func logRoutes(r *chi.Mux, logger *zap.Logger) {
if err := chi.Walk(r, zapPrintRoute(logger)); err != nil {
logger.Error("Failed to walk routes:", zap.Error(err))
}
}
func zapPrintRoute(logger *zap.Logger) chi.WalkFunc {
return func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
route = strings.Replace(route, "/*/", "/", -1)
logger.Debug("Registering route", zap.String("method", method), zap.String("route", route))
return nil
}
}The logRoutes function above uses chi.Walk to visit the registered routes and log them with zap.Logger at DEBUG level. This helps you identify the exact endpoints when debugging a complex router.
Finally, let's look at graceful shutdown, which I've simplified slightly compared with my earlier example.
// Start runs ListenAndServe on the http.Server with graceful shutdown
func (srv *Server) Start() {
srv.l.Info("Starting server...")
defer srv.l.Sync()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
srv.l.Fatal("Could not listen on", zap.String("addr", srv.Addr), zap.Error(err))
}
}()
srv.l.Info("Server is ready to handle requests", zap.String("addr", srv.Addr))
srv.gracefulShutdown()
}
func (srv *Server) gracefulShutdown() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
sig := <-quit
srv.l.Info("Server is shutting down", zap.String("reason", sig.String()))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.SetKeepAlivesEnabled(false)
if err := srv.Shutdown(ctx); err != nil {
srv.l.Fatal("Could not gracefully shutdown the server", zap.Error(err))
}
srv.l.Info("Server stopped")
}We run the web server in a goroutine, then wait on a signal channel for Ctrl+C before shutting down gracefully.
We've reached the complete example. Below is the full server.go file.
Loading improved-graceful-webserver/server.go…
You should now be able to build and run your web server as follows, assuming you've created main.go and server.go and initialized your Go module.
$ go build . && ./improved-graceful-webserver.exe
2019-07-04T23:04:55.775+0200 INFO improved-graceful-webserver/server.go:37 Configure API
2019-07-04T23:04:55.775+0200 DEBUG improved-graceful-webserver/server.go:119 Registering route {"method": "GET", "route": "/"}
2019-07-04T23:04:55.776+0200 DEBUG improved-graceful-webserver/server.go:119 Registering route {"method": "GET", "route": "/ping"}
2019-07-04T23:04:55.776+0200 INFO improved-graceful-webserver/server.go:55 Starting server...
2019-07-04T23:04:55.798+0200 INFO improved-graceful-webserver/server.go:63 Server is ready to handle requests {"addr": ":5000"}
2019-07-04T23:05:10.371+0200 INFO improved-graceful-webserver/server.go:95 Served {"proto": "HTTP/1.1", "path": "/", "lat": "0s", "status": 200, "size": 0, "reqId": "DESKTOP-B249DU4/DBQUBZ6RNh-000001"}
2019-07-04T23:05:15.187+0200 INFO improved-graceful-webserver/server.go:95 Served {"proto": "HTTP/1.1", "path": "/ping", "lat": "0s", "status": 200, "size": 4, "reqId": "DESKTOP-B249DU4/DBQUBZ6RNh-000002"}
2019-07-04T23:05:20.795+0200 INFO improved-graceful-webserver/server.go:129 Server is shutting down {"reason": "interrupt"}
2019-07-04T23:05:20.797+0200 INFO improved-graceful-webserver/server.go:138 Server stoppedAs promised, here are links to the Go documentation for chi and zap. I've also included a zip file with the complete example to download and run locally. If you don't have Go installed, use the included Dockerfile and docker-compose file to run it with:
docker-compose up --buildThanks for making it to the end! Please leave some feedback and share this post with your friends and colleagues. Want to read more? Check out my other posts on Go.
package main
import (
"flag"
)
var (
listenAddr string
)
func main() {
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
flag.Parse()
// bootstrap any other packages or components here.
// things like database connectivity or domain services
// you can inject this as a dependency on your webserver if required
// by adding another parameter to the NewServer func.
// Now I left this for brevity.
server, err := NewServer(listenAddr)
if err != nil {
panic(err)
}
server.Start()
}package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Server provides an http.Server
type Server struct {
l *zap.Logger
*http.Server
}
// NewServer creates and configures a server serving all application routes.
//
// The server implements a graceful shutdown and utilizes zap.Logger for logging purposes.
// chi.Mux is used for registering some convenient middlewares and easy configuration of
// routes using different http verbs.
func NewServer(listenAddr string) (*Server, error) {
logger, err := zap.NewDevelopment(zap.AddStacktrace(zapcore.FatalLevel))
if err != nil {
log.Fatalf("Can't initialize zap logger: %v", err)
}
defer logger.Sync()
logger.Info("Configure API")
api := newAPI(logger)
errorLog, _ := zap.NewStdLogAt(logger, zap.ErrorLevel)
srv := http.Server{
Addr: listenAddr,
Handler: api,
ErrorLog: errorLog,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
return &Server{logger, &srv}, nil
}
// Start runs ListenAndServe on the http.Server with graceful shutdown
func (srv *Server) Start() {
srv.l.Info("Starting server...")
defer srv.l.Sync()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
srv.l.Fatal("Could not listen on", zap.String("addr", srv.Addr), zap.Error(err))
}
}()
srv.l.Info("Server is ready to handle requests", zap.String("addr", srv.Addr))
srv.gracefulShutdown()
}
func newAPI(logger *zap.Logger) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(zapLogger(logger))
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
// register more routes over here...
logRoutes(r, logger)
return r
}
func zapLogger(l *zap.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
l.Info("Served",
zap.String("proto", r.Proto),
zap.String("path", r.URL.Path),
zap.Duration("lat", time.Since(t1)),
zap.Int("status", ww.Status()),
zap.Int("size", ww.BytesWritten()),
zap.String("reqId", middleware.GetReqID(r.Context())),
)
}()
next.ServeHTTP(ww, r)
})
}
}
func logRoutes(r *chi.Mux, logger *zap.Logger) {
if err := chi.Walk(r, zapPrintRoute(logger)); err != nil {
logger.Error("Failed to walk routes:", zap.Error(err))
}
}
func zapPrintRoute(logger *zap.Logger) chi.WalkFunc {
return func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
route = strings.Replace(route, "/*/", "/", -1)
logger.Debug("Registering route", zap.String("method", method), zap.String("route", route))
return nil
}
}
func (srv *Server) gracefulShutdown() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
sig := <-quit
srv.l.Info("Server is shutting down", zap.String("reason", sig.String()))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.SetKeepAlivesEnabled(false)
if err := srv.Shutdown(ctx); err != nil {
srv.l.Fatal("Could not gracefuly shutdown the server", zap.Error(err))
}
srv.l.Info("Server stopped")
}Marco Franssen
Flash a Raspbian image with Etcher, enable SSH, update packages, and configure Wi-Fi with a static IP in this Raspberry Pi Stretch setup walkthrough.
Marco Franssen
Follow my Raspberry Pi upgrade from Raspbian Stretch to Buster: update packages, change APT sources, resolve broken packages, clean up, and verify the release.
Marco Franssen
Build a Go API with the Elasticsearch 7 client, run a local cluster with Docker Compose, and add index creation and search to a practical demo.
Marco Franssen
Build a small Go Docker image with a multi-stage build, run it with Docker Compose, and use Traefik 1.7 to route requests and scale your local web service.