🐹 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:
| Component | Setup | Auto-Traced? |
|---|---|---|
| HTTP Endpoints | Add middleware (1 line) | ✓ Yes |
| Database Queries | GORM plugin or otelsql wrapper | ✓ Yes |
| HTTP Client Calls | Wrap http.Client transport | ✓ Yes |
| Redis Operations | Add Redis hook | ✓ Yes |
| gRPC Calls | Add interceptor | ✓ Yes |
| MongoDB Queries | Use otelmongo driver | ✓ Yes |
| Custom Business Logic | Manual 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-serviceLoad 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?
- Verify your API key is correct and not revoked
- Check the endpoint URL:
https://app.tracekit.dev(no http:// prefix in WithEndpoint) - Ensure
WithInsecure()is set for local development (no TLS) - Check application logs for OpenTelemetry errors
- 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.