OpenTelemetry is an observability framework designed to provide highly scalable and configurable instrumentation for monitoring and tracing software applications. It aims to standardize the generation, collection, and management of telemetry data like metrics, traces, and logs.
This framework supports a range of programming languages, including Go, and integrates with numerous back-end analysis tools. It offers developers and organizations flexibility in how they monitor applications.
OpenTelemetry automates the process of instrumenting applications. This means it provides libraries and agents that, once integrated into your application, automatically capture telemetry data. This data can include information about how long operations take (tracing), how often events occur (metrics), and detailed logs of what the application is doing at any given time. This can help diagnose issues, understand application behavior, and optimize performance.
Go, also known as Golang, is a statically typed, compiled programming language created by Google and designed for efficiency and ease of use. It emphasizes simplicity, high performance, and robustness, aiming to address some of the common challenges associated with large-scale software development.
Go features a minimalistic syntax that allows developers to manage complex programs with fewer lines of code compared to other languages. Its standard library, concurrency support through goroutines, and built-in garbage collection make it an appropriate choice for developing fast, scalable web applications, microservices, and distributed systems.
Integrating OpenTelemetry with Golang applications offers several advantages:
The example and code below is adapted from the OpenTelemetry documentation.
To get started with implementing OpenTelemetry in Go applications, you first need to set up your project environment. This involves creating a new directory for your project and initializing a Go module within it.
The Go module can be initiated with the command go mod init, followed by your module name. For instance, if your project is named “wheeloffortune,” the command would be:
go mod init wheeloffortune
This command creates a go.mod file in your directory, setting up the basis for managing dependencies and versions in your Go application.
The next step is to create and launch an HTTP server. This involves writing Go code in a file named main.go. The code defines a simple HTTP server that listens on port 8080 and handles requests to the /spinwheel path. Here’s how the main.go file looks:
package main import ( "log" "net/http" ) func main() { http.HandleFunc("/spinwheel", spinwheel) log.Fatal(http.ListenAndServe(":8080", nil)) }
Additionally, you need to define the behavior of your /spinwheel handler in a separate file, spinwheel.go. This handler generates a random number between 1 and 12 to simulate the spinning of a wheel of fortune and sends the result back to the client:
package main import ( "io" "log" "math/rand" "net/http" "strconv" ) func spinwheel(w http.ResponseWriter, r *http.Request) { roll := 1 + rand.Intn(12) resp := strconv.Itoa(roll) + "\n" if _, err := io.WriteString(w, resp); err != nil { log.Printf("Write failed: %v\n", err) } }
To run the server, use the command go run . (including the period) and navigate to http://localhost:8080/spinwheel in your web browser to see it in action. The output should look like this:
To integrate OpenTelemetry into your Go application, you need to add several dependencies. These include the OpenTelemetry SDK, standard exporters for metrics and traces, and instrumentation for the net/http package. Run the following command to install these packages:
go get "go.opentelemetry.io/otel" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
"go.opentelemetry.io/otel/propagation" \
"go.opentelemetry.io/otel/sdk/metric" \
"go.opentelemetry.io/otel/sdk/resource" \
"go.opentelemetry.io/otel/sdk/trace" \
"go.opentelemetry.io/otel/semconv/v1.24.0" \
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
The output should look something like:
This command ensures your project has all the necessary libraries to instrument your application with OpenTelemetry.
Initialization of the OpenTelemetry SDK is a crucial step to start exporting telemetry data (traces and metrics) from your application. You need to create a new file, otel.go, to bootstrap the OpenTelemetry pipeline.
This setup involves configuring trace and meter providers, as well as setting up a propagator. The otel.go file will look something like this:
package main
import (
// Import necessary packages
"context"
"errors"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
)
func bootstrap_sdk(ctx context.Context) (shutdown func(context.Context) error, err error) {
var shutdownFuncs []func(context.Context) error
// shutdown calls cleanup functions registered via shutdownFuncs
// The errors from the calls are joined
// Each registered cleanup will be invoked once
shutdown = func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctx))
}
shutdownFuncs = nil
return err
}
// handleErr calls shutdown for cleanup and makes sure that all errors are returned
handleErr := func(inErr error) {
err = errors.Join(inErr, shutdown(ctx))
}
// Set up propagator.
prop := newPropagator()
otel.SetTextMapPropagator(prop)
// Set up trace provider.
tracerProvider, err := newTraceProvider()
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
// Set up meter provider.
meterProvider, err := newMeterProvider()
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
return
}
func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
}
func newTraceProvider() (*trace.TracerProvider, error) {
traceExporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint())
if err != nil {
return nil, err
}
traceProvider := trace.NewTracerProvider(
trace.WithBatcher(traceExporter,
// Default is 5s. Set to 1s for demonstrative purposes.
trace.WithBatchTimeout(time.Second)),
)
return traceProvider, nil
}
func newMeterProvider() (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}
meterProvider := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// Default is 1m. Set to 3s for demonstrative purposes.
metric.WithInterval(3*time.Second))),
)
return meterProvider, nil
}
The initialization process sets up the necessary components for tracing and metrics, including exporters that output data to the console in this example.
To capture telemetry data from your HTTP server, modify the main.go file to incorporate OpenTelemetry instrumentation. This involves wrapping your HTTP handlers with the otelhttp middleware, which automatically captures HTTP request metrics and traces.
The modification includes setting up the OpenTelemetry SDK in your main function and using the otelhttp.NewHandler to instrument your server (some details omitted for brevity and replaced with an ellipsis):
package main import ( … "context" "errors" "log" "net" "net/http" "os" "os/signal" "time" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) func main() { if err := run(); err != nil { log.Fatalln(err) } } func run() (err error) { // Handle shutdown scenarios ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() otelShutdown, err := bootstrap_sdk(ctx) if err != nil { return } defer func() { err = errors.Join(err, otelShutdown(context.Background())) }() // Start HTTP server. srv := &http.Server{ Addr: ":8080", BaseContext: func(_ net.Listener) context.Context { return ctx }, ReadTimeout: time.Second, WriteTimeout: 10 * time.Second, Handler: newHTTPHandler(), } srvErr := make(chan error, 1) go func() { srvErr <- srv.ListenAndServe() }() // Handle interruptions select { case err = <-srvErr: return case <-ctx.Done(): stop() } err = srv.Shutdown(context.Background()) return } func newHTTPHandler() http.Handler { mux := http.NewServeMux() handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) { handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc)) mux.Handle(pattern, handler) } handleFunc("/spinwheel", spinwheel) // Add HTTP instrumentation for the whole server handler := otelhttp.NewHandler(mux, "/") return handler }
Finally, to run your instrumented application, make sure to tidy up your module dependencies with go mod tidy. Then, set the OTEL_RESOURCE_ATTRIBUTES environment variable to provide resource attributes like service name and version.
Run your application with go run and observe the telemetry data generated by both automatic and custom instrumentation in your console.
Here is an example of the output:
Data plus context are key to supercharging observability using OpenTelemetry. As Coralogix is open-source friendly, we support OpenTelemetry to get your app’s telemetry data (traces, logs, and metrics) as requests travel through its many services and other infrastructure. You can easily use OpenTelemetry’s APIs, SDKs, and tools to collect and export observability data from your environment directly to Coralogix.