Gorilla Mux
Monitor Gorilla Mux applications with OpenTelemetry instrumentation for comprehensive HTTP router performance tracking
Instrument your Gorilla Mux application with OpenTelemetry to send comprehensive telemetry data to Last9. This integration provides automatic instrumentation for HTTP requests, middleware, and database operations, giving you complete visibility into your Go web application’s performance.
Prerequisites
- Go 1.19 or higher
- Gorilla Mux router in your application
- Last9 account with OTLP endpoint configured
Installation
Install the required OpenTelemetry packages for Gorilla Mux instrumentation:
go get -u go.opentelemetry.io/otelgo get -u go.opentelemetry.io/otel/sdkgo get -u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttpgo get -u go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmuxgo get -u go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpcgo get -u go.opentelemetry.io/otel/sdk/metricOptional Database Instrumentation
For database operations, install the appropriate instrumentation:
# For standard database/sql packagego get -u go.nhat.io/otelsql# For pgx PostgreSQL drivergo get -u github.com/exaring/otelpgx# For Redis v9go get github.com/redis/go-redis/extra/redisotel/v9
# For Redis v8go get -u github.com/go-redis/redis/extra/redisotel/v8Configuration
-
Set Environment Variables
Configure the required environment variables for Last9 OTLP integration:
export OTEL_SERVICE_NAME="your-gorilla-mux-service"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,component=api"export OTEL_TRACES_SAMPLER="always_on"export OTEL_LOG_LEVEL="error" -
Create Instrumentation Package
Create a new file
pkg/instrumentation/otel.go:package instrumentationimport ("context""fmt""time""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc""go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp""go.opentelemetry.io/otel/propagation""go.opentelemetry.io/otel/sdk/metric""go.opentelemetry.io/otel/sdk/resource"sdktrace "go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.26.0""go.opentelemetry.io/otel/trace")// Instrumentation holds OpenTelemetry componentstype Instrumentation struct {TracerProvider *sdktrace.TracerProviderMeterProvider *metric.MeterProviderTracer trace.TracerShutdown func(context.Context) error}// NewInstrumentation creates a new instrumentation instancefunc NewInstrumentation(serviceName string) (*Instrumentation, error) {// Create resource with service informationres, err := resource.New(context.Background(),resource.WithFromEnv(),resource.WithTelemetrySDK(),resource.WithProcess(),resource.WithOS(),resource.WithContainer(),resource.WithHost(),resource.WithAttributes(semconv.ServiceNameKey.String(serviceName),),)if err != nil {return nil, fmt.Errorf("failed to create resource: %w", err)}// Initialize trace providertp, err := initTraceProvider(res)if err != nil {return nil, fmt.Errorf("failed to initialize trace provider: %w", err)}// Initialize metric providermp, err := initMetricProvider(res)if err != nil {return nil, fmt.Errorf("failed to initialize metric provider: %w", err)}// Set global providersotel.SetTracerProvider(tp)otel.SetMeterProvider(mp)otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{},propagation.Baggage{},))return &Instrumentation{TracerProvider: tp,MeterProvider: mp,Tracer: tp.Tracer(serviceName),Shutdown: func(ctx context.Context) error {var err errorif shutdownErr := tp.Shutdown(ctx); shutdownErr != nil {err = shutdownErr}if shutdownErr := mp.Shutdown(ctx); shutdownErr != nil {if err != nil {err = fmt.Errorf("%v; %v", err, shutdownErr)} else {err = shutdownErr}}return err},}, nil}func initTraceProvider(res *resource.Resource) (*sdktrace.TracerProvider, error) {exporter, err := otlptracehttp.New(context.Background())if err != nil {return nil, fmt.Errorf("failed to create trace exporter: %w", err)}tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter),sdktrace.WithResource(res),sdktrace.WithSampler(sdktrace.AlwaysSample()),)return tp, nil}func initMetricProvider(res *resource.Resource) (*metric.MeterProvider, error) {exporter, err := otlpmetricgrpc.New(context.Background())if err != nil {return nil, fmt.Errorf("failed to create metric exporter: %w", err)}mp := metric.NewMeterProvider(metric.WithResource(res),metric.WithReader(metric.NewPeriodicReader(exporter,metric.WithInterval(1*time.Minute),)),)return mp, nil} -
Instrument Your Main Application
Update your
main.goto use OpenTelemetry:package mainimport ("context""log""net/http""os""os/signal""syscall""time""github.com/gorilla/mux""go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux""your-app/pkg/instrumentation" // Update with your module path)func main() {serviceName := os.Getenv("OTEL_SERVICE_NAME")if serviceName == "" {serviceName = "gorilla-mux-app"}// Initialize OpenTelemetryotel, err := instrumentation.NewInstrumentation(serviceName)if err != nil {log.Fatalf("Failed to initialize OpenTelemetry: %v", err)}// Ensure proper shutdowndefer func() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := otel.Shutdown(ctx); err != nil {log.Printf("Error shutting down OpenTelemetry: %v", err)}}()// Create router with OpenTelemetry middlewarer := mux.NewRouter()r.Use(otelmux.Middleware(serviceName))// Add routessetupRoutes(r, otel)// Start HTTP serversrv := &http.Server{Addr: ":8080",Handler: r,}// Handle graceful shutdowngo func() {sigChan := make(chan os.Signal, 1)signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)<-sigChanlog.Println("Shutting down server...")ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Printf("Server forced to shutdown: %v", err)}}()log.Printf("Server starting on port 8080")if err := srv.ListenAndServe(); err != http.ErrServerClosed {log.Fatalf("Server failed to start: %v", err)}}func setupRoutes(r *mux.Router, otel *instrumentation.Instrumentation) {// Health check endpointr.HandleFunc("/health", healthHandler).Methods("GET")// API routesapi := r.PathPrefix("/api/v1").Subrouter()api.HandleFunc("/users", getUsersHandler(otel)).Methods("GET")api.HandleFunc("/users/{id}", getUserHandler(otel)).Methods("GET")api.HandleFunc("/users", createUserHandler(otel)).Methods("POST")api.HandleFunc("/users/{id}", updateUserHandler(otel)).Methods("PUT")api.HandleFunc("/users/{id}", deleteUserHandler(otel)).Methods("DELETE")}
Custom Route Handlers with Tracing
Implement route handlers with custom tracing:
package main
import ( "context" "encoding/json" "net/http" "strconv" "time"
"github.com/gorilla/mux" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace")
type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"`}
func healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "healthy", "timestamp": time.Now().UTC().Format(time.RFC3339), })}
func getUsersHandler(otel *instrumentation.Instrumentation) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() span := trace.SpanFromContext(ctx)
span.SetAttributes( attribute.String("handler.name", "get-users"), attribute.String("operation.type", "read"), )
// Simulate business logic with custom tracing users, err := fetchUsers(ctx, otel.Tracer) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return }
span.SetAttributes( attribute.Int("users.count", len(users)), attribute.Bool("operation.success", true), )
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) }}
func getUserHandler(otel *instrumentation.Instrumentation) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() span := trace.SpanFromContext(ctx)
vars := mux.Vars(r) userID, err := strconv.Atoi(vars["id"]) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "invalid user ID") http.Error(w, "Invalid user ID", http.StatusBadRequest) return }
span.SetAttributes( attribute.String("handler.name", "get-user"), attribute.Int("user.id", userID), attribute.String("operation.type", "read"), )
user, err := fetchUserByID(ctx, otel.Tracer, userID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) http.Error(w, err.Error(), http.StatusNotFound) return }
span.SetAttributes(attribute.Bool("user.found", user != nil))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) }}
func createUserHandler(otel *instrumentation.Instrumentation) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() span := trace.SpanFromContext(ctx)
span.SetAttributes( attribute.String("handler.name", "create-user"), attribute.String("operation.type", "create"), )
var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { span.RecordError(err) span.SetStatus(codes.Error, "invalid request body") http.Error(w, "Invalid request body", http.StatusBadRequest) return }
span.SetAttributes( attribute.String("user.name", user.Name), attribute.String("user.email", user.Email), )
newUser, err := createUser(ctx, otel.Tracer, user) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return }
span.SetAttributes( attribute.Int("user.id", newUser.ID), attribute.Bool("operation.success", true), )
w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newUser) }}
// Business logic functions with custom tracingfunc fetchUsers(ctx context.Context, tracer trace.Tracer) ([]User, error) { _, span := tracer.Start(ctx, "fetch-users-from-db") defer span.End()
span.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), )
// Simulate database operation time.Sleep(50 * time.Millisecond)
users := []User{ {ID: 1, Name: "John Doe", Email: "john@example.com"}, {ID: 2, Name: "Jane Smith", Email: "jane@example.com"}, }
span.SetAttributes( attribute.Int("db.rows_returned", len(users)), attribute.Bool("operation.success", true), )
return users, nil}
func fetchUserByID(ctx context.Context, tracer trace.Tracer, userID int) (*User, error) { _, span := tracer.Start(ctx, "fetch-user-by-id") defer span.End()
span.SetAttributes( attribute.String("db.operation", "SELECT"), attribute.String("db.table", "users"), attribute.Int("user.id", userID), )
// Simulate database operation time.Sleep(30 * time.Millisecond)
if userID == 404 { span.SetAttributes(attribute.Bool("user.found", false)) return nil, fmt.Errorf("user not found") }
user := &User{ ID: userID, Name: fmt.Sprintf("User %d", userID), Email: fmt.Sprintf("user%d@example.com", userID), }
span.SetAttributes(attribute.Bool("user.found", true)) return user, nil}
func createUser(ctx context.Context, tracer trace.Tracer, user User) (*User, error) { _, span := tracer.Start(ctx, "create-user-in-db") defer span.End()
span.SetAttributes( attribute.String("db.operation", "INSERT"), attribute.String("db.table", "users"), attribute.String("user.name", user.Name), attribute.String("user.email", user.Email), )
// Simulate database operation time.Sleep(100 * time.Millisecond)
user.ID = int(time.Now().Unix()) // Simulate ID generation
span.SetAttributes( attribute.Int("user.id", user.ID), attribute.Bool("operation.success", true), )
return &user, nil}Database Integration
PostgreSQL with pgx
package database
import ( "context" "fmt" "os"
"github.com/exaring/otelpgx" "github.com/jackc/pgx/v5/pgxpool")
func InitDB() (*pgxpool.Pool, error) { connString := os.Getenv("DATABASE_URL") if connString == "" { return nil, fmt.Errorf("DATABASE_URL environment variable is required") }
cfg, err := pgxpool.ParseConfig(connString) if err != nil { return nil, fmt.Errorf("failed to parse database config: %w", err) }
// Add OpenTelemetry tracer cfg.ConnConfig.Tracer = otelpgx.NewTracer()
pool, err := pgxpool.NewWithConfig(context.Background(), cfg) if err != nil { return nil, fmt.Errorf("failed to create connection pool: %w", err) }
return pool, nil}Standard SQL Database
package database
import ( "database/sql" "fmt"
"go.nhat.io/otelsql" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" _ "github.com/lib/pq" // PostgreSQL driver)
func InitSQLDB() (*sql.DB, error) { driverName, err := otelsql.Register("postgres", otelsql.AllowRoot(), otelsql.TraceQueryWithoutArgs(), otelsql.TraceRowsClose(), otelsql.TraceRowsAffected(), otelsql.WithDatabaseName("your-database"), otelsql.WithSystem(semconv.DBSystemPostgreSQL), ) if err != nil { return nil, fmt.Errorf("failed to register otelsql driver: %w", err) }
db, err := sql.Open(driverName, os.Getenv("DATABASE_URL")) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) }
// Record connection pool stats if err := otelsql.RecordStats(db); err != nil { return nil, fmt.Errorf("failed to record database stats: %w", err) }
return db, nil}Redis Integration
package cache
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 tracing if err := redisotel.InstrumentTracing(rdb); err != nil { panic(fmt.Errorf("failed to instrument Redis tracing: %w", err)) }
// Enable metrics if err := redisotel.InstrumentMetrics(rdb); err != nil { panic(fmt.Errorf("failed to instrument Redis metrics: %w", err)) }
return rdb}Docker Configuration
# DockerfileFROM golang:1.21-alpine AS builder
WORKDIR /appCOPY go.mod go.sum ./RUN go mod download
COPY . .RUN go build -o main .
FROM alpine:latestRUN apk --no-cache add ca-certificatesWORKDIR /root/
COPY --from=builder /app/main .
# Set OpenTelemetry environment variablesENV OTEL_SERVICE_NAME=gorilla-mux-docker-appENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"ENV OTEL_RESOURCE_ATTRIBUTES="deployment.environment=docker"
EXPOSE 8080CMD ["./main"]Troubleshooting
Common Issues
-
No traces appearing:
- Verify environment variables are correctly set
- Check network connectivity to Last9
- Enable debug logging
-
Missing spans:
- Ensure middleware is added before routes
- Verify instrumentation packages are imported
-
Performance impact:
- Use sampling in production
- Monitor memory usage
- Consider batch processing options
Debug Mode
Enable OpenTelemetry debugging:
import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/log/noop")
// Enable debug loggingotel.SetLogger(global.GetLoggerProvider().Logger("debug"))Monitoring Capabilities
This integration captures:
- HTTP Requests: All router operations and middleware
- Route Performance: Individual handler execution times
- Database Operations: SQL queries and connection pool metrics
- Cache Operations: Redis commands and performance
- Custom Business Logic: Through manual instrumentation
- Error Tracking: Detailed exception information
Your Gorilla Mux application will now provide comprehensive telemetry data to Last9, enabling detailed performance monitoring and debugging.