All jobs

Add OpenTelemetry Tracing to a Go Service

Send Rush a Go repository to add OpenTelemetry spans for HTTP, database and outbound calls, and get a pull request with exported traces.

Run this on RushIn a Project that has your repository.

What the agent does

In a Go service like miniflux/v2, the agent starts from go.mod: the module is miniflux.app/v2 on go 1.26.0, and the only database driver is github.com/lib/pq, a plain Postgres driver with no tracing of its own. The server lives in internal/http and internal/api, storage queries sit in internal/storage, and feed fetching lives in internal/reader, which issues the outbound HTTP calls a reader service is all about. The agent records the baseline with make test, which runs go test -cover -race -count=1 ./..., and notes that make lint runs go vet, a gofmt check and golangci-lint, whose .golangci.yml requires an SPDX header on every file, so new files must carry one.

The agent adds the go.opentelemetry.io/otel packages with go get and wires three seams. otelhttp wraps the server handler so every route emits a server span. For the database, internal/storage builds SQL against lib/pq, and the agent either switches the calls to the otel database/sql wrappers or instruments the query builder paths, so each query appears as a span carrying its statement. Outbound feed fetches get an otelhttp transport on the reader's HTTP client, which turns every feed poll into client spans with the target host. Configuration comes from the standard OTEL_* environment variables, matching the service's existing env-var configuration in internal/config: OTEL_EXPORTER_OTLP_ENDPOINT names the collector and OTEL_SERVICE_NAME names the service.

Proving the spans needs a running service, so the agent reuses the project's own integration path: make integration-test starts Postgres, boots the server with DATABASE_URL and RUN_MIGRATIONS, and runs the internal/api tests against TEST_MINIFLUX_BASE_URL. With a local collector added to that stack, the agent creates a user, subscribes to a feed and refreshes it, then exports the resulting spans to a file in OTLP JSON. The feed refresh is the interesting trace: one operation that touches the API handler, a storage query and an outbound fetch to the feed's origin.

The pull request contains the go.mod and go.sum additions, the instrumentation at the three seams, the collector config, and the trace export as evidence. Version stamping is unaffected: the Makefile stamps internal/version.Version from an exact-match git tag, and nothing in the change touches ldflags. A full make test plus make lint run concludes the branch, because the golangci-lint pipeline and its goheader check will reject any new file missing the Apache-2.0 SPDX block.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "codex",
  "input": "Add OpenTelemetry tracing to this service: HTTP server spans, database spans and outbound HTTP spans, configured through the standard OTEL_* environment variables. Run a local collector in the sandbox, exercise the main endpoints, and open a pull request with an export of the traces.",
  "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

The pull request shows the three instrumentation seams in internal/http, internal/storage and internal/reader, the go.mod additions, a collector config, and a trace export file. Its description maps each exercised endpoint to the spans it produced. The description lists the commands the agent ran in its sandbox, make test with the race detector and make lint, with their output, and the golangci-lint pipeline runs again on the branch.

A reviewer opens the export and looks for one end-to-end trace that covers all three span kinds, most likely a feed refresh: API server span, storage query span, outbound client span to the feed origin. The OTEL_* variables should be the only configuration surface; a hardcoded collector address in code is a finding. The session read returns the repository and pull request information, and the transcript holds the integration-test run, the collector logs and the export command.

Run it on every new service deployed

When a Go service repo gains its first production deployment, your automation sends POST /o/{org}/sessions with the repository URL and this tracing prompt, so every service ships with the same baseline spans instead of whichever signals each team added under pressure. Install the Rush GitHub App on the repository first and authenticate with an API key; an Idempotency-Key header keeps a retried submission from starting two runs.

Keep the returned execution_id and read the session when the run settles, or receive the key's completion webhook. If a service needs a different sampler or extra resource attributes, send a follow-up on the finished turn; the messages endpoint wakes the parked sandbox where it left off. A follow-up while the turn is running is refused with a conflict, so check status first.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

Why run it on Rush?

Go tracing work means standing up Postgres, booting the service and polling real feeds to see spans, and that stack does not fit neatly next to your other work on one laptop. On Rush the agent gets its own sandbox and uses the project's own make integration-test path to prove the wiring. The transcript keeps the collector logs and the export command beside the diff, so the claim that all three span kinds exist is something you can inspect.

For a fleet of Go services, submit one repository per run within your key's concurrency allowance and each comes back instrumented the same way. You review the pull requests independently and follow up where a service needs its sampler tuned. The useful proof is one trace that crosses the API, the database and an outbound call, with make test still green under the race detector.

Frequently asked questions

lib/pq has no built-in tracing. How do database spans work?

The agent wraps the driver through the OpenTelemetry database/sql instrumentation or instruments the storage query paths directly, so each query becomes a span with its statement attached. The trace export shows the result on a real query from internal/storage, which is the evidence to review.

Which outbound calls get spans?

Every HTTP call made through the instrumented client transport in internal/reader, which is where feed polling happens. If some fetcher path constructs its own client, the pull request should say so; a client that bypasses the wrapped transport emits no spans.

Does this change how the service is configured?

No new config files. OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_ENDPOINT are read by the SDK from the environment, matching the service's existing env-var style in internal/config. Your deployment sets the endpoint per environment.

Will the golangci-lint pipeline accept the new files?

Only if they carry the SPDX Apache-2.0 header the .golangci.yml goheader check enforces on every file. The run performs make lint itself, so a missing header fails the run before you review anything.

Do the integration tests still pass?

They must. make test and the integration suite that runs internal/api against a live server are part of the run, and the agent exercises the service through that same path. A failure ends the run with the failure named rather than a partial pull request.