Skip to content

Gin OpenTelemetry instrumentation

Instrument a Gin service with OpenTelemetry. The otelgin middleware wraps every route so each inbound request becomes a span named after the matched route template.

  • Go 1.22 or newer.
  • A Go module using github.com/gin-gonic/gin.
  • 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/gin-gonic/gin/otelgin
  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 otelgin.Middleware before your routes.

    package main
    import (
    "context"
    "log"
    "net/http"
    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
    )
    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)
    }
    }()
    r := gin.New()
    r.Use(otelgin.Middleware("checkout-service"))
    r.GET("/hello", func(c *gin.Context) {
    c.String(http.StatusOK, "hello")
    })
    if err := r.Run(":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 otelgin.Middleware before any other middleware and before your route handlers so it spans the whole chain. gin.Default() works the same as gin.New() here; it only adds Gin’s logger and recovery middleware.

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

r.GET("/orders/:id", func(c *gin.Context) {
ctx := c.Request.Context()
order, err := fetchOrder(ctx, c.Param("id"))
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, order)
})

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

r.Use(otelgin.Middleware("checkout-service",
otelgin.WithGinFilter(func(c *gin.Context) bool {
return c.Request.URL.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 template with method and status code.

  • Some middleware or handlers are missing from spans. otelgin.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.