# Add OpenTelemetry Tracing to a Gin API

Yes. An agent adds OpenTelemetry to your Gin API with the otelgin middleware and the GORM tracing plugin, proves spans appear for real requests, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-opentelemetry/gin/), in a Project that has your repository.

## What the agent does

The agent reads how the service starts, because tracing has to wrap the router and the database handle at the point they are created. In a Gin API shaped like gothinkster/golang-gin-realworld-example-app, `go.mod` declares `go 1.21` and depends on `gin-gonic/gin` v1.10.0, `gorm.io/gorm` v1.25.12 with the SQLite driver v1.5.7, `golang-jwt/jwt/v5` v5.2.1 and `stretchr/testify` v1.10.0. `hello.go` is the whole bootstrap: `common.Init()` opens the database, `Migrate(db)` runs the GORM auto-migrations, `gin.Default()` builds the router with its logger and recovery middleware, the `/api` group registers the users, profiles, articles and tags routes with `users.AuthMiddleware`, a `/api/ping` route answers `pong`, and `r.Run` listens on `PORT` or 8080. `common/database.go` opens SQLite at `DB_PATH`, default `./data/gorm.db`, and keeps the handle in a package variable that `GetDB()` returns to every handler. A search for `otel` across the module finds nothing.

It reads the repository's own rules next. `AGENTS.md` documents the layout: one package per domain with `models.go`, `serializers.go`, `routers.go`, `validators.go` and a `unit_test.go` beside them, and `common/` for shared code. CI in `.github/workflows/ci.yml` runs on Go 1.21, 1.22 and 1.23, checks formatting with `scripts/gofmt.sh`, and runs `scripts/coverage.sh`, which loops over every package directory and runs `go test -coverprofile`. `.golangci.yml` enables `staticcheck`, `unused` and `misspell`. `scripts/run-api-tests.sh` builds the binary, starts it on 8080 and runs the RealWorld Postman collection against it. The agent checks the minimum Go version the current OpenTelemetry modules require against the `go 1.21` directive and the CI matrix before it adds anything, and says in the pull request whether it had to raise the directive and drop a matrix entry or pin older module versions to keep 1.21.

The change follows the layout it found. A new `common/telemetry.go` builds a tracer provider from `go.opentelemetry.io/otel/sdk/trace` with a resource that names the service, an OTLP HTTP exporter from `otlptracehttp` configured by the standard `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME` variables, and returns a shutdown function that flushes pending spans. `hello.go` calls it first, defers the shutdown, and adds `otelgin.Middleware` from `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` to the router right after `gin.Default()`, so every route under `/api` gets a server span named by its route pattern. `common.Init()` registers the GORM tracing plugin from `gorm.io/plugin/opentelemetry/tracing` with `db.Use`, so each query becomes a child span carrying the statement. Both variables are added to `.env.example`, which already documents `PORT`, `GIN_MODE` and `DB_PATH`.

The proof is a test and a run. `common/telemetry_test.go` wires an in-memory exporter from `go.opentelemetry.io/otel/sdk/trace/tracetest` into the provider, sends `GET /api/ping` through a router built the same way `hello.go` builds it, and asserts one server span with the `/api/ping/` route attribute; a second test registers a user and asserts the request span has a GORM child span for the insert. The agent then builds the binary, starts it with the exporter pointed at a stdout exporter it enables for the run, sends the registration and article requests the Postman collection uses, and keeps the printed span tree. It runs `bash scripts/gofmt.sh`, `bash scripts/coverage.sh` and `golangci-lint run`, then opens the pull request with the span output in the description.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add OpenTelemetry tracing to this Gin API. Read hello.go, common/database.go, users/middlewares.go, AGENTS.md, go.mod, .env.example and .github/workflows/ci.yml first. Check which Go version the current otel, otelgin and gorm.io/plugin/opentelemetry modules need against the go directive and the CI matrix, and say what you changed about either. Add common/telemetry.go with a tracer provider, a service-name resource and an OTLP HTTP exporter configured by OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_ENDPOINT, with shutdown called from main. Add otelgin.Middleware to the router and the GORM tracing plugin to common.Init. Document both variables in .env.example. Add tests with an in-memory span exporter that assert a server span for GET /api/ping and a database child span for user registration. Run the binary with a stdout exporter, make a registration and an article request, and keep the span output. Run scripts/gofmt.sh, scripts/coverage.sh and golangci-lint, and open a pull request with the span output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that adds `common/telemetry.go` and `common/telemetry_test.go`, changes `hello.go` to initialize tracing, defer its shutdown and register the `otelgin` middleware, changes `common/database.go` to register the GORM plugin, updates `go.mod` and `go.sum` with the OpenTelemetry modules, and adds two lines to `.env.example`. If the module versions required a newer Go, the `go` directive and the CI matrix change too, and the description says so in its first paragraph. Route handlers, models and serializers are untouched.

The description quotes the span tree from the run: the server span for `POST /api/users/` with its child query spans from GORM, and the same for the article request, with span names and the parent relationship visible. A reviewer checks that the middleware sits before the `/api` group so the auth middleware runs inside the request span, that `common.Init()` still works when no exporter endpoint is set, which the tests exercise by using the in-memory exporter, and that the gofmt, coverage and lint runs in the transcript are clean.

## Run it on every Go service without tracing

A script on your server lists the organization's repositories with `GET /o/{org}/github`, fetches each `go.mod`, and sends one `POST /o/{org}/sessions` with this prompt for every module that lacks a `go.opentelemetry.io/otel` requirement, naming that repository in `resources`. Each run works in its own sandbox and opens its own pull request; a `request_id` per repository makes the script safe to re-run.

Set a `webhook_url` on the API key and Rush POSTs `run.completed` when each turn finishes, with the `session_url` to read the pull request from; a `run.failed` names the service that needs a person.

## Why run it on Rush?

Adding tracing to a Gin service is a few imports and then a morning of checking: did the middleware wrap every route, do the GORM spans attach to the request span, does the service still start with no collector configured, does the `go 1.21` directive still hold. The agent answers each with a command in its own sandbox and keeps the output, and your laptop never builds the module.

The pull request carries the span tree it produced, tests that keep the wiring covered, and the format, coverage and lint runs the repository's own CI performs. Several Go services can be instrumented at once, each in its own sandbox and branch, and the transcript shows every step behind each one.

## Frequently asked questions

### Will it send traces somewhere?

Only where `OTEL_EXPORTER_OTLP_ENDPOINT` points. The pull request sets no endpoint; `.env.example` documents the variable, and the tests use an in-memory exporter. Point the variable at your collector or vendor and add headers through `OTEL_EXPORTER_OTLP_HEADERS`, with no code change.

### Does the GORM plugin trace SQLite queries?

Yes. The plugin hooks GORM's callbacks, not a specific driver, so the SQLite queries this app runs through `gorm.io/driver/sqlite` appear as child spans with the statement text. The second test asserts exactly that for the insert that user registration performs.

### What happens to the go 1.21 directive?

The agent checks it rather than assuming. Current OpenTelemetry Go modules state a minimum Go version in their own `go.mod`; if it is above 1.21, the agent raises the directive and removes the matching entry from the CI matrix, and says so first thing in the pull request. If you need 1.21, say so in the prompt and it pins module versions that still support it.

### Will the JWT middleware still see the right context?

Yes. `otelgin.Middleware` is added right after `gin.Default()`, before the `/api` group registers `users.AuthMiddleware`, so the auth check runs inside the request span and anything it does appears as a child. The middleware reads and writes `c.Request.Context()`, which the handlers already receive.
