Documentation

OpenTelemetry Instrumentation for GO

Overview

OpenTelemetry provides instrumentation for GO applications, enabling you to collect telemetry data such as traces and metrics. Although Lumigo does not offer its own OpenTelemetry Distribution for GO, you can still configure it. This guide explains how to set up OpenTelemetry GO Instrumentation and configure it to report data to Lumigo.

Prerequisites

Before you begin, ensure you have the following:

  • Go 1.18+
  • OpenTelemetry-Go SDK
  • Logrus (for logging)

Installation Steps

1. Initialize the OTLP Log Exporter

To send logs to Lumigo, we use an OpenTelemetry Protocol (OTLP) HTTP log exporter. This exporter will transmit logs to Lumigo's designated endpoint using your Lumigo token.

The key configuration parameters for the exporter are:

  • Endpoint: The Lumigo OTLP endpoint is ga-otlp.lumigo-tracer-edge.golumigo.com. This is where logs are sent.
  • Authentication: The exporter requires your Lumigo token for authentication. This token should be added to the HTTP headers as Authorization: LumigoToken <your_token>.

Use the OpenTelemetry Protocol (OTLP) HTTP log exporter to send logs to Lumigo:

func initLogExporter() {
	exporter, err := otlploghttp.New(context.Background(),
		otlploghttp.WithEndpoint("ga-otlp.lumigo-tracer-edge.golumigo.com"),
		otlploghttp.WithURLPath("/v1/logs"),
		otlploghttp.WithHeaders(map[string]string{
			"Authorization": "LumigoToken t_xxxxxxxxxxxxxxxx", // Replace with your Lumigo token
		}),
	)
	if err != nil {
		log.Fatalf("Failed to create log exporter: %v", err)
	}
}

This exporter sends logs directly to the Lumigo OTLP endpoint. It attaches an authentication token to every request, allowing Lumigo to ingest and analyze your logs.

2. Set Up the Log Provider

The Log Provider in OpenTelemetry is responsible for managing the lifecycle of the loggers and ensuring logs are processed and exported properly.

In this example, we configure the log provider with:

  • Batch Processor: This sends logs in batches to improve performance.
  • Resource: This defines the attributes for your application, such as the service name ("my-webserver" in this example). These attributes are attached to all logs sent to Lumigo, providing context about where the logs originated from.

Here’s how to set up the log provider:

func initLogExporter() {
	logProvider := sdklog.NewLoggerProvider(
		sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)), // Send logs in batches
	)
}

The log provider manages the creation of loggers, and its job is to ensure that the logs you create with Logrus, or any other logging library, are correctly processed and sent to Lumigo through the exporter.

3. Logging in Your Application

Once the log exporter and provider are set up, you can integrate logging into your application using the logrus package. The example below shows how to log incoming HTTP requests:

func helloHandler(w http.ResponseWriter, r *http.Request) {
	logger.WithFields(logrus.Fields{
		"method": r.Method,
		"path":   r.URL.Path,
	}).Info("Request received")

	w.Write([]byte("Hello, World!"))
}

func main() {
	// Initialize OTLP Log Exporter
	initLogExporter()

	// Setup HTTP server
	http.HandleFunc("/", helloHandler)

	port := ":8080"
	logger.WithField("port", port).Info("Starting server")
	if err := http.ListenAndServe(port, nil); err != nil {
		logger.WithError(err).Fatal("Server failed to start")
	}
}

Full example

Here is a full example of a webserver sending logs to Lumigo:

package main

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

	"github.com/sirupsen/logrus"
	"go.opentelemetry.io/contrib/bridges/otellogrus"
	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
	sdklog "go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

var logger *logrus.Logger

func initLogExporter() {
	// Define the service name and create a new resource for OTLP
	resources, err := resource.New(
		context.Background(),
		resource.WithAttributes(
			semconv.ServiceNameKey.String("my-webserver"),
		),
	)
	if err != nil {
		log.Fatalf("Failed to create resource: %v", err)
	}
	
	// Create a new OTLP log exporter that sends logs to Lumigo
	exporter, err := otlploghttp.New(context.Background(),
		otlploghttp.WithEndpoint("ga-otlp.lumigo-tracer-edge.golumigo.com"),
		otlploghttp.WithURLPath("/v1/logs"),
		otlploghttp.WithHeaders(map[string]string{
			"Authorization": "LumigoToken t_xxxxxxxxxxxxxxxx",
		}),
	)
	if err != nil {
		log.Fatalf("Failed to create log exporter: %v", err)
	}

	// Create a new logger provider with the OTLP log exporter and resource
	logProvider := sdklog.NewLoggerProvider(
		sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
		sdklog.WithResource(resources),
	)

	// Create a new logrus logger and add the OTLP log exporter as a hook
	logger = logrus.New()
	logger.AddHook(otellogrus.NewHook("test",
		otellogrus.WithLoggerProvider(logProvider),
		otellogrus.WithLevels(
			[]logrus.Level{logrus.InfoLevel},
		),
	))

	// Some formatting for the logrus logger - this is optional
	logger.SetFormatter(&logrus.JSONFormatter{})
	logger.SetOutput(os.Stdout)
	logger.SetLevel(logrus.InfoLevel)

	logger.Info("Logger started")
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	logger.WithFields(logrus.Fields{
		"method": r.Method,
		"path":   r.URL.Path,
	}).Info("Request received")

	w.Write([]byte("Hello, World!"))
}

func main() {
	initLogExporter()

	http.HandleFunc("/", helloHandler)

	port := ":8080"
	logger.WithField("port", port).Info("Starting server")
	if err := http.ListenAndServe(port, nil); err != nil {
		logger.WithError(err).Fatal("Server failed to start")
	}
}

Traces

Once logging using logrus is integrated, you can also integrate distributed tracing using Lumigo and OpenTelemetry. To initialize Lumigo Tracing:

import (
    "fmt"
    "log"
    "context"

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

var tracer trace.Tracer

func Resource() *resource.Resource {
    return resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceName("yourAppName"),
    )
}

func InitiateLumigoTracer(ctx context.Context) (func(context.Context) error, error) {
    
    options := []otlptracehttp.Option{
        otlptracehttp.WithEndpoint("ga-otlp.lumigo-tracer-edge.golumigo.com"),
        otlptracehttp.WithHeaders(map[string]string{"Authorization": "LumigoToken <yourLumigoToken>}),
    }
    
    client := otlptracehttp.NewClient(options...)
    exporter, err := otlptrace.New(ctx, client)
    if err != nil {
        return nil, fmt.Errorf("creating stdout exporter: %w", err)
    }

    tracerProvider := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(Resource()),
    )
    otel.SetTracerProvider(tracerProvider)

    tracer = tracerProvider.Tracer("tracer")

    return tracerProvider.Shutdown, nil
}
                    
func main() {
    ctx := context.Background()
    // Registers a tracer Provider globally.
    _, err := InitiateLumigoTracer(ctx)
    if err != nil {
      panic(err.Error())
    }                  

To manually create a span in your function:

_, span := tracer.Start(r.Context(), "hello-span")
    defer span.End()