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.
Prerequisites
Section titled “Prerequisites”- Go 1.22 or newer.
- A Go module using
github.com/labstack/echo/v4. - An Osuite ingest endpoint and token for your region.
Instrument the service
Section titled “Instrument the service”-
Install the packages.
Terminal window go get go.opentelemetry.io/otelgo get go.opentelemetry.io/otel/sdkgo get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpcgo get go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho -
Create
otel.gonext to yourmainpackage. It initialises theTracerProvider, the propagator, and the service resource, and returns a shutdown function.package mainimport ("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} -
Register the middleware in
main. CallsetupOTelfirst, defer the shutdown, then addotelecho.Middlewarebefore your routes.package mainimport ("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)}}
Configure and run
Section titled “Configure and run”Set the ingest environment variables for your deployment target, then start the service. Every tab is a complete path.
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 .docker run \ -e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443" \ -e OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>" \ checkout-serviceCreate a secret with the ingest header, then reference it from the pod spec.
kubectl create secret generic osuite-ingest \ --from-literal=otlp-headers="x-osuite-ingest-token=<your-ingest-token>"env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "https://ingest.<region>.osuite.io:443" - name: OTEL_EXPORTER_OTLP_HEADERS valueFrom: secretKeyRef: name: osuite-ingest key: otlp-headersFramework notes
Section titled “Framework notes”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" }),))Verify in Osuite
Section titled “Verify in Osuite”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.
Troubleshooting
Section titled “Troubleshooting”- Some middleware or handlers are missing from spans.
otelecho.Middlewarewas registered after them. Add it first. - Downstream calls start a new trace. You passed
context.Background()instead ofc.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:443for 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/osuiteexporter 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.