Skip to content

Instrument a Go App

In this guide, we will walk you through the process of setting up and using OpenTelemetry in Go. You will learn how to instrument a simple application to produce tracing telemetry with OpenTelemetry Go.

Step 1: Prerequisites

Before diving into OpenTelemetry, be sure to have Go 1.16 or newer installed on your local machine.

Step 2: Example Application

For this tutorial, we will be using an application that computes Fibonacci numbers for users.

  1. Let’s start by creating a new directory named fib. This directory will contain the Fibonacci project.
mkdir fib
cd fib

2. Create a file named fib.go in the fib directory and add the following code to it:

package main

func Fibonacci(n uint) (uint64, error) {
 if n <= 1 {
  return uint64(n), nil
 }

 var n2, n1 uint64 = 0, 1
 for i := uint(2); i < n; i++ {
  n2, n1 = n1, n1+n2
 }

 return n2 + n1, nil
}

This is the core logic around which we will create our application.

3. Create another file named app.go and add the following code to it:

package main

import (
 "context"
 "fmt"
 "io"
 "log"
)

type App struct {
 r io.Reader
 l *log.Logger
}

func NewApp(r io.Reader, l *log.Logger)*App {
 return &App{r: r, l: l}
}

func (a *App) Run(ctx context.Context) error {
 for {
  n, err := a.Poll(ctx)
  if err != nil {
   return err
  }
  a.Write(ctx, n)
 }
}

func (a *App) Poll(ctx context.Context) (uint, error) {
 a.l.Print("What Fibonacci number would you like to know: ")

 var n uint
 _, err := fmt.Fscanf(a.r, "%d\n", &n)
 return n, err
}

func (a *App) Write(ctx context.Context, n uint) {
 f, err := Fibonacci(n)
 if err != nil {
    a.l.Printf("Fibonacci(%d): %v\n", n, err)
 } else {
    a.l.Printf("Fibonacci(%d) = %d\n", n, f)
 }
}

4. Create a new file named main.go, which will be used to run the application. Add the following code to it:

package main

import (
 "context"
 "log"
 "os"
 "os/signal"
)

func main() {
 l := log.New(os.Stdout, "", 0)

 sigCh := make(chan os.Signal, 1)
 signal.Notify(sigCh, os.Interrupt)

 errCh := make(chan error)
 app := NewApp(os.Stdin, l)
 go func() {
  errCh <- app.Run(context.Background())
 }()

 select {
 case <-sigCh:
  l.Println("\ngoodbye")
  return
 case err := <-errCh:
  if err != nil {
   l.Fatal(err)
  }
 }
}

5. Run the following command in your terminal in the fib directory to initialize it as a Go module:

go mod init fib

6. Run the application using the following command:

go run .

Step 3: Trace Instrumentation

Now that our application code is ready, we will be using the OpenTelemetry Trace API from the go.opentelemetry.io/otel/trace package to instrument the application code.

  1. Run the following command in your working directory to install the necessary packages for the Trace API:
go get go.opentelemetry.io/otel \
       go.opentelemetry.io/otel/trace

2. Next, add imports to your application by adding the following code to the app.go file:

import (
 "context"
 "fmt"
 "io"
 "log"
 "strconv"

 "go.opentelemetry.io/otel"
 "go.opentelemetry.io/otel/attribute"
 "go.opentelemetry.io/otel/trace"
)

3. The Trace API provides a Tracer to create traces. Add the following code line in app.go to uniquely identify your application the Tracer:

// name is the Tracer name used to identify this instrumentation library.
const name = "fib"

4. Instrument the Run method in your app.go file by updating it with the following code:

// Run starts polling users for Fibonacci number requests and writes results.
func (a *App) Run(ctx context.Context) error {
 for {
  // Each execution of the run loop, we should get a new "root" span and context.
  newCtx, span := otel.Tracer(name).Start(ctx, "Run")

  n, err := a.Poll(newCtx)
  if err != nil {
   span.End()
   return err
  }

  a.Write(newCtx, n)
  span.End()
 }
}

5. Instrument the Poll method in your app.go file by updating it with the following code:

// Poll asks a user for input and returns the request.
func (a *App) Poll(ctx context.Context) (uint, error) {
 _, span := otel.Tracer(name).Start(ctx, "Poll")
 defer span.End()

 a.l.Print("What Fibonacci number would you like to know: ")

 var n uint
 _, err := fmt.Fscanf(a.r, "%d\n", &n)

 // Store n as a string to not overflow an int64.
 nStr := strconv.FormatUint(uint64(n), 10)
 span.SetAttributes(attribute.String("request.n", nStr))

 return n, err
}

6. Instrument the Write method in your app.go file by updating it with the following code:

// Write writes the n-th Fibonacci number back to the user.
func (a *App) Write(ctx context.Context, n uint) {
 var span trace.Span
 ctx, span = otel.Tracer(name).Start(ctx, "Write")
 defer span.End()

 f, err := func(ctx context.Context) (uint64, error) {
  _, span := otel.Tracer(name).Start(ctx, "Fibonacci")
  defer span.End()
  return Fibonacci(n)
 }(ctx)
 if err != nil {
  a.l.Printf("Fibonacci(%d): %v\n", n, err)
 } else {
  a.l.Printf("Fibonacci(%d) = %d\n", n, f)
 }
}

Step 4: SDK Installation

The OpenTelemetry Go project provides go.opentelemetry.io/otel/sdk SDK package that implements the Trace API that we used in the previous step to instrument the application code.

  1. Run the following command in the fib directory to install the trace STDOUT exporter and the SDK:
go get go.opentelemetry.io/otel/sdk \
       go.opentelemetry.io/otel/exporters/stdout/stdouttrace \
       go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp

2. Next, add the necessary imports to the main.go file.

import (
 "context"
 "io"
 "log"
 "os"
 "os/signal"

 "go.opentelemetry.io/otel"
 "go.opentelemetry.io/otel/attribute"
 "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
 "go.opentelemetry.io/otel/sdk/resource"
 "go.opentelemetry.io/otel/sdk/trace"
 semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

Step 5: Create a Console Exporter

Exporter packages are required to send the telemetry data to the target system or KloudMate in our case. To create and initialize a console exporter, add the following function to your main.go file:

// KMTraceExporter returns an exporter with KM endpoint.
func KMTraceExporter() (*sdktrace.SpanExporter, error) {
  return otlptracehttp.New(context.Background(),
    otlptracehttp.WithEndpoint("otel.kloudmate.com:4318"),
    otlptracehttp.WithHeaders(map[string]string{
      "Authorization": "",
    }))
}

Step 6: Create a Resource

When Telemetry data is produced, it is important to be able to identify what service or service instance the data is coming from in order to solve issues with the service. To represent the entity producing the telemetry data, OpenTelemetry uses a Resource.

To create a Resource for your application, add the following function to the main.go file:

// newResource returns a resource describing this application.
func newResource() *resource.Resource {
 r, _ := resource.Merge(
  resource.Default(),
  resource.NewWithAttributes(
   semconv.SchemaURL,
   semconv.ServiceName("fib"),
   semconv.ServiceVersion("v0.1.0"),
   attribute.String("environment", "demo"),
  ),
 )
 return r
}

Step 7: Install a Tracer Provider

The instrumented code will produce the telemetry data using Tracer. The Trace provider is required to send the telemetry data from these Tracers to the exporter.

To configure a TraceProvider, update your main function in the main.go file with the following:

func main() {
 l := log.New(os.Stdout, "", 0)

 exp, err := KMTraceExporter()
 if err != nil {
  l.Fatal(err)
 }

 tp := trace.NewTracerProvider(
  trace.WithBatcher(exp),
  trace.WithResource(newResource()),
 )
 defer func() {
  if err := tp.Shutdown(context.Background()); err != nil {
   l.Fatal(err)
  }
 }()
 otel.SetTracerProvider(tp)

    /*…*/
}

Step 8: Run the Instrumented App

You should now have a working application that produces trace telemetry data. Run the application using the following command:

go run .
NameDescription
go_memory_usedMemory used by the Go runtime.
go_memory_typeThe type of memory.
go_memory_limitGo runtime memory limit configured by the user, if a limit exists.
go_memory_allocatedMemory allocated to the heap by the application.
go_memory_allocationsCount of allocations to the heap by the application.
go_memory_gc_goalHeap size target for the end of the GC cycle.
go_goroutine_countCount of live goroutines.
go_processor_limitThe number of OS threads that can execute user-level Go code simultaneously.
go_schedule_durationThe time goroutines have spent in the scheduler in a runnable state before actually running. 
go_config_gogcHeap size target percentage configured by the user, otherwise 100.

When you run the instrumented application, the traces created from running the application will be exported to Kloudmate. To learn more, explore the official documentation of the OpenTelemetry Go Project.


Source URL for the example application: https://opentelemetry.io/docs/instrumentation/go/getting-started/