Using Go's 'context' library for performance monitoring

Just about any server application with users is an application with impatient users. You need to keep on top of performance of your application. If you have an HTTP handler, you can just go and do some simple timing of start and end of the request and log it. Then you can analyze the logs, and look for slow endpoints.

But to dig deeper, inevitably you need to collect information which lets you triage the performance issue. Is it time spent waiting for a service? Is it time spend doing real work? Is it the database? Is it lupus?

One such library that solves these problems nicely is newrelic’s go agent library. It’s not the only approach in the go community, but it is a gold standard in terms of feature set and efficiency of data collection. A surprising amount of information is tallied, aggregated in your program and lazily sent to newrelic for later useful insights.

Whatever profiling library you use, hopefully you’ll find the use of this library as an illustration helpful in solving this concern for yourself.

Passing profiling objects via context, or: the missing guide to using newrelic’s go-agent in practice

The examples and guide tell you to set up an application, and then wrap your main handler function with it. Already, this has achieved the simple timing of the request’s start and stop, and gives you a fancy dashboard to boot. But it scratches the surface of the library’s ability to help answer that question: why is my endpoint slow?

This library has the same fundamental problem as logging does when it comes to its use. There’s an object it wants to pass down to users, but there is no clear way to do it and you can’t use globals. The agent sneaks its way in by wrapping the http.ResponseWriter in its own interface type. To get it out, you use:

    func myHandler(w http.ResponseWriter, r *http.Request) {
        if txn, ok := w.(newrelic.Transaction); ok {
            txn.NoticeError(errors.New("my error message"))
        }
    }

Very clever, newrelic. This certainly gets around the fact that the standard net/http handler function API does not allow any extra auxilliary stack arguments.

But what about if you have several layers of abstraction between the handler, and the service call (eg, database call) you want to time? Pass that http.ResponseWriter around? The newrelic.Transaction object?

The answer is, naturally, context.

Saving and Retrieving profiling objects

Again, let’s make a couple of functions to get newrelic things in and out of the context:

    package profiling
    
    import (
        "context"
    
        newrelic "github.com/newrelic/go-agent"
    )
    
    type profilingKeyType int
    const (
        appKey profilingKeyType = iota
        txKey
    )
    
    // NewContext adds a newrelic thingy to the context
    func NewContext(
        ctx context.Context, thing interface{},
    ) context.Context {
        var key profilingKeyType
        switch thing := thing.(type) {
            case newrelic.Application:
                key = appKey
            case newrelic.Transaction:
                key = txKey
            default:
                return ctx
        }
        return context.WithValue(ctx, key, thing)
    }
    
    // GetApp returns a stashed newrelic.Application from the context
    func GetApp(ctx context.Context) newrelic.Application {
        if nrApp, ok := ctx.Value(appKey).(newrelic.Application); ok {
            return nrApp
        } else {
            return nil
        }
    }

    // GetTx returns a stashed newrelic.Transaction from the context
    func GetTx(ctx context.Context) newrelic.Transaction {
        if nrTx, ok := ctx.Value(appKey).(newrelic.Transaction); ok {
            return nrTx
        } else {
            return nil
        }
    }

Timing your database queries

In your model code, you can now access the newrelic objects and time your database calls appropriately. First we’ll need a common function and some types in your model class.

    package mymodels
    
    type Ender interface {
        End()
    }

    type NoopEnder struct {}
    func (*NoopEnder) End() {}
    var noopEnder = NoopEnder{}
    
    // TraceOp allows the definition of a datastore segment to be
    // referenced with a single data structure
    type TraceOp struct {
	    Collection string
	    Operation  string
    }

    // StartTrace takes a context, and if newrelic is detected, will
    // return a started newrelic.DatastoreSegment representing the
    // specified TraceOp.  Otherwise, returns a no-op Ender
    func StartTrace(ctx context.Context, traceOp TraceOp) Ender {
	    if txn := profiling.GetTx(ctx); txn != nil {
		    ds := &newrelic.DatastoreSegment{
			    Product:    newrelic.DatastorePostgres,
			    Collection: traceOp.Collection,
			    Operation:  traceOp.Operation,
		    }
		    ds.StartTime = newrelic.StartSegmentNow(txn)
		    return ds
	    } else {
		    return &noopEnder
	    }
    }

With that established, in the model code, you can augment your model functions with a single line call to this StartTrace function:

    var MyModelFuncTrace = TraceOp{
        Collection: "Model",
        Operation: "Func",
    }
    
    func MyModelFunc(ctx context.Context, arg int) {
        defer StartTrace(ctx, &MyModelFuncTrace).End()
        modelFuncStmt.Exec(arg)
    }

One important characteristic is that regardless of whether the passed-in function had a newrelic transaction in it or not, the method can succeed. But if it does, you’ll get a nice attribution of some time to the “Func on Model” postgres database operation.

So, if you can connect all the call stacks to the model function with ctx calls, then you can get tracing. If you don’t, then you don’t get tracing. This gives a nice, gradual path for enhancing your code base to pass context around.

Unless of course, you were already passing it all around for the logging based on part 1 of this blog series!

Slave goroutines and passing a root context into the application

For various reasons (my guess is performance and model simplicity), newrelic’s transaction are not to be shared between goroutines. This means that if your program starts a new goroutine, it needs to create a new newrelic.Transaction for that goroutine. To do that, it needs the newrelic.Application instance. But if you followed the examples, that was in your main package, and go’s strict limitation of dependencies means you can’t access it as a global variable.

Now, you could of course just create it as a common dependency and use a global, but why not use context?

A little trick with a closure can feed this context with a populated newrelic.Application (apologies for the strange line wrap, but I’m working with narrow margins here):

    package main

    import (
        "fmt"
        "io"
        "net/http"
        "os"
    
        "github.com/newrelic/go-agent"
        "github.com/FooStartup/profiling"
    )
    
    func helloHandler(
        ctx context.Context, w http.ResponseWriter, r *http.Request,
    ) {
        // to be meaningful, this handler would do something with
        // ctx :)
        io.WriteString(w, "hello, world")
    }

    // wrapWithProfilingContext curries the passed-in three argument
    // handler function to a standard 2-argument function, and makes
    // a new context with the newrelic transaction.
    func wrapWithProfilingContext(
        httpCtx context.Context,
        ctxHanlerFunc func(context.Context, http.ResponseWriter,
            *http.Request),
    ) (
        rf func(w http.ResponseWriter, r *http.Request),
    ) {
        ctx := httpCtx
        if tx, ok := w.(newrelic.Transaction); ok {
            ctx = profiling.NewContext(ctx, tx)
        }
        return func(w http.ResponseWriter, r *http.Request) {
            ctxHandlerFunc(ctx, w, r)
        }
    }
    
    func main() {
        // Create a config.  You need to provide the desired
        // application name and your New Relic license key.
        cfg := newrelic.NewConfig(
            "My Go Application", "__YOUR_NEW_RELIC_LICENSE_KEY__",
        )
    
        // Create an application.  This represents an application in
        // the New Relic UI.
        app, err := newrelic.NewApplication(cfg)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }

        httpContext := profiling.NewContext(context.Background(), app)
    
        // Wrap helloHandler.  The performance of this handler will
        // be recorded.
        http.HandleFunc(
            newrelic.WrapHandleFunc(
                app, "/", wrapWithProfilingContext(
                    httpContext, helloHandler,
                ),
            ),
        )
        
        http.ListenAndServe(":8000", nil)
    }

Now, any caller can create a new newrelic.Transaction by retrieving the newrelic.Application object during the goroutine setup:

    go func() {
        goroCtx := ctx
        var txn newrelic.Transaction
        if app := profiling.GetApp(ctx); app != nil {
            txn = app.StartTransaction("MyBackgroundFunc", nil, nil)
            defer txn.End() 
        }
        goroCtx = profiling.NewContext(ctx, txn)
        MyBackgroundFunc(goroCtx, foo, bar)
    }()

i.e., if the application was stored in the context, then create a new transaction and “overwrite” the one that was already there by creating a new context with the key replaced.

There’s a subtlety here: if the application wasn’t in the context, I explicitly want the stored transaction to be “removed”. So I store a nil. This is equivalent to there never having been any item there.

It’s probably possible to turn that into a convenient one-liner as the StartTrace example did, but hopefully this can illustrate how you can use context to pass objects for cross-cutting concerns like profiling.

Share Comments
comments powered by Disqus