Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

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/otel
go get -u go.opentelemetry.io/otel/sdk
go get -u go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get -u go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux
go get -u go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
go get -u go.opentelemetry.io/otel/sdk/metric

Optional Database Instrumentation

For database operations, install the appropriate instrumentation:

# For standard database/sql package
go get -u go.nhat.io/otelsql

Configuration

  1. 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"
  2. Create Instrumentation Package

    Create a new file pkg/instrumentation/otel.go:

    package instrumentation
    import (
    "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 components
    type Instrumentation struct {
    TracerProvider *sdktrace.TracerProvider
    MeterProvider *metric.MeterProvider
    Tracer trace.Tracer
    Shutdown func(context.Context) error
    }
    // NewInstrumentation creates a new instrumentation instance
    func NewInstrumentation(serviceName string) (*Instrumentation, error) {
    // Create resource with service information
    res, 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 provider
    tp, err := initTraceProvider(res)
    if err != nil {
    return nil, fmt.Errorf("failed to initialize trace provider: %w", err)
    }
    // Initialize metric provider
    mp, err := initMetricProvider(res)
    if err != nil {
    return nil, fmt.Errorf("failed to initialize metric provider: %w", err)
    }
    // Set global providers
    otel.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 error
    if 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
    }
  3. Instrument Your Main Application

    Update your main.go to use OpenTelemetry:

    package main
    import (
    "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 OpenTelemetry
    otel, err := instrumentation.NewInstrumentation(serviceName)
    if err != nil {
    log.Fatalf("Failed to initialize OpenTelemetry: %v", err)
    }
    // Ensure proper shutdown
    defer 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 middleware
    r := mux.NewRouter()
    r.Use(otelmux.Middleware(serviceName))
    // Add routes
    setupRoutes(r, otel)
    // Start HTTP server
    srv := &http.Server{
    Addr: ":8080",
    Handler: r,
    }
    // Handle graceful shutdown
    go func() {
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan
    log.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 endpoint
    r.HandleFunc("/health", healthHandler).Methods("GET")
    // API routes
    api := 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 tracing
func 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

# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
# Set OpenTelemetry environment variables
ENV OTEL_SERVICE_NAME=gorilla-mux-docker-app
ENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
ENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
ENV OTEL_RESOURCE_ATTRIBUTES="deployment.environment=docker"
EXPOSE 8080
CMD ["./main"]

Troubleshooting

Common Issues

  1. No traces appearing:

    • Verify environment variables are correctly set
    • Check network connectivity to Last9
    • Enable debug logging
  2. Missing spans:

    • Ensure middleware is added before routes
    • Verify instrumentation packages are imported
  3. 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 logging
otel.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.