2018-03-08
It’s been a couple of years since I’ve been neck-deep in Go code, and
I’m getting up to speed on a few patterns that have gained traction
since the last time. The most interesting one to me is the context
package.
In combination with logrus
, it
enables a logging pattern I’ve been finding very useful.
A context.Context
supports the association of an
arbitrary key/value pair, which follows that context around through your
application. It feels apparent that this is meant to track data such as
an HTTP Request ID, URI path, etc.
The basic form of WithValue
use might look something
like:
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
:= r.Context()
ctx = context.WithValue(ctx, "requestID", newRequestID())
ctx // ...
}
While this would work, the context
documentation
strongly suggests using a non-primitive type for the key, in order to
prevent mixing up conflicting context.Context
types created
by different components of your application, so we can refactor this
to:
type myKey string
var requestIDKey = myKey("requestID")
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
:= r.Context()
ctx = context.WithValue(ctx, requestIDKey, newRequestID())
ctx // ...
}
Now we have the request ID associated to the context
.
What if we wanted to log this ID?
import log "github.com/sirupsen/logrus"
// ...
.WithField("requestID", ctx.Value(requestIDKey)).Info("handling request") log
This obviously gets verbose quickly. What if we also wanted to
associate the URI path to the context
, and log it as well,
along with an error?
if err != nil {
.
log("requestID", ctx.Value(requestIDKey)).
WithField("path", ctx.Value(pathKey)).
WithField("error", err).
WithField("bad thing happened!")
Warn}
We can build a helper in our server
package to deal with
this:
func serverLog(ctx context.Context, err error) *log.Entry {
:= log.WithField("package", "server")
entry if err != nil {
= entry.WithField("error", err)
entry }
if requestID := ctx.Value(requestIDKey); requestID != nil {
= entry.WithField("requestID", requestID)
entry }
if path := ctx.Value(pathKey); path != nil {
= entry.WithField("path", path)
entry }
return entry
}
Then, from elsewhere in the package, we can do:
func arbitrary(ctx context.Context) {
(ctx, nil).Info("attempting flaky operation")
serverLogif err := flakyOperation(ctx); err != nil {
(ctx, err).Warn("operation failed")
serverLog}
}
This would generate logs something like:
INFO[0000] attempting flaky operation \
component=server path=/flaky requestID=124814
WARN[0005] operation failed \
component=server error="timed out" path=/flaky requestID=124814
I’ve found this to be a great pattern to get nice structured logs without a lot of per-invocation overhead. Full example over at gist.github.com