OpenTelemetry Echo by Labstack [echo-otel]

Vladimir Mihailenco
February 20, 2026
8 min read

OpenTelemetry Echo instrumentation allows developers to monitor and diagnose issues with their Echo applications, providing valuable insights into application behavior in production.

Quick Setup

StepActionCode/Command
1. InstallInstall echo-otel packagego get github.com/labstack/echo-otel/v4
2. ImportImport echootelimport echootel "github.com/labstack/echo-otel/v4"
3. InstrumentAdd middleware to Echoe.Use(echootel.NewMiddleware("api.example.com"))
4. VerifyCheck your backend for tracesTraces and metrics collected automatically

Minimal working example:

go
import (
    echootel "github.com/labstack/echo-otel/v4"
    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    e.Use(echootel.NewMiddleware("api.example.com"))

    e.GET("/", func(c echo.Context) error {
        return c.JSON(200, map[string]string{"message": "Hello World"})
    })
    e.Start(":8080")
}

This single middleware call automatically captures all incoming HTTP requests, traces request flow, records HTTP metrics, and exports telemetry data to your configured backend.

What is Echo?

Echo is a high performance, extensible, minimalist web framework for Go. It features a highly optimized HTTP router with zero dynamic memory allocations, making it one of the fastest Go web frameworks available.

Echo's key features include:

  • Optimized router with zero dynamic memory allocation
  • Middleware support with built-in middleware
  • Data binding for JSON, XML, and form payloads
  • HTTP/2 support
  • Automatic TLS via Let's Encrypt

Echo's simplicity and performance make it a popular choice for building web applications and RESTful APIs in Go.

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. The OpenTelemetry architecture provides a modular, vendor-neutral approach to observability.

Configuration can be managed through OpenTelemetry environment variables, providing a standardized way to configure exporters, resource attributes, and sampling behavior across environments.

Echo instrumentation

The OpenTelemetry middleware for Echo is maintained by the Echo team in the echo-otel repository (Go package echootel). It provides automatic tracing and metrics collection for your Echo applications. It captures HTTP request details, timing information, and error states without requiring manual instrumentation.

The module's major version follows the Echo major version:

Echo versionModuleMinimal Echo version
Echo v4github.com/labstack/echo-otel/v4v4.15.4
Echo v5github.com/labstack/echo-otel/v5v5.2.1

To install echo-otel instrumentation for Echo v4:

shell
go get github.com/labstack/echo-otel/v4

Always include the major version suffix (/v4 or /v5); go get github.com/labstack/echo-otel without it fails.

Usage

You can instrument Echo router by installing OpenTelemetry middleware:

go
import (
    echootel "github.com/labstack/echo-otel/v4"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()
    e.Use(middleware.Recover())
    e.Use(echootel.NewMiddleware("api.example.com"))

    e.GET("/users/:id", getUser)
    e.Start(":8080")
}

Once instrumented, echo-otel automatically creates spans for each incoming HTTP request, capturing method, route, status code, and timing information.

The argument of NewMiddleware is the server name, not the service name. It is recorded as the server.address and server.port span attributes, so it must be a host name with an optional port, for example api.example.com or api.example.com:8080. NewMiddleware panics on a value without a host such as :8080; pass an empty string to use the request Host header instead. Set the service name with the OTEL_SERVICE_NAME environment variable or a resource when you configure the OpenTelemetry SDK.

The middleware records a panic and then re-panics, so add middleware.Recover() before it, as shown above.

Echo v5

For Echo v5, use the /v5 module. The API is the same, but handlers and callbacks take *echo.Context:

go
import (
    echootel "github.com/labstack/echo-otel/v5"
    "github.com/labstack/echo/v5"
)

func main() {
    e := echo.New()
    e.Use(echootel.NewMiddleware("api.example.com"))

    e.GET("/", func(c *echo.Context) error {
        return c.JSON(200, map[string]string{"message": "Hello World"})
    })
    e.Start(":8080")
}

Migrating from otelecho

go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho is deprecated in favor of echo-otel, and so is the interim github.com/labstack/echo-opentelemetry module. You don't need to upgrade to Echo v5 to migrate: use echo-otel/v4 with Echo v4.

  • Replace the import with echootel "github.com/labstack/echo-otel/v4".
  • Replace otelecho.Middleware("my-server") with echootel.NewMiddleware("my-server"), and otelecho.Middleware("my-server", opts...) with echootel.NewMiddlewareWithConfig(echootel.Config{ServerName: "my-server", ...}). Make sure the name is a valid host name (see above); otelecho accepted any string.
  • Options become Config fields: WithTracerProvider → TracerProvider, WithMeterProvider → MeterProvider, WithPropagators → Propagators, WithSkipper → Skipper, WithMetricAttributeFn / WithEchoMetricAttributeFn → MetricAttributes, WithOnError → OnNextError.
  • otelecho called the Echo error handler (c.Error(err)) from the middleware; echo-otel does not. If you use a custom HTTPErrorHandler that maps errors to other status codes, set OnNextError: func(c echo.Context, err error) { c.Error(err) } so the span reports the status code that was actually sent.
  • The instrumentation scope name changes to github.com/labstack/echo-otel/v4, and the echo.error span attribute is replaced by error.type and the span status. Update dashboards and alerts that rely on them.

Middleware options

Use NewMiddlewareWithConfig with an echootel.Config to customize the middleware:

Config fieldDescription
ServerNameServer host name with an optional port, recorded as server.address/server.port
SkipperSkip tracing (return true to skip, false to trace)
TracerProviderUse a custom TracerProvider instead of the global one
MeterProviderUse a custom MeterProvider instead of the global one
PropagatorsSpecify propagators for extracting trace context
SpanStartOptionsAdditional trace.SpanStartOptions applied to each new span
SpanStartAttributesAdd span attributes when the span starts
SpanEndAttributesAdd span attributes when the request completes
MetricAttributesCompose the attributes recorded on HTTP metrics
MetricsReplace the default metrics with your own MetricsRecorder
OnNextErrorCalled with errors returned by the next middleware or handler
OnExtractionErrorCalled when extracting request attributes fails

Skipping health checks

Use the Skipper field to exclude certain endpoints from tracing:

go
import (
    echootel "github.com/labstack/echo-otel/v4"
    "github.com/labstack/echo/v4"
)

e := echo.New()
e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
    ServerName: "api.example.com",
    Skipper: func(c echo.Context) bool {
        // Return true to skip, false to trace
        return c.Path() == "/health" || c.Path() == "/ready"
    },
}))

Custom tracer and meter providers

For more control over telemetry collection, you can specify custom providers:

go
import (
    echootel "github.com/labstack/echo-otel/v4"
    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/otel"
)

e := echo.New()
e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
    ServerName:     "api.example.com",
    TracerProvider: otel.GetTracerProvider(),
    MeterProvider:  otel.GetMeterProvider(),
}))

Adding custom attributes

Use SpanStartAttributes and SpanEndAttributes to add attributes to spans, and MetricAttributes to add attributes to metrics:

go
import (
    echootel "github.com/labstack/echo-otel/v4"
    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/otel/attribute"
)

e := echo.New()
e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
    ServerName: "api.example.com",
    SpanEndAttributes: func(c echo.Context, v *echootel.Values, attr []attribute.KeyValue) []attribute.KeyValue {
        // Append to attr and return it, otherwise the default attributes are dropped.
        return append(attr, attribute.String("tenant.id", c.Request().Header.Get("X-Tenant-ID")))
    },
    MetricAttributes: func(c echo.Context, v *echootel.Values) []attribute.KeyValue {
        // MetricAttributes replaces the default metric attributes, so start from v.MetricAttributes().
        return append(v.MetricAttributes(), attribute.String("deployment.environment.name", "production"))
    },
}))

Span callbacks must append to the attr argument and return it. Keep metric attributes low cardinality: values chosen by clients, such as IP addresses or user agents, belong on spans, not metrics.

HTTP metrics

The echo-otel middleware automatically collects the following HTTP server metrics:

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

These metrics follow the OpenTelemetry semantic conventions for HTTP and include the http.request.method, url.scheme, http.route, network.protocol.name, network.protocol.version, http.response.status_code, and error.type attributes.

Creating Custom Spans

Add custom spans to trace specific operations within your handlers:

go
import (
    "net/http"

    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

var tracer = otel.Tracer("my-service")

func getUser(c echo.Context) error {
    ctx := c.Request().Context()

    // Create a custom span for the database query
    ctx, span := tracer.Start(ctx, "fetch_user")
    defer span.End()

    userID := c.Param("id")
    span.SetAttributes(attribute.String("user.id", userID))

    user, err := fetchUserFromDB(ctx, userID)
    if err != nil {
        span.SetAttributes(attribute.Bool("user.found", false))
        return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"})
    }

    span.SetAttributes(attribute.Bool("user.found", true))
    return c.JSON(http.StatusOK, user)
}

Recording Errors

Record errors on spans to mark them as failed and capture error details:

go
import (
    "net/http"

    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

func processOrder(c echo.Context) error {
    span := trace.SpanFromContext(c.Request().Context())

    var order Order
    if err := c.Bind(&order); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "invalid request body")
        return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    if err := validateOrder(order); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
    }

    return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}

When a handler returns an error, echo-otel sets the span status and the error.type attribute for you. 4xx responses are not treated as errors on server spans, and 5xx responses are.

FAQ

What versions of Echo are supported? Echo v4 with github.com/labstack/echo-otel/v4 (Echo v4.15.4 or later) and Echo v5 with github.com/labstack/echo-otel/v5 (Echo v5.2.1 or later). If you're still on Echo v3, upgrade to v4 first—the migration is straightforward and well-documented.

Is otelecho still supported? No. go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho is deprecated in favor of echo-otel. See Migrating from otelecho for the changes.

How do I pass context to downstream services? The echo-otel middleware automatically injects trace context into the request context. Use c.Request().Context() to retrieve it, then pass it to your database clients, HTTP clients, or gRPC calls to propagate tracing across services.

Why are my health check endpoints cluttering traces? Use the Skipper config field to exclude /health and /ready endpoints. This reduces noise and lowers telemetry costs without losing visibility into real traffic.

Can I use echo-otel with Echo's built-in middleware? Yes. Add middleware.Recover() first, then echootel.NewMiddleware, then the rest of your middleware (logging, auth, etc.) so that all downstream middleware and handlers are captured in the trace.

How do I add metrics alongside traces? The middleware automatically collects HTTP server metrics (http.server.request.duration, http.server.request.body.size, http.server.response.body.size). Configure a MeterProvider to export them.

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.

What's next?

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

Next steps to enhance your observability: