Menu
OpenTelemetry OpenTelemetry instrumentation Manual instrumentation of Go applications

Manual instrumentation of Go applications with OpenTelemetry Metrics SDK

To start recording and processing metrics from Go applications they have to be instrumented first. There are two types of instrumentation - automatic, and manual. To effectively instrument applications and produce high-quality metrics data you have to be familiar with both types of instrumentation. In this topic, we provide a high-level overview of how to manually instrument Go applications with the OpenTelemetry Go Metrics SDK.

Dependencies

To start instrumenting the Go application or library users should first install the OpenTelemetry Metrics API and OpenTelemetry Metrics SDK packages:

go get go.opentelemetry.io/otel/metric /
  go.opentelemetry.io/otel/sdk/metric 

Go Metric SDK components

To start recording measurements for Go applications, users should configure all required SDK components, such as meter providers, meters, views, metrics readers, and exporters. Once the listed components are configured, users can start creating instruments that are used to record measurements. Let’s have a look at each component.

MeterProvider

MeterProvider handles the creation of Meters. All Meters created by an instance of MeterProvider will be associated with the same Resource, have the same Views, and pass produced metrics to configured Readers.

import (
    // ...
    "go.opentelemetry.io/otel/sdk/metric"
    // ...
)

//...

provider := metric.NewMeterProvider(
    metric.WithResource(...),
    metric.WithReader(...),
    metric.WithViews(...),
)

See the MeterProvider section in the OpenTelemetry specification for additional information.

Meter

Meter is responsible for creating Instruments. Meter instances are always created via MeterProvider. To create a Meter instance provide a name (use Go module name as the name of Meter), an optional version and schema URL arguments to the MeterProvider's Meter method.

import (
    // ...
    "go.opentelemetry.io/otel/metric"
    // ...
)

//...

meter := provider.Meter(
	"go.opentelemetry.io/otel/metric/example"
	metric.WithInstrumentationVersion(...),
	metric.WithSchemaURL(...), 
)

Once the Meter instance is initialized, it allows us to use Instruments factories that are used to create Instruments. The following factories are used to get synchronous or asynchronous Instruments of either integer or float data type:

  • Meter#AsyncInt64()
  • Meter#AsyncFloat64()
  • Meter#SyncInt64()
  • Meter#SyncFloat64()

The Meter instance is also used to register callback functions to record observations for asynchronous Instruments:

  • Meter#RegisterCallback([]instrument.Asynchronous{}, func(ctx context.Context) {})

See the Meter creation section for additional information.

Instruments

Instruments are used to report Measurements. Each Instrument has a name, an instrument kind, an optional description and a unit of measure. Each Instrument is associated with a Meter through which it was created.

Instruments can be synchronous or asynchronous. Synchronous instruments are supposed to be used in line with application logic. Measurements recorded by synchronous instruments can be associated with Context.

Asynchronous instruments allow users to register the callback function that will be invoked only on demand. Measurements recorded by asynchronous instruments cannot be associated with Context.

The Go Metrics SDK supports the following instrument kinds defined by the OpenTelemetry specification:

Instrument KindSyncAsync
Counter
UpDownCounter
Histogram
Gauge

To create an instance of Instrument choose the instrument factory that satisfies your requirements, then choose the instrument kind by invoking the corresponding factory method and provide required the arguments to create Instrument. For example, to create a synchronous integer Counter do the following:

import (
    // ...
    "go.opentelemetry.io/otel/metric/instrument"
    // ...
)

// ...

counter, err := meter.SyncInt64().Counter(
	"counter",
    instrument.WithDescription("..."),
    instrument.WithUnit("..."), 
)
if err != nil {
	// ...
}

For more detailed information on Instruments, please refer to the Instruments section of the OpenTelemetry specification.

Reporting Measurements

A Measurement represents a data point produced by instrumentation. Each Measurement contains a value, an optional set of attributes, and an associated Context if applicable.

Synchronous and asynchronous Instruments record measurements in different ways. As it was mentioned above, to record measurements with synchronous instruments we should inline it with application logic. Please see an example of the measurement recording by synchronous float UpDownCounter:

import (
    // ...
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/metric/instrument"
    // ...
)

// ...

ctx := context.Background()

counter, err := meter.SyncFloat64().UpDownCounter(
	"UpDownCounter",
    instrument.WithDescription("..."),
    instrument.WithUnit("..."), 
)
if err != nil {
	// ...
}

// ... application logic

attrs := []attribute.KeyValue{
    attribute.Key("...").String("..."),
}
counter.Add(ctx, 1, attrs...)

To observe measurements with asynchronous instruments it must be registered in a callback. Here is an example of using the integer Gauge to observe measurements of some rare or expensive operations:

import (
    // ...
    "go.opentelemetry.io/otel/metric/instrument"
    // ...
)

// ...

gauge, err := meter.AsyncInt64().Gauge(
    "gauge",
    instrument.WithDescription("..."),
    instrument.WithUnit("..."),
)
if err != nil {
    // ...
}

// ... application logic

err = meter.RegisterCallback([]instrument.Asynchronous{gauge}, func(ctx context.Context) {
	mesurement := // result of some rare operation
    gauge.Observe(ctx, mesurement)
})
if err != nil {
    // ...
}

Views

Views allow users to customize metrics that are produced by the SDK. It can be used to match metrics and modify their names, descriptions, aggregation types, and attributes. Also, it is used to filter out certain metrics produced by instrumentation. Views are usually most useful when working with existing instrumented libraries to modify or filter out produced metrics, therefore, we won’t cover Views in this manual.

For more information on Views, please refer to the View section of the OpenTelemetry specification.

MetricReader and MetricExporter

We talked about most of the SDK components and know how to record Measurements using Instruments. Now produced telemetry data has to be sent to some agent or to an observability platform. The SDK handles that with the help of MetricReader and MetricExporter components.

MetricReader is the interface between the Metrics SDK and an exporter. It collects recorded measurements from SDK and hands them over to the exporter either on demand or periodically. MetricReader is always associated with MetricExporter.

MetricExporter is a protocol specific component that supports sending or emitting produced by the SDK telemetry data.

OpenTelemetry Go Metrics SDK officially supports the following exporters:

See an example of the MeterProvided configuration that makes collected telemetry data available for pulling via the Prometheus exporter and sends it periodically to an upstream service that can receive data via HTTP in the OTLP format.

import (
    // ...
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
    "go.opentelemetry.io/otel/exporters/prometheus"
    "go.opentelemetry.io/otel/sdk/metric"
    // ...
)

// ...

// Push-based periodic OTLP exporter
otlpExporter, err := otlpmetrichttp.New(ctx)
if err != nil {
    // ...
}

// Pull-based Prometheus exporter
prometheusExporter, err := prometheus.New()
if err != nil {
    // ...
}

meterProvider := metric.NewMeterProvider(
    metric.WithResource(resources),
    metric.WithReader(metric.NewPeriodicReader(otlpExporter)),
    metric.WithReader(prometheusExporter),
)

Example

Let’s take a look at the example of a simple HTTP Go application with a single /health endpoint that is instrumented with Go Metrics SDK. We instrumented the application with two synchronous instruments: Counter and Histogram which record the number of incoming requests and their durations. Recorded observations will be periodically exported to the configured upstream service using the OTLP protocol via HTTP.

Instrumentation dependencies:

go get go.opentelemetry.io/otel/metric /
  go.opentelemetry.io/otel/sdk/metric /
  go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp

In this example telemetry data is exported in the OTLP format via HTTP, therefore, you can use anything that can receive the OTLP data format via HTTP as the
upstream service. Some options are:

To configure the OTLP exporter, refer to Configure OpenTelemetry instrumentation via environment variables.

package main

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

	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/metric/instrument"
	"go.opentelemetry.io/otel/metric/unit"
	sdk "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
)

func main() {
	ctx := context.Background()

	resources := resource.NewWithAttributes(
		semconv.SchemaURL,
		semconv.ServiceNameKey.String("service"),
		semconv.ServiceVersionKey.String("v0.0.0"),
	)

	// Instantiate the OTLP HTTP exporter
	exporter, err := otlpmetrichttp.New(ctx)
	if err != nil {
		log.Fatalln(err)
	}

	// Instantiate the OTLP HTTP exporter
	meterProvider := sdk.NewMeterProvider(
		sdk.WithResource(resources),
		sdk.WithReader(sdk.NewPeriodicReader(exporter)),
	)
	defer func() {
		err := meterProvider.Shutdown(context.Background())
		if err != nil {
			log.Fatalln(err)
		}
	}()

	// Create an instance on a meter for the given instrumentation scope
	meter := meterProvider.Meter(
		"github.com/.../example/manual-instrumentation",
		metric.WithInstrumentationVersion("v0.0.0"),
	)

	// Create two synchronous instruments: counter and histogram
	requestCount, err := meter.SyncInt64().Counter(
		"request_count",
		instrument.WithDescription("Incoming request count"),
		instrument.WithUnit("request"),
	)
	if err != nil {
		log.Fatalln(err)
	}
	requestDuration, err := meter.SyncFloat64().Histogram(
		"duration",
		instrument.WithDescription("Incoming end to end duration"),
		instrument.WithUnit(unit.Milliseconds),
	)
	if err != nil {
		log.Fatalln(err)
	}

	http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
		requestStartTime := time.Now()

		_, _ = w.Write([]byte("UP"))

		elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond)

		// Record measurements
		attrs := semconv.HTTPServerMetricAttributesFromHTTPRequest("", req)
		requestCount.Add(ctx, 1, attrs...)
		requestDuration.Record(ctx, elapsedTime, attrs...)
	})

	http.ListenAndServe(":8081", nil)
}