OpenTelemetry Gorilla Mux monitoring [otelmux]

Vladimir Mihailenco
February 04, 2026
6 min read

OpenTelemetry Gorilla Mux instrumentation allows developers to monitor and diagnose issues with their Gorilla Mux applications, providing valuable insights into HTTP request handling and routing performance.

What is Gorilla Mux?

Gorilla Mux is one of the oldest and most popular HTTP routers for Go. It implements a request router and dispatcher that matches incoming requests to their respective handlers based on URL patterns.

Key features of Gorilla Mux include:

  • Powerful URL pattern matching with variables and regex support
  • Route grouping with subrouters
  • Middleware support through handlers chain
  • URL building and reverse routing
  • Request matching based on headers, query parameters, and more

What is OpenTelemetry?

OpenTelemetry is an open-source observability framework that aims to standardize and simplify the collection, processing, and export of telemetry data from applications and systems.

OpenTelemetry supports multiple programming languages and platforms, making it suitable for a wide range of applications and environments.

OpenTelemetry enables developers to instrument their code and collect telemetry data, which can then be exported to various OpenTelemetry backends or observability platforms for analysis and visualization. Understanding the OpenTelemetry architecture helps developers leverage its full potential with proper instrumentation and data collection strategies.

Gorilla Mux instrumentation

OpenTelemetry Gorilla Mux instrumentation (otelmux) provides automatic tracing for your Gorilla Mux applications. It captures HTTP request details, route parameters, timing information, and error states.

To install otelmux instrumentation:

shell
go get go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux

Usage

You can instrument Gorilla Mux router by installing OpenTelemetry middleware:

go
import (
    "net/http"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
)

func main() {
    router := mux.NewRouter()
    router.Use(otelmux.Middleware("my-service"))

    router.HandleFunc("/users/{id}", getUser).Methods("GET")
    router.HandleFunc("/articles/{category}/{id:[0-9]+}", getArticle).Methods("GET")

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

Once instrumented, otelmux automatically creates spans for each incoming HTTP request, capturing the HTTP method, route pattern, status code, and timing information.

Middleware options

The otelmux middleware supports several configuration options:

OptionDescription
WithTracerProviderUse a custom TracerProvider instead of the global one
WithMeterProviderUse a custom MeterProvider instead of the global one
WithPropagatorsSpecify propagators for extracting trace context
WithSpanOptionsConfigure additional span start options
WithSpanNameFormatterCustomize span names for requests
WithPublicEndpointMark the endpoint as public (starts new trace)
WithPublicEndpointFnConditionally mark endpoints as public

Custom tracer provider

For more control over telemetry collection, you can specify a custom tracer provider:

go
import (
    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
    "go.opentelemetry.io/otel"
)

router := mux.NewRouter()
router.Use(otelmux.Middleware("my-service",
    otelmux.WithTracerProvider(otel.GetTracerProvider()),
    otelmux.WithMeterProvider(otel.GetMeterProvider()),
))

Custom span names

Customize how span names are generated:

go
import (
    "net/http"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
)

router := mux.NewRouter()
router.Use(otelmux.Middleware("my-service",
    otelmux.WithSpanNameFormatter(func(routeName string, r *http.Request) string {
        return r.Method + " " + routeName
    }),
))

Public endpoints

Mark endpoints as public to start new traces instead of continuing from incoming trace context:

go
import (
    "net/http"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
)

// Mark all endpoints as public
router.Use(otelmux.Middleware("my-service",
    otelmux.WithPublicEndpoint(),
))

// Or conditionally mark endpoints as public
router.Use(otelmux.Middleware("my-service",
    otelmux.WithPublicEndpointFn(func(r *http.Request) bool {
        return r.URL.Path == "/webhook"
    }),
))

Adding custom attributes

You can add custom attributes to spans within your handlers by accessing the span from the request context:

go
import (
    "net/http"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/trace"
)

func getUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID := vars["id"]

    // Get the current span from context
    span := trace.SpanFromContext(r.Context())

    // Add custom attributes
    span.SetAttributes(
        attribute.String("user.id", userID),
        attribute.String("user.request_type", "profile"),
    )

    // Your handler logic here
    w.WriteHeader(http.StatusOK)
}

Recording errors

Record errors in your handlers to mark spans as failed:

go
import (
    "encoding/json"
    "net/http"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

func getUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID := vars["id"]

    span := trace.SpanFromContext(r.Context())

    user, err := fetchUser(r.Context(), userID)
    if err != nil {
        // Record the error on the span
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())

        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    // Continue with successful response
    json.NewEncoder(w).Encode(user)
}

Filtering routes

Exclude health checks and other noisy endpoints from tracing by wrapping the middleware:

go
import (
    "net/http"
    "strings"

    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
)

// Middleware wrapper that skips tracing for certain paths
func tracingMiddleware(excludePaths []string) mux.MiddlewareFunc {
    otelMiddleware := otelmux.Middleware("my-service")

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Skip tracing for excluded paths
            for _, path := range excludePaths {
                if strings.HasPrefix(r.URL.Path, path) {
                    next.ServeHTTP(w, r)
                    return
                }
            }
            // Apply OTel middleware for other routes
            otelMiddleware(next).ServeHTTP(w, r)
        })
    }
}

func main() {
    router := mux.NewRouter()

    // Skip tracing for health and metrics endpoints
    router.Use(tracingMiddleware([]string{"/health", "/ready", "/metrics"}))

    router.HandleFunc("/health", healthCheck).Methods("GET")
    router.HandleFunc("/api/users", listUsers).Methods("GET")
}

Subrouters and route grouping

Instrument subrouters for organized route grouping:

go
import (
    "github.com/gorilla/mux"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
)

func main() {
    router := mux.NewRouter()
    router.Use(otelmux.Middleware("my-service"))

    // API v1 subrouter
    apiV1 := router.PathPrefix("/api/v1").Subrouter()
    apiV1.HandleFunc("/users", listUsers).Methods("GET")
    apiV1.HandleFunc("/users/{id}", getUser).Methods("GET")

    // Admin subrouter
    admin := router.PathPrefix("/admin").Subrouter()
    admin.HandleFunc("/dashboard", adminDashboard).Methods("GET")
}

Outgoing HTTP requests

To trace outgoing HTTP requests from your handlers, use the otelhttp transport:

go
import (
    "net/http"

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

// Create an instrumented HTTP client
var httpClient = &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

func getUser(w http.ResponseWriter, r *http.Request) {
    // Outgoing request will be traced and linked to the parent span
    req, _ := http.NewRequestWithContext(r.Context(), "GET", "https://api.example.com/user/123", nil)
    resp, err := httpClient.Do(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer resp.Body.Close()

    // Process response...
}

The otelhttp transport automatically:

  • Creates child spans for each outgoing request
  • Injects trace context headers (W3C Trace Context)
  • Records HTTP method, URL, status code, and duration
  • Propagates errors to the span

HTTP metrics

The otelmux middleware automatically collects HTTP server metrics following OpenTelemetry semantic conventions:

MetricDescription
http.server.request.durationDuration of HTTP server requests
http.server.request.body.sizeSize of HTTP server request bodies
http.server.response.body.sizeSize of HTTP server response bodies

What is Uptrace?

Uptrace is an OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace Overview

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.

FAQ

What is otelmux? The otelmux package is the official OpenTelemetry instrumentation for Gorilla Mux. It provides middleware that automatically creates spans for incoming HTTP requests, capturing method, route, status code, and timing information.

How do I filter health check endpoints? Use the WithPublicEndpointFn option to conditionally handle specific endpoints, or implement custom logic in your handlers. Unlike some frameworks, otelmux doesn't have a built-in filter option, but you can wrap the middleware with your own filtering logic.

What metrics does otelmux collect? otelmux automatically collects http.server.request.duration, http.server.request.body.size, and http.server.response.body.size metrics following OpenTelemetry semantic conventions.

Does otelmux capture route variables? Yes, the route template (e.g., /users/{id}) is captured in the span name and attributes. The actual variable values can be added as custom attributes using mux.Vars(r) in your handlers.

How do I propagate trace context to downstream services? otelmux automatically extracts incoming trace context from headers. For outgoing requests, use otelhttp.NewTransport to inject trace context into your HTTP client calls.

Can I use otelmux with Gorilla Mux subrouters? Yes, add the middleware to the root router and it will trace requests across all subrouters. The span will include the full matched route path.

What's the difference between WithPublicEndpoint and WithPublicEndpointFn? WithPublicEndpoint marks all endpoints as public (starting new traces), while WithPublicEndpointFn lets you conditionally mark endpoints based on the request, useful for webhooks or external API callbacks.

What's next?

With OpenTelemetry Gorilla Mux instrumentation in place, you can monitor request latency, track error rates, and trace requests across your distributed systems.

Next steps to enhance your observability: