Product release

Elastic APM Go Agent Beta Released

When Elastic APM went GA early in 2018, it came with support for Node.js, Python, Ruby (beta), and frontend JavaScript (beta). Since then, we’ve started work on support for Java and Go. If you’re not already familiar with Elastic APM, it’s our new open source Application Performance Monitoring (APM) solution, built on top of the Elastic Stack.

We are pleased to announce the Go agent’s first beta release, version 0.4.0. The agent is ready for serious testing, but there may be minor API changes until we call it GA. We will, however, avoid making breaking changes as far as possible, to avoid code churn. If you are looking to track performance of your Go application, now is the time to try it out. Let us know what you think in the APM Discuss forum.

Instrumenting a Go Web Application

Let’s take a quick look at how to instrument a dummy Go web application written using plain old net/http and database/sql:

package main
import (
        "database/sql"
        "fmt"
        "math/rand"
        "net/http"
        "time"
        sqlite3 "github.com/mattn/go-sqlite3"
)
func sleep(n int64) int64 {
        time.Sleep(time.Duration(n))
        return n
}
func main() {
        sql.Register("sqlite3_custom", &sqlite3.SQLiteDriver{
                ConnectHook: func(conn *sqlite3.SQLiteConn) error {
                        return conn.RegisterFunc("sleep", sleep, true)
                },
        })
        db, err := sql.Open("sqlite3_custom", ":memory:")
        if err != nil {
                panic(err)
        }
        http.HandleFunc("/sleep", func(w http.ResponseWriter, req *http.Request) {
                delay := time.Duration(rand.ExpFloat64() * float64(time.Millisecond*50))
                if _, err := db.ExecContext(req.Context(), "SELECT sleep(?)", int64(delay)); err != nil {
                        panic(err)
                }
                fmt.Fprintln(w, "slept for", delay)
        })
        http.ListenAndServe(":8080", nil)
}

Please note the use of the request context as input to the database query — the Go agent will make use of this. We’re using a custom go-sqlite3 driver to inject some delays in the database queries.

Go Agent Installation and Instrumentation

Before we go further, you should install the Go agent packages:

go get github.com/elastic/apm-agent

To instrument your application, you should start with the incoming requests. For each incoming request, we’ll report a transaction to Elastic APM. The Go agent provides several packages to simplify instrumenting specific frameworks or packages. For example, module/apmhttp provides wrappers for net/http servers and clients. To report the performance of your HTTP handlers, you can use the apmhttp.Wrap:

mux := http.NewServeMux()
mux.Handle(...)
http.ListenAndServe(":8080", apmhttp.Wrap(mux))

In addition to apmhttp, there are packages tailored to several popular web frameworks/toolkits: module/apmgin,module/apmecho, module/apmgorilla, and module/apmhttprouter.

If all we were to do is wrap the HTTP handler, all we would see is the route performance with few details on what’s going on within it. For significant operations within your transaction (e.g. database queries, RPC requests), you should also report spans. As mentioned above, apmhttp provides a client wrapper. Using this, outgoing HTTP requests will be reported as spans within the context’s transaction. In this example, however, we’re interested in measuring SQL (database/sql) queries.

For SQL queries, module/apmsql provides a means of wrapping a database driver such that queries and other operations are reported as spans. This depends on using the "context" methods (e.g. QueryContext), and passing in a context object that contains a transaction. The context object received by your HTTP handler will have a transaction added to it by apmhttp.

To simplify things, apmsql provides a pair of functions which mirror those of the same name in database/sql: apmsql.Register and apmsql.Open. Registering and opening database connections using these functions will provide wrapped versions that report spans. If you need to register a customized driver, as we do in our example, you must use apmsql.Register instead of database/sql.Register. Alternatively, if you rely on default drivers, several well-known database drivers can be automatically registered with apmsql by importing packages: apmsql/mysql, apmsql/pq, and apmsql/sqlite3.

So in the end, all we need to do is

  1. Replace our sql.Open call with apmsql.Open and sql.Register with apmsql.Register
  2. Replace the go-sqlite3 import with apmsql/sqlite3
  3. Wrap the net/http handler (http.DefaultServeMux) with a wrapped one.

The end result looks like this:

package main
import (
        "fmt"
        "math/rand"
        "net/http"
        "time"
        "github.com/elastic/apm-agent-go/module/apmhttp"
        "github.com/elastic/apm-agent-go/module/apmsql"
        apmsqlite3 "github.com/elastic/apm-agent-go/module/apmsql/sqlite3"
        sqlite3 "github.com/mattn/go-sqlite3"
)
func sleep(n int64) int64 {
        time.Sleep(time.Duration(n))
        return n
}
func main() {
        apmsql.Register("sqlite3_custom", &sqlite3.SQLiteDriver{
                ConnectHook: func(conn *sqlite3.SQLiteConn) error {
                        return conn.RegisterFunc("sleep", sleep, true)
                },
        }, apmsql.WithDSNParser(apmsqlite3.ParseDSN))
        db, err := apmsql.Open("sqlite3_custom", ":memory:")
        if err != nil {
                panic(err)
        }
        http.HandleFunc("/sleep", func(w http.ResponseWriter, req *http.Request) {
                delay := time.Duration(rand.ExpFloat64() * float64(time.Millisecond*50))
                if _, err := db.ExecContext(req.Context(), "SELECT sleep(?)", int64(delay)); err != nil {
                        panic(err)
                }
                fmt.Fprintln(w, "slept for", delay)
        })
        http.ListenAndServe(":8080", apmhttp.Wrap(http.DefaultServeMux))
}

Done! Now what? We need to tell the application where to send the data.

Go Agent Configuration

When you instrument your application like above, you’re making use of the default tracer -- a global instance of a tracer object, which is responsible for sending data to the Elastic APM Server. The default tracer is configured using environment variables. It is also possible to create a non-global tracer, and configure it in-process. This may be desirable for more complicated applications, that we won’t cover here, e.g. dynamically controlling tracing through your application’s existing configuration system.

The single most important configuration is the APM Server URL, which is configured via ELASTIC_APM_SERVER_URL:

export ELASTIC_APM_SERVER_URL=http://apm-server.local:8200

If your server requires a secret token for authentication, you must also set ELASTIC_APM_SECRET_TOKEN. Everything else is optional.

You can find instructions on setting up the APM Server, along with Elasticsearch and Kibana, on our Elastic APM Solution page.

Viewing APM Data

Once the data has made its way from your application through the API server and into Elasticsearch, you can view the performance data by opening up Kibana and navigating to the APM UI, and selecting your service. If you haven’t configured it specifically, the service name will be based on your Go application binary’s name (e.g. if it’s called "/usr/local/bin/app", it will show up in the UI as "app".)

apm-ui.png

Future

We’re always working on improving our solutions. Right now we're beginning to work on support for distributed tracing and application metrics (custom and predefined). Some time in the not too distant future you can expect to find these things in each of the agents. We’ll also be looking for ways to simplify the process of instrumenting your Go applications, in a way that makes most sense for today’s landscape.

All of this is open source, so you can watch (and contribute to) the repositories on GitHub, or come and talk about it in the Discuss forum. We would also love it if you filled out the Go agent survey, to help shape its future.