🐹 Go Integration Guide

Learn how to instrument your Go applications with OpenTelemetry to send distributed traces to TraceKit.

90% Automatic Tracing!

With the right libraries, most of your application will be traced automatically with minimal setup. No need to manually instrument every function.

📋

Prerequisites

  • • Go 1.19 or higher
  • • An active TraceKit account
  • • A generated API key from the API Keys page

🔍 What Gets Traced Automatically?

With proper setup, these operations are traced automatically with zero manual instrumentation:

ComponentSetupAuto-Traced?
HTTP EndpointsAdd middleware (1 line)✓ Yes
Database QueriesGORM plugin or otelsql wrapper✓ Yes
HTTP Client CallsWrap http.Client transport✓ Yes
Redis OperationsAdd Redis hook✓ Yes
gRPC CallsAdd interceptor✓ Yes
MongoDB QueriesUse otelmongo driver✓ Yes
Custom Business LogicManual spans (optional)Manual

📦 Installation

Install the required OpenTelemetry packages:

go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin

⚙️ Basic Setup

Create a tracing initialization function in your application:

1. Create tracing.go

package tracing

import (
    "context"
    "crypto/tls"

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

func InitTracer(serviceName, endpoint, apiKey string, useSSL bool) (func(), error) {
    ctx := context.Background()

    // Configure OTLP exporter options
    var opts []otlptracehttp.Option
    opts = append(opts,
        otlptracehttp.WithEndpoint(endpoint),
        otlptracehttp.WithURLPath("/v1/traces"),
        otlptracehttp.WithHeaders(map[string]string{
            "X-API-Key": apiKey,
        }),
    )

    // Configure TLS
    if useSSL {
        opts = append(opts, otlptracehttp.WithTLSClientConfig(&tls.Config{}))
    } else {
        opts = append(opts, otlptracehttp.WithInsecure())
    }

    // Create OTLP exporter
    exporter, err := otlptracehttp.New(ctx, opts...)
    if err != nil {
        return nil, err
    }

    // Create trace provider
    tp := trace.NewTracerProvider(
        trace.WithBatcher(exporter),
        trace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String(serviceName),
            semconv.ServiceVersionKey.String("1.0.0"),
        )),
    )

    // Set global trace provider
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.TraceContext{})

    // Return cleanup function
    return func() {
        _ = tp.Shutdown(ctx)
    }, nil
}

2. Initialize in main.go

package main

import (
    "log"
    "os"

    "yourapp/tracing"
)

func main() {
    // Initialize OpenTelemetry tracing
    cleanup, err := tracing.InitTracer(
        "my-service",              // Service name
        "{ appURL }",          // TraceKit endpoint
        os.Getenv("CONTEXTIO_API_KEY"), // API key from environment
        false,                     // Use SSL (false for local development)
    )
    if err != nil {
        log.Fatalf("Failed to initialize tracer: %v", err)
    }
    defer cleanup()

    // Your application code here...
}

🚀 Framework Integration

TraceKit works seamlessly with popular Go web frameworks through OpenTelemetry instrumentation.

Gin Framework

For Gin, use the otelgin middleware:

package main

import (
    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

func main() {
    // Initialize tracing (as shown above)
    cleanup, _ := tracing.InitTracer(...)
    defer cleanup()

    // Create Gin router
    r := gin.Default()

    // Add OpenTelemetry middleware
    r.Use(otelgin.Middleware("my-service"))

    // Define routes - they're automatically traced!
    r.GET("/api/users", func(c *gin.Context) {
        c.JSON(200, gin.H{"users": []string{"alice", "bob"}})
    })

    r.Run(":8080")
}

net/http (Standard Library)

For standard library HTTP servers:

package main

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
    // Initialize tracing (as shown above)
    cleanup, _ := tracing.InitTracer(...)
    defer cleanup()

    // Wrap your handlers with otelhttp
    mux := http.NewServeMux()

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    mux.Handle("/", otelhttp.NewHandler(handler, "root"))

    http.ListenAndServe(":8080", mux)
}

Echo Framework

For Echo, use the otelecho middleware:

package main

import (
    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
)

func main() {
    // Initialize tracing (as shown above)
    cleanup, _ := tracing.InitTracer(...)
    defer cleanup()

    e := echo.New()

    // Add OpenTelemetry middleware
    e.Use(otelecho.Middleware("my-service"))

    e.GET("/", func(c echo.Context) error {
        return c.String(200, "Hello, World!")
    })

    e.Start(":8080")
}

Automatic Instrumentation Libraries

These libraries automatically create child spans for common operations. Set them up once, and every call is traced automatically.

Database Queries

Automatically trace all database operations:

GORM (PostgreSQL, MySQL, SQLite)

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "go.opentelemetry.io/contrib/instrumentation/gorm.io/otelgorm"
)

func initDB() (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    // Add OpenTelemetry plugin
    if err := db.Use(otelgorm.NewPlugin()); err != nil {
        return nil, err
    }

    return db, nil
}

Now every db.Find(), db.Create(), db.Update() call creates a span automatically!

database/sql with pgx

import (
    "database/sql"

    "github.com/jackc/pgx/v5/stdlib"
    "github.com/XSAM/otelsql"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

func initDB() (*sql.DB, error) {
    // Register with OpenTelemetry instrumentation
    driverName, err := otelsql.Register("pgx",
        otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
    )
    if err != nil {
        return nil, err
    }

    db, err := sql.Open(driverName, dsn)
    if err != nil {
        return nil, err
    }

    return db, nil
}

HTTP Client Calls

Automatically trace all outgoing HTTP requests:

import (
    "net/http"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// Create an HTTP client with OpenTelemetry transport (one-time setup)
var httpClient = &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

// Now use it for all HTTP calls - automatically traced!
func callExternalAPI(ctx context.Context) (*Response, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/users", nil)

    // This HTTP call is automatically traced as a child span
    resp, err := httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // Parse response...
    return parseResponse(resp)
}

Every HTTP call made with this client is automatically traced, including request/response details. The spans will appear as children of your current operation.

Redis Operations

Trace Redis commands automatically with go-redis:

import (
    "github.com/redis/go-redis/v9"
    "github.com/redis/go-redis/extra/redisotel/v9"
)

func initRedis() *redis.Client {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    // Enable OpenTelemetry instrumentation
    if err := redisotel.InstrumentTracing(rdb); err != nil {
        panic(err)
    }

    return rdb
}

// Usage - all operations are automatically traced!
func cacheUser(ctx context.Context, rdb *redis.Client, userID string, data string) error {
    // This Redis SET is automatically traced
    return rdb.Set(ctx, "user:"+userID, data, time.Hour).Err()
}

func getUser(ctx context.Context, rdb *redis.Client, userID string) (string, error) {
    // This Redis GET is automatically traced
    return rdb.Get(ctx, "user:"+userID).Result()
}

All Redis operations (GET, SET, HGET, etc.) are now traced automatically.

gRPC Calls

Automatically trace gRPC server and client calls:

Server Side

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func main() {
    // Initialize tracing first...
    cleanup, _ := tracing.InitTracer(...)
    defer cleanup()

    // Create gRPC server with OpenTelemetry interceptor
    s := grpc.NewServer(
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
    )

    // Register your services
    pb.RegisterYourServiceServer(s, &yourService{})

    // All RPC calls are now automatically traced!
    s.Serve(lis)
}

Client Side

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func createGRPCClient() (*grpc.ClientConn, error) {
    // Create gRPC client with OpenTelemetry interceptor
    conn, err := grpc.Dial(
        "localhost:50051",
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
        grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    )
    if err != nil {
        return nil, err
    }

    // All outgoing RPC calls are automatically traced!
    return conn, nil
}

MongoDB Queries

Automatically trace MongoDB operations:

import (
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo"
)

func initMongo(ctx context.Context) (*mongo.Client, error) {
    // Create client options with OpenTelemetry monitor
    opts := options.Client().
        ApplyURI("mongodb://localhost:27017").
        SetMonitor(otelmongo.NewMonitor())

    client, err := mongo.Connect(ctx, opts)
    if err != nil {
        return nil, err
    }

    return client, nil
}

// Usage - all operations are automatically traced!
func findUsers(ctx context.Context, client *mongo.Client) ([]User, error) {
    coll := client.Database("mydb").Collection("users")

    // This MongoDB query is automatically traced
    cursor, err := coll.Find(ctx, bson.M{"status": "active"})
    if err != nil {
        return nil, err
    }

    var users []User
    cursor.All(ctx, &users)
    return users, nil
}

All MongoDB queries, inserts, updates, and deletes are automatically traced.

Message Queues

Kafka (Sarama)

import (
    "github.com/IBM/sarama"
    "go.opentelemetry.io/contrib/instrumentation/github.com/IBM/sarama/otelsarama"
)

func createKafkaProducer() (sarama.SyncProducer, error) {
    config := sarama.NewConfig()
    config.Producer.Return.Successes = true

    producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
    if err != nil {
        return nil, err
    }

    // Wrap with OpenTelemetry instrumentation
    producer = otelsarama.WrapSyncProducer(config, producer)

    return producer, nil
}

// Usage - all messages are automatically traced!
func publishMessage(ctx context.Context, producer sarama.SyncProducer, msg string) error {
    message := &sarama.ProducerMessage{
        Topic: "user-events",
        Value: sarama.StringEncoder(msg),
    }

    // This publish is automatically traced
    _, _, err := producer.SendMessage(message)
    return err
}

RabbitMQ (amqp091-go)

import (
    amqp "github.com/rabbitmq/amqp091-go"
    "go.opentelemetry.io/contrib/instrumentation/github.com/rabbitmq/amqp091-go/otelmqp"
)

func publishToQueue(ctx context.Context, ch *amqp.Channel, msg string) error {
    // Wrap channel with OpenTelemetry instrumentation
    otlChannel := otelmqp.NewChannel(ch)

    // Publish message - automatically traced!
    err := otlChannel.PublishWithContext(
        ctx,
        "",          // exchange
        "my-queue",  // routing key
        false,       // mandatory
        false,       // immediate
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(msg),
        },
    )
    return err
}

// Consumer side
func consumeFromQueue(ctx context.Context, ch *amqp.Channel) error {
    otlChannel := otelmqp.NewChannel(ch)

    // Consume messages - automatically traced!
    msgs, err := otlChannel.Consume(
        "my-queue", // queue
        "",         // consumer
        true,       // auto-ack
        false,      // exclusive
        false,      // no-local
        false,      // no-wait
        nil,        // args
    )
    if err != nil {
        return err
    }

    for msg := range msgs {
        // Process message with tracing context
        processMessage(msg)
    }
    return nil
}

🔧 Manual Instrumentation (Optional)

For custom business logic that isn't covered by auto-instrumentation libraries, you can manually create spans. This is optional and only needed for specific operations you want to measure.

package main

import (
    "context"

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

func processOrder(ctx context.Context, orderID string) error {
    // Get tracer
    tracer := otel.Tracer("my-service")

    // Start a parent span
    ctx, span := tracer.Start(ctx, "processOrder")
    defer span.End()

    // Add attributes to the span
    span.SetAttributes(
        attribute.String("order.id", orderID),
        attribute.String("order.status", "processing"),
    )

    // Create nested child spans - they automatically inherit from parent via ctx
    if err := validateOrder(ctx, orderID); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return err
    }

    if err := chargePayment(ctx, orderID); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return err
    }

    // Mark span as successful
    span.SetStatus(codes.Ok, "Order processed successfully")
    return nil
}

func validateOrder(ctx context.Context, orderID string) error {
    tracer := otel.Tracer("my-service")

    // Child span - automatically linked to parent via ctx
    ctx, span := tracer.Start(ctx, "validateOrder")
    defer span.End()

    span.SetAttributes(attribute.String("order.id", orderID))

    // Validation logic here...
    span.SetStatus(codes.Ok, "Validation successful")
    return nil
}

func chargePayment(ctx context.Context, orderID string) error {
    tracer := otel.Tracer("my-service")

    // Another child span - also linked to parent via ctx
    ctx, span := tracer.Start(ctx, "chargePayment")
    defer span.End()

    span.SetAttributes(attribute.String("order.id", orderID))

    // Payment processing logic here...
    span.SetStatus(codes.Ok, "Payment successful")
    return nil
}

🔐 Environment Variables

Best practice: Store sensitive configuration in environment variables:

# .env
CONTEXTIO_API_KEY=ctxio_your_generated_api_key_here
CONTEXTIO_ENDPOINT={ appURL }
SERVICE_NAME=my-backend-service

Load in your application:

import "os"

cleanup, err := tracing.InitTracer(
    os.Getenv("SERVICE_NAME"),
    os.Getenv("CONTEXTIO_ENDPOINT"),
    os.Getenv("CONTEXTIO_API_KEY"),
    false, // or use os.Getenv("USE_SSL") == "true"
)

🏭 Production Configuration

⚠️

Production Checklist

  • • Use HTTPS/TLS for the OTLP endpoint
  • • Store API keys in a secrets manager (AWS Secrets Manager, HashiCorp Vault)
  • • Set appropriate service names and versions
  • • Configure resource attributes (deployment.environment, host.name, etc.)
  • • Adjust sampling rates if needed for high-traffic services

🔧 Troubleshooting

Traces Not Appearing?

  1. Verify your API key is correct and not revoked
  2. Check the endpoint URL: https://app.tracekit.dev (no http:// prefix in WithEndpoint)
  3. Ensure WithInsecure() is set for local development (no TLS)
  4. Check application logs for OpenTelemetry errors
  5. Verify TraceKit is running and accessible at port 8081

Connection Refused Errors?

Make sure TraceKit APM is running and the endpoint is correct:

# Check if TraceKit is running
curl http://{ appURL }/

# Test OTLP endpoint (should return 400 or 401, not connection refused)
curl -X POST http://{ appURL }/v1/traces

Complete Example

Here's a complete working example with Gin:

package main

import (
    "log"
    "os"

    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

    "yourapp/tracing"
)

func main() {
    // Initialize tracing
    cleanup, err := tracing.InitTracer(
        "backend-api",
        "{ appURL }",
        os.Getenv("CONTEXTIO_API_KEY"),
        false,
    )
    if err != nil {
        log.Fatalf("Failed to initialize tracer: %v", err)
    }
    defer cleanup()

    // Create Gin router
    r := gin.Default()
    r.Use(otelgin.Middleware("backend-api"))

    // Routes
    r.GET("/api/users", getUsers)
    r.POST("/api/users", createUser)

    // Start server
    log.Println("Server starting on :8080")
    r.Run(":8080")
}

func getUsers(c *gin.Context) {
    // This endpoint is automatically traced!
    c.JSON(200, gin.H{
        "users": []string{"alice", "bob", "charlie"},
    })
}

func createUser(c *gin.Context) {
    // This endpoint is automatically traced too!
    c.JSON(201, gin.H{
        "message": "User created successfully",
    })
}
🎉

You're all set!

Your Go application is now sending traces to TraceKit. Visit the Dashboard to see your traces.

🚀 Next Steps

  • Add auto-instrumentation libraries for components you use (Redis, gRPC, MongoDB, etc.)
  • Explore your traces on the Traces page to identify performance bottlenecks
  • Optionally add custom spans for specific business logic you want to measure
  • Configure sampling for high-traffic services to reduce overhead
  • Set up alert rules to get notified when issues occur
💡

Pro Tip

Start with framework middleware + database instrumentation. This usually gives you 80% coverage with just 2-3 lines of setup code. Add more auto-instrumentation libraries as needed.