OpenCensus is being archived! Read the blog post to learn more

Go kit

Introduction

Go kit is a toolkit for microservices. It provides guidance and solutions for most of the common operational and infrastructural concerns. Allowing you to focus your mental energy on your business logic. It provides the building blocks for separating transports from business domains; making it easy to switch one transport for the other. Or even service multiple transports for a service at once.

Go kit provides tracing and metrics middleware for consistent idiomatic views of your Go kit services regardless of chosen transport. It includes native OpenCensus tracing middleware and is very easy to get started with.

Example

For this step by step example we’re using the kitgen tool as provided with Go kit; to build a bare bones HTTP transport driven greeting service. For more advanced Go kit services you’d probably create your microservice infrastructure manually. Adding instrumentation for these services will be very similar and this step by step guide will also provide you with the needed background to get it done.

1. Service definition

We start by first creating our Go kit service definition for kitgen to use and save it as service.go:

package kitoc

import "context"

type Service interface {
	Hello(ctx context.Context, firstName string, lastName string) (greeting string, err error)
}
2. Kitgen scaffolding

Now we can create our Go kit scaffolding with kitgen:

mkdir hello
cd hello
~/g/s/g/g/hello $ kitgen ../service.go

We will now have our basic implementation boilerplate for the service in a tree structure like this:

.../go-kit-example
├── hello
│   ├── endpoints
│   │   └── endpoints.go
│   ├── http
│   │   └── http.go
│   └── service
│       └── service.go
├── LICENSE
└── service.go
3. Service implementation

To make the service work we need to implement the service implementation’s Hello method. By changing hello/service/service.go:

func (s Service) Hello(ctx context.Context, firstName string, lastName string) (string, error) {
	panic(errors.New("not implemented"))
}

Into something like this:

type serializableError struct{ error }

func (s *serializableError) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.Error())
}

func newSerializableError(text string) error {
	return &serializableError{errors.New(text)}
}

func (s Service) Hello(ctx context.Context, firstName string, lastName string) (string, error) {
	firstName = strings.Trim(firstName, "\t\r\n ")
	lastName = strings.Trim(lastName, "\t\r\n ")

	if len(firstName) == 0 && len(lastName) == 0 {
		return "", newSerializableError("missing required name information")
	}
	if len(firstName) == 0 {
		return fmt.Sprintf(
			"Hello Mr./Ms. %s, nice to meet you. Do you have a first name?",
			lastName,
		), nil
	}
	if len(lastName) == 0 {
		return fmt.Sprintf(
			"Hello %s, nice to meet you. Do you have a last name?",
			firstName,
		), nil
	}
	return fmt.Sprintf(
		"Hello %s %s, nice to meet you.",
		firstName, lastName,
	), nil
}
4. Failer implementation

To include business error messages as annotations in OpenCensus spans we need the Go kit Response structs to implement the endpoint.Failer interface. An issue report has been filed so this next step might become deprecated in the (near) future. To manually add the interface add the following code to hello/endpoints/endpoints.go:

func (r HelloResponse) Failed() error { return r.Err }
5. Go kit server options

Kitgen omits the ability to inject transport options into Go kit servers. Let’s fix this first so we can attach our OpenCensus handler, server error logger and other generic server options later. Change the following generated code in hello/http/http.go:

func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler {
	m := http.NewServeMux()
	m.Handle("/hello", httptransport.NewServer(
        endpoints.Hello, DecodeHelloRequest, EncodeHelloResponse))
	return m
}

Into:

func NewHTTPHandler(endpoints endpoints.Endpoints, options ...httptransport.ServerOption) http.Handler {
	m := http.NewServeMux()
	m.Handle("/hello", httptransport.NewServer(
        endpoints.Hello, DecodeHelloRequest, EncodeHelloResponse, options...))
	return m
}
6. Application bootstrap

The business logic of the service is done as well as the Go kit HTTP transport handler. Now we need to create our main.go to turn our service package into a runnable application. Let’s create under cmd/hello/main.go:

package main

import (
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	"github.com/go-kit/kit/log"
	httptransport "github.com/go-kit/kit/transport/http"
	"github.com/oklog/run"

	"github.com/opencensus-integrations/go-kit-example/hello/endpoints"
	svchttp "github.com/opencensus-integrations/go-kit-example/hello/http"
	"github.com/opencensus-integrations/go-kit-example/hello/service"
)

const (
	serviceName = "oc-gokit-example"
)

func main() {
	// Set-up our contextual logger.
	var logger log.Logger
	{
		logger = log.NewLogfmtLogger(os.Stdout)
		logger = log.NewSyncLogger(logger)
		logger = log.With(logger, "svc", serviceName)
	}

	// Set-up our service.
	var handler http.Handler
	{
		// Create our hello service implementation.
		svc := service.Service{}

		// Create our Go kit Endpoints.
		endpoints := endpoints.Endpoints{
			Hello: endpoints.MakeHelloEndpoint(svc),
		}

		// Set-up our Go kit HTTP transport options.
		var serverOptions []httptransport.ServerOption
		serverOptions = append(serverOptions, httptransport.ServerErrorLogger(logger))

		// Create our HTTP transport handler.
		handler = svchttp.NewHTTPHandler(endpoints, serverOptions...)
	}

	// run.Group manages our goroutine lifecycles
	// see: https://www.youtube.com/watch?v=LHe1Cb_Ud_M&t=15m45s
	var g run.Group
	{
		// Set-up our HTTP service.
		var (
			listener, _ = net.Listen("tcp", ":0") // dynamic port assignment
			addr        = listener.Addr().String()
		)
		g.Add(func() error {
			logger.Log("msg", "service start", "transport", "http", "address", addr)
			return http.Serve(listener, handler)
		}, func(error) {
			listener.Close()
		})
	}
	{
		// Set-up our signal handler.
		var (
			cancelInterrupt = make(chan struct{})
			c               = make(chan os.Signal, 2)
		)
		defer close(c)

		g.Add(func() error {
			signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
			select {
			case sig := <-c:
				return fmt.Errorf("received signal %s", sig)
			case <-cancelInterrupt:
				return nil
			}
		}, func(error) {
			close(cancelInterrupt)
		})
	}

	// Spawn our Go routines and wait for shutdown.
	logger.Log("exit", g.Run())
}
7. OpenCensus instrumentation

We now have a complete runnable Go kit service. To add OpenCensus tracing to our app we need to add transport and endpoint middleware. Go kit provides the OpenCensus tracing middleware as part of its core middleware.

In this example we’ll be using a Zipkin tracing backend. To start a local Zipkin server you can either do:

# run a local Zipkin server (needs Java 8 or higher installed)
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar

or:

# run Zipkin in Docker
docker run -d -p 9411:9411 openzipkin/zipkin

When Zipkin has finished starting up you can look at the Zipkin dashboard here: http://localhost:9411

We need to add the following imports to our application bootstrap code:

import (
	kitoc "github.com/go-kit/kit/tracing/opencensus"
	zipkin "github.com/openzipkin/zipkin-go"
	httpreporter "github.com/openzipkin/zipkin-go/reporter/http"
	oczipkin "contrib.go.opencensus.io/exporter/zipkin"
	"go.opencensus.io/trace"
)

Add our OpenCensus configuration with Zipkin backend:

// Set-up our OpenCensus instrumentation with Zipkin backend
var (
	zipkinURL        = "http://localhost:9411/api/v2/spans"
	reporter         = httpreporter.NewReporter(zipkinURL)
	localEndpoint, _ = zipkin.NewEndpoint(serviceName, ":0")
	exporter         = oczipkin.NewExporter(reporter, localEndpoint)
)
defer reporter.Close()

// Always sample our traces for this demo.
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})

// Register our trace exporter.
trace.RegisterExporter(exporter)

Add the Go kit endpoint middleware for OpenCensus:

// Wrap our service endpoints with OpenCensus tracing middleware.
endpoints.Hello = kitoc.TraceEndpoint("gokit:endpoint hello")(endpoints.Hello)

Add the Go kit HTTP transport middleware for OpenCensus:

// Add the GO kit HTTP transport middleware to our serverOptions.
serverOptions = append(serverOptions, kitoc.HTTPServerTrace())

The complete updated cmd/hello/main.go code looks like this:

package main

import (
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	"github.com/go-kit/kit/log"
	kitoc "github.com/go-kit/kit/tracing/opencensus"
	httptransport "github.com/go-kit/kit/transport/http"
	"github.com/oklog/run"
	zipkin "github.com/openzipkin/zipkin-go"
	httpreporter "github.com/openzipkin/zipkin-go/reporter/http"
	oczipkin "contrib.go.opencensus.io/exporter/zipkin"
	"go.opencensus.io/trace"

	"github.com/opencensus-integrations/go-kit-example/hello/endpoints"
	svchttp "github.com/opencensus-integrations/go-kit-example/hello/http"
	"github.com/opencensus-integrations/go-kit-example/hello/service"
)

const (
	serviceName = "oc-gokit-example"
	zipkinURL   = "http://localhost:9411/api/v2/spans"
)

func main() {
	// Set-up our contextual logger.
	var logger log.Logger
	{
		logger = log.NewLogfmtLogger(os.Stdout)
		logger = log.NewSyncLogger(logger)
		logger = log.With(logger, "svc", serviceName)
	}

	// Set-up our OpenCensus instrumentation with Zipkin backend
	{
		var (
			reporter         = httpreporter.NewReporter(zipkinURL)
			localEndpoint, _ = zipkin.NewEndpoint(serviceName, ":0")
			exporter         = oczipkin.NewExporter(reporter, localEndpoint)
		)
		defer reporter.Close()

		// Always sample our traces for this demo.
		trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})

		// Register our trace exporter.
		trace.RegisterExporter(exporter)
	}

	// Set-up our service.
	var handler http.Handler
	{
		// Create our hello service implementation.
		svc := service.Service{}

		// Create our Go kit Endpoints.
		endpoints := endpoints.Endpoints{
			Hello: endpoints.MakeHelloEndpoint(svc),
		}

		// Wrap our service endpoints with OpenCensus tracing middleware.
		endpoints.Hello = kitoc.TraceEndpoint("gokit:endpoint hello")(endpoints.Hello)

		// Set-up our Go kit HTTP transport options.
		var serverOptions []httptransport.ServerOption
		serverOptions = append(serverOptions, httptransport.ServerErrorLogger(logger))
		serverOptions = append(serverOptions, kitoc.HTTPServerTrace())

		// Create our HTTP transport handler.
		handler = svchttp.NewHTTPHandler(endpoints, serverOptions...)
	}

	// run.Group manages our goroutine lifecycles
	// see: https://www.youtube.com/watch?v=LHe1Cb_Ud_M&t=15m45s
	var g run.Group
	{
		// Set-up our HTTP service.
		var (
			listener, _ = net.Listen("tcp", ":0") // dynamic port assignment
			addr        = listener.Addr().String()
		)
		g.Add(func() error {
			logger.Log("msg", "service start", "transport", "http", "address", addr)
			return http.Serve(listener, handler)
		}, func(error) {
			listener.Close()
		})
	}
	{
		// Set-up our signal handler.
		var (
			cancelInterrupt = make(chan struct{})
			c               = make(chan os.Signal, 2)
		)
		defer close(c)

		g.Add(func() error {
			signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
			select {
			case sig := <-c:
				return fmt.Errorf("received signal %s", sig)
			case <-cancelInterrupt:
				return nil
			}
		}, func(error) {
			close(cancelInterrupt)
		})
	}

	// Spawn our Go routines and wait for shutdown.
	logger.Log("exit", g.Run())
}

The hello service example as provided here can be found at: https://github.com/opencensus-integrations/go-kit-example

Each step in this guide is a separate commit in the repository so it is easy to see the changes made per step by comparing commits like this: step 7: add OC middleware

Running the example

To run our example we can simply use go run or do a go build followed by calling the just compiled executable. Once our service is running, it will display a message including the listen address:

$ go run cmd/hello/main.go
svc=oc-gokit-example msg="service start" transport=http address=[::]:49269

Now we can use curl to call the service API and receive responses:

~ $ curl -X POST -d '{}' http://localhost:49269/hello
{"Greeting":"","Err":"missing required name information"}

~ $ curl -X POST -d '{"FirstName":"John"}' http://localhost:49269/hello
{"Greeting":"Hello John, nice to meet you. Do you have a last name?","Err":null}

~ $ curl -X POST -d '{"LastName":"Doe"}' http://localhost:49269/hello
{"Greeting":"Hello Mr./Ms. Doe, nice to meet you. Do you have a first name?","Err":null}

~ $ curl -X POST -d '{"FirstName":"John","LastName":"Doe"}' http://localhost:49269/hello
{"Greeting":"Hello John Doe, nice to meet you.","Err":null}

Examine the traces

To look at the traces from our service open the Zipkin dashboard at: http://localhost:9411

Traces list

By clicking on displayed traces we can see their details:

Trace 1

And clicking on spans we can see the span details: Trace 1 detail

Trace 2

Trace 2 detail

Resources

Other Examples

A very comprehensive example of Go kit including OpenCensus instrumentation can be found here: https://github.com/basvanbeek/opencensus-gokit-example. It contains a couple of backend microservices and an API frontend service. It also shows the ability to run all services together in an elegant monolith. The elegant monolith highlights how many microservice concepts when stripped from their networking aspect still make sense, including OpenCensus observability.