Skip to content

Echo OpenTelemetry instrumentation

Instrument an Echo service with OpenTelemetry. The otelecho middleware wraps every route so each inbound request becomes a span named after the matched route.

  • Go 1.22 or newer.
  • A Go module using github.com/labstack/echo/v4.
  • An Osuite ingest endpoint and token for your region.
  1. Install the packages.

    Terminal window
    go get go.opentelemetry.io/otel
    go get go.opentelemetry.io/otel/sdk
    go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
    go get go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho
  2. Create otel.go next to your main package. It initialises the TracerProvider, the propagator, and the service resource, and returns a shutdown function.

    package main
    import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
    )
    func setupOTel(ctx context.Context, serviceName, environment string) (func(context.Context) error, error) {
    res, err := resource.New(ctx,
    resource.WithAttributes(
    semconv.ServiceName(serviceName),
    attribute.String("service.environment", environment),
    ),
    )
    if err != nil {
    return nil, err
    }
    exporter, err := otlptracegrpc.New(ctx)
    if err != nil {
    return nil, err
    }
    tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),
    sdktrace.WithResource(res),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
    propagation.TraceContext{},
    propagation.Baggage{},
    ))
    return tp.Shutdown, nil
    }
  3. Register the middleware in main. Call setupOTel first, defer the shutdown, then add otelecho.Middleware before your routes.

    package main
    import (
    "context"
    "log"
    "net/http"
    "github.com/labstack/echo/v4"
    "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
    )
    func main() {
    ctx := context.Background()
    shutdown, err := setupOTel(ctx, "checkout-service", "production")
    if err != nil {
    log.Fatalf("otel setup: %v", err)
    }
    defer func() {
    if err := shutdown(context.Background()); err != nil {
    log.Printf("otel shutdown: %v", err)
    }
    }()
    e := echo.New()
    e.Use(otelecho.Middleware("checkout-service"))
    e.GET("/hello", func(c echo.Context) error {
    return c.String(http.StatusOK, "hello")
    })
    if err := e.Start(":8080"); err != nil {
    log.Fatal(err)
    }
    }

Set the ingest environment variables for your deployment target, then start the service. Every tab is a complete path.

Terminal window
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443"
export OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>"
go run .

Register the middleware first. Add otelecho.Middleware before any other middleware and before your routes so it spans the whole chain.

Propagate the request context. The active span lives on c.Request().Context(). Pass that context into every downstream call so the trace continues.

e.GET("/orders/:id", func(c echo.Context) error {
ctx := c.Request().Context()
order, err := fetchOrder(ctx, c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, order)
})

Skip noisy routes. Exclude health checks and probes with a skipper. Returning true skips tracing for that request.

e.Use(otelecho.Middleware("checkout-service",
otelecho.WithSkipper(func(c echo.Context) bool {
return c.Path() == "/healthz"
}),
))

What you should see

Send a request (curl localhost:8080/hello). The service appears in APM within a minute, and the request shows as a span in the trace explorer named after its route with method and status code.

  • Some middleware or handlers are missing from spans. otelecho.Middleware was registered after them. Add it first.
  • Downstream calls start a new trace. You passed context.Background() instead of c.Request().Context() into the downstream call.

No data in Osuite after a few minutes? Work through these checks.

  • Endpoint — confirm the exporter targets exactly ingest.<region>.osuite.io:443 for your region, over TLS.
  • Token — confirm the ingest header carries a valid <your-ingest-token> and has not been rotated.
  • Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the otlp/osuite exporter attached.
  • Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
  • Network — confirm the host has outbound access to the ingest endpoint on port 443.
  • Timing — allow up to a minute for the first data to appear before assuming a failure.

Still stuck? Ask the Investigation Agent or contact support.