Golang OpenTelemetry Instrumentation
This tutorial demonstrates how to instrument Golang applications to capture traces using OpenTelemetry and send them to Coralogix.
The OpenTelemetry-Go instrumentation for traces is currently stable, while instrumentation for metrics in mixed beta. The OpenTelemetry Go SDK is documented here.
Only manual instrumentation is supported for Golang. The examples provided here do not represent a concrete standard for instrumenting Golang applications but serve as guides for basic implementation. You should review your code base and identify the best approach to manual instrumentation for your specific project.
Prerequisites
Traces
First, we will demonstrate instrumentation for a simple web application for traces.
Setup
# create app directory
mkdir goapp
cd goapp
# initialise golang app
go mod init github.com/my/app
# create main.go
touch main.go
# note that for each new module/package added to main.go
# you will need run:
go mod tidy
Initialise Trace Provider
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http" // Import the missing http package
"os"
"time"
"github.com/coralogix/coralogix-opentelemetry-go/sampler"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"google.golang.org/grpc/credentials"
"math/rand"
)
const (
traceName = "my_trace"
coralogixEndpoint = "ingress.<span class="domain-value"></span>:443" //adjust your coralogix endpoint for your environment ->https://coralogix.com/docs/coralogix-endpoints/
cxApplicationName = "go-instrumentation"
cxSubsystemName = "manual-instro-traces"
)
func init() {
ctx := context.Background()
// 1. define trace connection options
var headers = map[string]string{
"Authorization": "Bearer <your_private_key" + os.Getenv("CX_TOKEN"),
}
traceConnOpts := []otlptracegrpc.Option{
otlptracegrpc.WithTimeout(1 * time.Second),
otlptracegrpc.WithEndpoint(coralogixEndpoint),
otlptracegrpc.WithHeaders(headers),
otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{})),
}
// 2. set up a trace exporter
exporter, err := otlptracegrpc.New(ctx, traceConnOpts...)
if err != nil {
log.Fatalf("failed to create trace exporter: %v", err)
}
// 3 define span resource attributes,
// these resource attributes will be added to all Spans
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("go-manual-instro-traces-example"),
// cx.application.name and cx.subsystem.name are required for the
// spans being sent to the coralogix platform
attribute.String("cx.application.name", cxApplicationName),
attribute.String("cx.subsystem.name", cxSubsystemName),
),
)
// 4. create batch span processor
// Note: SpanProcessor is a processing pipeline for spans in the trace signal.
// SpanProcessors registered with a TracerProvider and are called at the start and end of a
// Span's lifecycle, and are called in the order they are registered.
// https://pkg.go.dev/go.opentelemetry.io/otel/sdk/trace#SpanProcessor
sp := sdktrace.NewSimpleSpanProcessor(exporter)
// 5. add span processor and resource attributes to the trace provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sampler.NewCoralogixSampler(sdktrace.AlwaysSample())),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(sp),
)
// 6. set the global trace provider
otel.SetTracerProvider(tp)
}
The code above imports the required packages and defines an init function that instantiates and configures the global trace provider. The trace provider is configured to send traces to the Coralogix Domain ingress.<span class="domain-value"></span>:443
using the SimpleSpanProcessor
. It also fetches the Coralogix Private key from the env variable CX_TOKEN
.
Add Traces and Spans
Next we will create a simple webserver that returns a random number between 0 and 6 when called.
// http handle to roll dice
func rollhanler(w http.ResponseWriter, r *http.Request) {
// 1. get tracer
tracer := otel.Tracer("cx.example.tracer")
// 2. start a span
ctx, span := tracer.Start(r.Context(), "rollhandle", trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
roll := rolldice(ctx)
// 3. add attributes to the span
// Note: these attribute could also be added using middleware
span.SetAttributes(
attribute.String("span.kind", "server"),
attribute.String("resource.name", r.Method+" "+r.URL.Path),
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.Path),
attribute.String("http.route", r.URL.Path),
attribute.String("http.target", r.URL.String()),
attribute.String("http.useragent", r.UserAgent()),
attribute.String("http.host", r.Host),
)
fmt.Fprintf(w, "rolled a %d\n", roll)
}
// rolldice returns a random number between 1 and 6
func rolldice(ctx context.Context) int {
roll := rand.Intn(6) + 1
// 1. get tracer
tracer := otel.Tracer(traceName)
// 2. start a span from context
_, span := tracer.Start(ctx, "roll-dice")
defer span.End()
// 3. add attributes to the span
span.SetAttributes(attribute.Int("dice.roll", roll))
return roll
}
func main() {
http.HandleFunc("/roll", rollhanler)
log.Println("serving... :8080/roll")
http.ListenAndServe(":8080", nil)
}
Above we have defined to 2 functions, rollhandler
which is the http handler function that is executed when the endpoint /roll
is called, and rolldice
which is a function that is used to generate the random number.
The above demonstrates a key feature and requirement of Instrumentation, managing context. When starting a trace in the rollhandle
function, a context is passed to the tracer.Start
function, this function in turn returns a new context. We then use the returned context when calling the rolldice
function. This chain of passing contexts between functions and creating traces from the context allows the tracer to establish and represent relationships between functions.
We can run the application by doing the following:
When we execute the code above, this will create the following trace on our Coralogix console:
We can see that by passing the contexts correctly between functions, we get a proper representation of the relationship between them.
Troubleshooting and Validation
If traces are not visible in the Coralogix console, you can run the following checks.
Make sure your Coralogix PrivateKey is assigned to the environment variable
CX_TOKEN
and thecoralogixEndpoint
variable repris correct for your Coralogix Domain.Traces can be written to stdout to verify if they are being generated correctly, below we define an
init()
function that initialise the tracer with astdouttrace.Exporter
that prints to stdout:
func initConsole() {
// Set up a trace exporter that writes to Stdout
// requires import: "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
exporter, err := stdouttrace.New(
stdouttrace.WithWriter(os.Stdout),
// Use human-readable output.
stdouttrace.WithPrettyPrint(),
// Do not print timestamps for the demo.
stdouttrace.WithoutTimestamps(),
)
// exporter, err = otlptrace.New()
if err != nil {
log.Fatalf("failed to create trace exporter: %v", err)
}
// Set global trace provider
resource := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceNameKey.String("manual-instro-traces"))
// define a batch span processor to export spans
bsp := sdktrace.NewBatchSpanProcessor(exporter)
// create a trace provider
traceprovider := sdktrace.NewTracerProvider(
sdktrace.WithResource(resource),
sdktrace.WithSpanProcessor(bsp),
)
// Set the Tracer Provider globally
otel.SetTracerProvider(traceprovider)
otel.SetTextMapPropagator(propagation.TraceContext{})
}
You can view more on Instrumentation in Golang using OpenTelemetry here.
Metrics
Now we will demonstrate instrumentation for metrics using the same web application.
Initialise Metric Provider
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/metric"
sdkmetrics "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"google.golang.org/grpc/credentials"
)
const (
coralogixEndpoint = "ingress.<span class="domain-value"></span>:443"
cxApplicationName = "go-instrumentation"
cxSubsystemName = "manual-instro-traces"
traceName = "cx.example.tracer"
)
func init() {
ctx := context.Background()
// 1. define metrics connection options
var headers = map[string]string{
"Authorization": "Bearer " + os.Getenv("CX_TOKEN"),
}
metricsConnOpts := []otlpmetricgrpc.Option{
otlpmetricgrpc.WithTimeout(1 * time.Second),
otlpmetricgrpc.WithEndpoint(coralogixEndpoint),
otlpmetricgrpc.WithHeaders(headers),
otlpmetricgrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{})),
}
// 2. set up a metrics exporter
metricsExporter, err := otlpmetricgrpc.New(ctx, metricsConnOpts...)
if err != nil {
log.Fatalf("failed to create metrics exporter: %v", err)
}
reader := sdkmetrics.NewPeriodicReader(metricsExporter)
// 3. create a controller
// 3. define resource attributes,
// these resource attributes will be added to all metrics
resource := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("go-manual-instro-traces-example"),
// cx.application.name and cx.subsystem.name are required for the
// metrics being sent to the coralogix platform
attribute.String("cx.application.name", cxApplicationName),
attribute.String("cx.subsystem.name", cxSubsystemName),
)
// 4. create batch metrics processor
// Note: MetricsProcessor is a processing pipeline for metrics in the metrics signal.
// MetricsProcessors registered with a MeterProvider and are called at the start and end of a
// Metric's lifecycle, and are called in the order they are registered.
// https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric#MetricsProcessor
mp := sdkmetrics.NewMeterProvider(
sdkmetrics.WithResource(resource),
sdkmetrics.WithReader(reader),
)
// 5. set the global meter provider
otel.SetMeterProvider(mp)
}
Above we define an init
function that initializes the metrics instrumentation components for the application. It defines connection options for communicating with the Coralogix platform, including authorisation headers and endpoint details. An exporter is then set up to send metrics to Coralogix, and a periodic reader reads and exports these metrics at regular intervals. A metrics processor is then established to manage the collection, batching, and export of metrics data, and this processor is set as the default global meter provider. This ensures that the application is ready to capture and relay metrics to Coralogix seamlessly.
As before, the Coralogix Private Key will be read in using an Environment variable, CX_TOKEN
HTTP Request Counter
We will implement a simple HTTP Request Counter metric. To do this, we will define a web app with middleware that increments the counter for each request received.
// requestCounterMiddleware - middleware that counts each request
func requestCounterMiddleware(reqCount metric.Int64Counter, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// increment counter
reqCount.Add(ctx, 1)
next(w, r)
}
}
This middleware function increments a counter each time an HTTP request passes through it. Its parameters are the counter (reqCount
) and the next HTTP handler (next
) in the pipeline. Upon invocation:
It retrieves the context from the incoming request.
Increments the request counter.
Proceeds to the next HTTP handler in the chain.
// http handle to roll dice
func rollhanler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "rolled a %d\n", rand.Intn(6)+1)
}
We have defined an HTTP handler function which, when invoked, simulates rolling a dice and responds with the outcome to the client.
func main() {
// define a meter
meter := otel.Meter("goapp")
// Create two synchronous instruments: counter and histogram
reqCounter, err := meter.Int64Counter(
"http.request.counter",
metric.WithDescription("HTTP Request counter"),
)
if err != nil {
log.Fatalf("failed to create request count metric: %v", err)
}
http.HandleFunc("/roll", requestCounterMiddleware(reqCounter, rollhanler))
log.Println("serving... :8080/roll")
http.ListenAndServe(":8080", nil)
}
The main function does the following:
Initializes a meter named "goapp" to produce metrics instruments.
Defines an Int64Counter named "http.request.counter", serving as a counter for incoming HTTP requests.
Registers the "/roll" endpoint, wrapping the
rollhandler
with therequestCounterMiddleware
to ensure each request is counted.
The application can be run and tested using the same method as described for the traces example.
Troubleshooting and Validation
Sent metrics can be view by visiting Grafana -> Explore -> Browse Metrics
If you wish to validate that that the application is generating metrics correctly, similar to traces, it is possible to export metrics to stdout.
First we define a stdout exporter and create a reader.
// requires import: "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
stdoutexporter, err := stdoutmetric.New()
if err != nil {
log.Fatalf("failed to create metrics exporter: %v", err)
}
stdoutreader := sdkmetrics.NewPeriodicReader(stdoutexporter)
Then add the reader to the Metric Provider.
mp := sdkmetrics.NewMeterProvider(
sdkmetrics.WithResource(resource),
sdkmetrics.WithReader(reader),
sdkmetrics.WithReader(stdoutreader), // <----
)
Metrics will be printed to stdout as well as sent to Coralogix. Note that metrics are not sent instantly when there is a delay using the Periodic Reader.
Service Flows
For customers with functional Golang OpenTelemetry instrumentation, this section guides reconfiguring the existing setup to define, report, and monitor Coralogix Service Flows.
New customers or those who haven’t configured the Golang OpenTelemetry instrumentation must follow the Setup instructions above. The steps in this section are included in those instructions.
Add to the package:
Add to the code:
Run:
Support
Need help?
Our world-class customer success team is available 24/7 to walk you through your setup and answer any questions that may come up.
Feel free to reach out to us via our in-app chat or by sending us an email at [email protected].