Learning Go by examples: part 10 - Instrument your Go app with OpenTelemetry and send traces to Jaeger - Distributed Tracing
Tags: go, beginners, opentelemetry, tracing
Published: 2022-11-22 07:29:34

---
title: Learning Go by examples: part 10 - Instrument your Go app with OpenTelemetry and send traces to Jaeger - Distributed Tracing
published: true
description: Serie of article in order to learn Golang language by concrete applications as example
series: Learning Go by examples
tags: Golang, beginners, opentelemetry, tracing
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38pl2u0md1pg4usuzuqn.png
---

In previous articles we created an [HTTP REST API server](https://dev.to/aurelievache/learning-go-by-examples-part-2-create-an-http-rest-api-server-in-go-1cdm), a [CLI](https://dev.to/aurelievache/learning-go-by-examples-part-3-create-a-cli-app-in-go-1h43), a [Bot for Discord](https://dev.to/aurelievache/learning-go-by-examples-part-4-create-a-bot-for-discord-in-go-43cf) and even a [game for Nintendo Game Boy Advance](https://dev.to/aurelievache/learning-go-by-examples-part-5-create-a-game-boy-advance-gba-game-in-go-5944). 

Today, we will learn how to use OpenTelemetry Go library to create an instrumented application and send traces to a Jaeger instance.

## OpenTelemetry

![OpenTelemetry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9yuspug7wcqehk1an5me.jpg)

[OpenTelemetry](https://opentelemetry.io/) is a collection of tools, APIs, and SDKs. Useful to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) to help you analyze your software’s performance and behavior.

OpenTelemetry integrates with popular libraries and frameworks such as Spring, Express, Quarkus, and [with a lot of languages](https://opentelemetry.io/docs/instrumentation/), including Go :-).

![OpenTracing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wzkdtmi2chayexzdwx5d.jpg)

If you have ever heard of [OpenTracing](https://opentracing.io/) or are used to using it, know that now OpenTracing is deprecated, so it is better to use OpenTelemetry 🙂.
If you want to [migrate from OpenTracing to OpenTelemetry](https://opentelemetry.io/docs/migration/opentracing/), an official guide exists.

## Jaeger

![Jaeger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cin8hxpjbk58ec6gvoht.jpg)

[Jaeger](https://www.jaegertracing.io/) is an open-source distributed tracing platform.

It can be used for monitoring microservices-based distributed systems:

* Distributed context propagation
* Distributed transaction monitoring
* Root cause analysis
* Service dependency analysis
* Performance / latency optimization

Jaeger contains several components:

![Jaeger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39lrxmu2vi52f65ld9bs.png)

### Run Jaeger locally

We will use Docker to run the Jaeger UI, collector, query, and agent, with an in memory storage component:

```bash
$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.6
```

The Jaeger UI will be available on port `16686` and you will send traces to port `14268`.

Open your browser, enter the URL `http://localhost:16686` to display the Jaeger UI:

![Jaeger UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gxtquhyqc6n8t1a4tx7.png)

## Initialization

We created our Git repository in a previous article, so now we just have to retrieve it locally:

```bash
$ git clone https://github.com/scraly/learning-go-by-examples.git
$ cd learning-go-by-examples
```

We will create a folder `go-gopher-opentelemetry` for our CLI application and go into it:

```bash
$ mkdir go-gopher-opentelemetry
$ cd go-gopher-opentelemetry
```

Now, we have to initialize Go modules (dependency management):

```bash
$ go mod init github.com/scraly/learning-go-by-examples/go-gopher-opentelemetry
go: creating new go.mod: module github.com/scraly/learning-go-by-examples/go-gopher-opentelemetry
```

This will create a `go.mod` file like this:

```go
module github.com/scraly/learning-go-by-examples/go-gopher-opentelemetry

go 1.19
```

Before to start our super instrumented application, as good practices, we will create a simple code organization.

Create the following folders organization:

```bash
.
├── README.md
├── bin
└── go.mod
```

That's it? Yes, the rest of our code organization will be created shortly ;-).

## Create our application

OpenTelemetry is split into two parts: an API to instrument code with, and SDKs that implement the API.

Let's install OpenTelemetry Trace API in order to use it in our code:

```bash
$ go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
$ go get go.opentelemetry.io/otel/exporters/jaeger
$ go get go.opentelemetry.io/otel/sdk/resource
$ go get go.opentelemetry.io/otel/sdk/trace
```

Good, now we can create a `main.go` file and copy/paste the following code into it.

Go code is organized into packages. So, first, we initialize the package, called main, and all dependencies/librairies we need to import and use in our main file:

```go
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    tracesdk "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
)
```

With the imports added, you can start instrumenting.

The OpenTelemetry Tracing API provides a Tracer to create traces. These Tracers are designed to be associated with one instrumentation library. To uniquely identify an application to the Tracer we will use create a constant with the package name in `main.go` file.

So, we define const:

```go
const (
    service     = "go-gopher-opentelemetry"
    environment = "development"
    id          = 1
)
```

Then, we create a function called `tracerProvider()` that initiates a connection to a tracer provider, a `Jaeger` instance for example.

```go
func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
    // Create the Jaeger exporter
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
    if err != nil {
        return nil, err
    }
    tp := tracesdk.NewTracerProvider(
        // Always be sure to batch in production.
        tracesdk.WithBatcher(exp),
        // Record information about this application in a Resource.
        tracesdk.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String(service),
            attribute.String("environment", environment),
            attribute.Int64("ID", id),
        )),
    )
    return tp, nil
}
```

And the `main()` function that:
* connects to the Jaeger collector you previously deployed
* create a HTTP server that listening on port `8080`
* and creates and sends a span each time the / HTTP route will be called:

```go
func main() {

    // Tracer
    tp, err := tracerProvider("http://localhost:14268/api/traces")
    if err != nil {
        log.Fatal(err)
    }

    // Register our TracerProvider as the global so any imported
    // instrumentation in the future will default to using it.
    otel.SetTracerProvider(tp)

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Cleanly shutdown and flush telemetry when the application exits.
    defer func(ctx context.Context) {
        // Do not make the application hang when it is shutdown.
        ctx, cancel = context.WithTimeout(ctx, time.Second*5)
        defer cancel()
        if err := tp.Shutdown(ctx); err != nil {
            log.Fatal(err)
        }
    }(ctx)

    tr := tp.Tracer("component-main")

    ctx, span := tr.Start(ctx, "hello")
    defer span.End()

    // HTTP Handlers
    helloHandler := func(w http.ResponseWriter, r *http.Request) {
        // Use the global TracerProvider
        tr := otel.Tracer("hello-handler")
        _, span := tr.Start(ctx, "hello")
        span.SetAttributes(attribute.Key("mykey").String("value"))
        defer span.End()

        yourName := os.Getenv("MY_NAME")
        fmt.Fprintf(w, "Hello %q!", yourName)
    }

    otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello")

    http.Handle("/", otelHandler)

    log.Println("Listening on localhost:8080")

    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

When the HTTP route is called, we print a message with "Hello" and your name that we retrieves in "MY_NAME" environment variable.

## Let's test it locally

First, locally, we have to define the environment variable and run your app:

```bash
$ export MY_NAME=scraly ; go run main.go
2022/11/08 19:07:54 Listening on localhost:8080
```

Make a call in our HTTP server:

```bash
$ curl localhost:8080
Hello "scraly"!
```

Let's watch our Jaeger UI again. Cool, a new `go-gopher-opentelemetry` service appear. Select it and click on `Find Traces` button:

![Jaeger new span](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnk98f9mbebd9xougncp.png)

You can now click in a trace and visualize useful information.

![trace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oacez97tbddzkwikwyoh.png)

In the trace, you can see the `environement` and `id` constants we defined are visible in the trace.

## Conclusion

As you have seen in this article and previous articles, it's possible to create applications in Go: CLI, REST API... and also useful apps ready for production with distributed tracing that you can link to other tools.

All the code of our instrumented app is available in: https://github.com/scraly/learning-go-by-examples/tree/main/go-gopher-opentelemetry

In the following articles we will create others kind/types of applications in Go.

Hope you'll like it.