AppOptics is SaaS-based monitoring with distributed tracing, code-level application performance monitoring, and host and infrastructure monitoring. This repository provides instrumentation for Go, which allows Go-based applications to be monitored using AppOptics.
AppOptics's cross-language, production-ready, low-overhead distributed tracing system (similar to Dapper, Zipkin, or X-Trace) can follow the path of an application request as it is processed and forwarded between services written in Go, Java, Scala, Node.js, Ruby, Python, PHP, and C#/.NET, reporting data to AppOptics's cloud platform for analysis, monitoring, and fine-grained, filterable data visualization.
We run this in our production environment, as do our customers. Please share any feedback you have; PRs welcome.
To get tracing, you'll need a (free) AppOptics account. Follow the instructions during signup to install the Go Agent.
The in-app install flow will wait for some data to come in from your service before continuing. You can either instrument your application (read on), or check out the demo app below to get a quick start!
Install the following to get started:
- This package: go get github.com/appoptics/appoptics-apm-go/v1/ao
Note that the service key needs to be configured for a successful setup. See Configuration for more info.
No tracing occurs when the environment variable APPOPTICS_DISABLED
is explicitly set to true
. With this environment variable,
calls to the APIs are no-ops, and empty structs keep span and instrumentation types from making memory allocations. This allows for precise control over which deployments are traced.
# Set to disable tracing and APM instrumentation
$ export APPOPTICS_DISABLED=true
The simplest integration option is this package's ao.HTTPHandler wrapper. This will monitor the performance of the provided http.HandlerFunc, visualized with latency distribution heatmaps filterable by dimensions such as URL host & path, HTTP status & method, server hostname, etc. ao.HTTPHandler will also continue a distributed trace described in the incoming HTTP request headers (from either another instrumented Golang application or AppOptics's automatic instrumentation of our other supported application runtimes).
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
"github.com/appoptics/appoptics-apm-go/v1/ao"
)
var startTime = time.Now()
func normalAround(ts, stdDev time.Duration) time.Duration {
return time.Duration(rand.NormFloat64()*float64(stdDev)) + ts
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(normalAround(2*time.Second, 100*time.Millisecond))
w.WriteHeader(404)
fmt.Fprintf(w, "Slow request, path: %s", r.URL.Path)
}
func increasinglySlowHandler(w http.ResponseWriter, r *http.Request) {
// getting slower at a rate of 1ms per second
offset := time.Duration(float64(time.Millisecond) * time.Now().Sub(startTime).Seconds())
time.Sleep(normalAround(time.Second+offset, 100*time.Millisecond))
fmt.Fprintf(w, "Slowing request, path: %s", r.URL.Path)
}
func main() {
http.HandleFunc("/slow", ao.HTTPHandler(slowHandler))
http.HandleFunc("/slowly", ao.HTTPHandler(increasinglySlowHandler))
http.ListenAndServe(":8899", nil)
}
To monitor more than just the overall latency of each request to your Go service, you will need to
break a request's processing time down by placing small benchmarks into your code. To do so, first
start or continue a Trace
(the root Span
), then create a series of Span
s to capture the time used by different parts of the app's stack as it is processed.
AppOptics provides a way to measure time spent by your code: a Span
can measure e.g. a
single DB query or cache request, an outgoing HTTP or RPC request, the entire time spent within a
controller method, or the time used by a middleware method between calls to child Spans. Spans can be created
as children of other Spans.
AppOptics identifies a reported Span's type from its key-value pairs; if keys named "Query" and "RemoteHost" are used, a Span is assumed to measure the extent of a DB query. KV pairs can be appended to Spans as optional extra variadic arguments to methods such as BeginSpan() or Info(), and End(). We also provide helper methods such as BeginQuerySpan(), BeginCacheSpan(), BeginRemoteURLSpan(), BeginRPCSpan(), and BeginHTTPClientSpan() that match the spec in our custom instrumentation docs to report attributes associated with different types of service calls, used for indexing AppOptics's filterable charts and latency heatmaps.
func myHandler(ctx context.Context) {
// create new ao.Span and context.Context for this part of the request
L, ctxL := ao.BeginSpan(ctx, "myHandler")
defer L.End()
// Start a new Span, given a parent span
mL, _ := L.BeginSpan("myMiddleware")
// ... do something slow ...
mL.End()
// Start a new query Span, given a context.Context
q1L := ao.BeginQuerySpan(ctxL, "myDB", "SELECT * FROM tbl1", "postgresql", "db1.com")
// perform a query
q1L.End()
}
func processRequest() {
// Create ao.Trace and bind to new context.Context
ctx := ao.NewContext(context.Background(), ao.NewTrace("myApp"))
myHandler(ctx) // Dispatch handler for request
ao.EndTrace(ctx)
}
A common pattern when tracing in golang is to call ao.HTTPHandler(handler)
then retrieve the trace
context inside of the handler.
func myHandler( w http.ResponseWriter, r *http.Request ) {
// trace this request, overwriting w with wrapped ResponseWriter
t, w, r := ao.TraceFromHTTPRequestResponse("myHandler", w, r)
ctx := ao.NewContext(context.Background(), t)
defer t.End()
// create a query span
q1L := ao.BeginQuerySpan(ctx, "myDB", "SELECT * FROM tbl1", "postgresql", "db1.com")
// run your query here
// end the query span
q1L.End()
fmt.Fprintf(w, "I ran a query")
}
func main() {
http.HandleFunc("/endpoint", ao.HTTPHandler(myHandler))
http.ListenAndServe(":8899", nil)
}
Our out-of-the-box instrumentation assigns transaction name based on URL and Controller/Action values detected. However, you may want to override the transaction name to better describe your instrumented operation. Take note that transaction name is converted to lowercase, and might be truncated with invalid characters replaced.
If multiple transaction names are set on the same trace, then the last one would be used.
Empty string and null are considered invalid transaction name values and will be ignored.
ao.SetTransactionName(ctx context.Context, name string)
is used to set the custom transaction name in
the HTTP handler. The current http.Request context is needed to retrieve the AppOptics tracing context, if any.
func slowHandler(w http.ResponseWriter, r *http.Request) {
ao.SetTransactionName(r.Context(), "my-custom-transaction-name")
time.Sleep(normalAround(2*time.Second, 100*time.Millisecond))
w.WriteHeader(404)
fmt.Fprintf(w, "Slow request... Path: %s", r.URL.Path)
}
When you use AppOptics SDK to create the Span by yourself, the SetTransactionName(string)
method can be invoked
to set the custom transaction name.
...
// create new ao.Span and context.Context for this part of the request
L, ctxL := ao.BeginSpan(ctx, "myHandler")
L.SetTransactionName("my-custom-transaction-name")
defer L.End()
...
You can also set the environment variable APPOPTICS_PREPEND_DOMAIN
to true
if you need to
prepend the hostname to the transaction name. This works for both default transaction names and
the custom transaction names provided by you.
An AppOptics trace is defined by a context (a globally unique ID and metadata) that is persisted across the different hosts, processes, languages and methods that are used to serve a request. Thus to start a new Span you need either a Trace or another Span to begin from. (The Trace interface is also the root Span, typically created when a request is first received.) Each new Span is connected to its parent, so you should begin new child Spans from parents in a way that logically matches your application; for more information see our custom span documentation.
The Go blog recommends propagating request context using the package context: "At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests." Frameworks like Gin and Gizmo use Context implementations, for example. We provide helper methods that allow a Trace to be associated with a context.Context interface; for example, ao.BeginSpan returns both a new Span and an associated context, and ao.Info(ctx) and ao.End(ctx) both use the Span defined in the provided context.
It is not required to work with context.Context to trace your app, however. You can also use just the Trace and Span interfaces directly, if it better suits your instrumentation use case.
func runQuery(ctx context.Context, query string) {
l := ao.BeginQuerySpan(ctx, "myDB", query, "postgresql", "db1.com")
// .. execute query ..
l.End()
}
func main() {
// create trace and bind to new context
ctx := ao.NewContext(context.Background(), ao.NewTrace("myApp"))
// pass the root span context to runQuery
runQuery(ctx)
// end trace
ao.EndTrace(ctx)
}
The only environment variable you need to set before kicking off is the service key:
Variable Name | Required | Default | Description |
---|---|---|---|
APPOPTICS_SERVICE_KEY | Yes | N.A | The service key identifies the service being instrumented within your Organization. It should be in the form of <api token>:<service name> . |
For the full list of the configuration items and descriptions, including YAML config file options, please refer to our knowledge base website: https://docs.appoptics.com/kb/apm_tracing/go/configure/
While we use AppOptics to trace our own production Go services, this version of our Go instrumentation is currently in beta and under active development. We welcome your feedback, issues and feature requests, and please contact us at technicalsupport@solarwinds.com!
If you have installed AppOptics and this package, you can run the sample “web app” included with appoptics-apm-go:
$ cd $GOPATH/src/github.com/appoptics/appoptics-apm-go/examples/sample_app
$ go run main.go
A web server will run on port 8899. It doesn’t do much, except wait a bit and echo back your URL path:
$ curl http://localhost:8899/hello
Slow request... Path: /hello
You should see these requests appear on your AppOptics dashboard.
There is also a demonstration of distributed tracing in examples/distributed_app, a sample system comprised of three Go services and two Python services. It can be built and run from source in each service's subdirectory, or by using docker-compose:
$ cd $GOPATH/src/github.com/appoptics/appoptics-apm-go/examples/distributed_app
$ docker-compose build
# ... (building)
$ APPOPTICS_API_TOKEN=xxx docker-compose up -d
Starting distributedapp_alice_1
Starting distributedapp_bob_1
Starting distributedapp_caroljs_1
Starting distributedapp_davepy_1
Starting distributedapp_redis_1
and substituting "xxx" with your AppOptics API token. Note that because this spins up multiple services, the service name is provided in the docker-compose.yml
file.
This app currently provides two HTTP handlers:
aliceHandler
, which randomly forwards requests to either of "bob" (Go), "caroljs", or "davepy",
and concurrentAliceHandler
, which makes requests to all three in parallel.
$ curl http://localhost:8890/alice
{"result":"hello from bob"}
$ curl http://localhost:8890/alice
Hello from Flask!
$ curl http://localhost:8890/alice
Hello from app.js
$ curl http://localhost:8890/concurrent
Hello from Flask!{"result":"hello from bob"}Hello from app.js
You should see traces for these appear on your AppOptics dashboard. Here is an example trace of the concurrent handler:
Support for the OpenTracing 1.0 API is available in the opentracing package as a technology preview. The OpenTracing tracer in that package provides support for OpenTracing span reporting and context propagation by using AppOptics's span reporting and HTTP header formats, permitting the OT tracer to continue distributed traces started by AppOptics's instrumentation and vice versa. Some of the OpenTracing standardized tags are mapped to AppOptics tag names as well.
To set AppOptics's tracer to be your global tracer, use something like the following:
import(
stdopentracing "github.com/opentracing/opentracing-go"
aotracing "github.com/appoptics/appoptics-apm-go/v1/ao/opentracing"
)
// tracer returns the global tracing construct for OpenTracing.
// it is returned here so it can be used in functions that expect
// a Tracer, such as server init functions in go-kit
func tracer() stdopentracing.Tracer {
tracer := aotracing.NewTracer()
stdopentracing.SetGlobalTracer(tracer)
return tracer
}
Currently, opentracing.NewTracer()
does not accept any options, but this may change in the future.
Please let us know if you are using this package while it is in preview by contacting us at technicalsupport@solarwinds.com.
Copyright (c) 2018 SolarWinds, LLC
Released under the Apache License 2.0