# Add Health Check Endpoints to a Gin App

Yes. An agent adds liveness and readiness endpoints to your Gin service, the readiness one pinging the database with a timeout, with tests, and opens a pull request.

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

## What the agent does

The agent reads how the service starts before it adds a route. In a Gin app like `gothinkster/golang-gin-realworld-example-app`, `go.mod` declares Go 1.21 with `gin-gonic/gin v1.10.0`, `gorm.io/gorm v1.25.12` and the SQLite driver, and `hello.go` is the whole bootstrap: `common.Init()` opens the SQLite file at `DB_PATH`, defaulting to `./data/gorm.db`, `Migrate(db)` runs GORM's `AutoMigrate` for the user and article models, `gin.Default()` builds the router with `RedirectTrailingSlash` turned off, and the `/api` group is registered in two halves, anonymous routes first and then the ones behind `users.AuthMiddleware(true)`. There is already a `/api/ping` route that answers `{"message": "pong"}`, but it says nothing about the database, and it sits under the `/api` prefix an ingress probe may not be allowed to reach.

It adds a `common/health.go` with two handlers and a `HealthRegister` function in the style of `users.UsersRegister` and `articles.ArticlesRegister`. `GET /healthz` is liveness: it returns 200 with `{"status": "ok"}` and touches nothing, so a deadlocked database never makes the orchestrator restart a process that is otherwise serving. `GET /readyz` is readiness: it takes the `*sql.DB` from `db.DB()`, the same call `hello.go` already makes to close the pool on exit, and runs `PingContext` with a two-second timeout; success is 200 with `{"status": "ok", "database": "ok"}`, failure is 503 with `"database": "unavailable"` and the error in the log rather than in the body. Both routes are registered on the root router before any `AuthMiddleware`, so no JWT is needed to probe them, and `/api/ping` stays as it is.

The tests follow the ones in the repository. `common/unit_test.go` already builds a router with `httptest.NewRecorder`, uses `assert.New(t)` from testify, and proves a failing `Ping()` by `chmod`-ing the database file to `0000` and back, so the new `common/health_test.go` reuses that approach: one test for each route's success body and status, and one that makes the database unreachable and asserts 503 from `/readyz`. The test database path comes from `GetTestDBPath()`, as the existing tests do, so nothing touches `./data/gorm.db`.

Before it pushes, the agent runs what `.github/workflows/ci.yml` runs: `bash ./scripts/gofmt.sh`, `go vet ./...`, `bash ./scripts/coverage.sh` for the tests across the Go versions the matrix lists where the sandbox has them, and `golangci-lint run` with the repository's `.golangci.yml`, which enables `staticcheck`, `unused` and `misspell`. It adds the two routes to `readme.md` under the API section and to `AGENTS.md`, which documents the package layout for coding agents, so the next change finds them.

## Start it with one API call

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

```json
{
  "agent": "codex",
  "input": "Add health check endpoints to this Gin service and open a pull request. Read go.mod, hello.go, common/database.go, common/unit_test.go, users/routers.go, users/middlewares.go, .golangci.yml, scripts/gofmt.sh, scripts/coverage.sh, .github/workflows/ci.yml, readme.md and AGENTS.md first. Add common/health.go with a HealthRegister function in the style of the existing Register functions: GET /healthz returns 200 and {\"status\":\"ok\"} without touching anything, and GET /readyz calls PingContext on the *sql.DB from db.DB() with a two-second timeout, returning 200 with {\"status\":\"ok\",\"database\":\"ok\"} or 503 with \"database\":\"unavailable\" and the error logged, not returned. Register both on the root router in hello.go before any AuthMiddleware so no token is needed, and leave /api/ping unchanged. Add common/health_test.go using httptest and testify as common/unit_test.go does, covering both routes and a failing readiness check using the test database path and the same file permission approach the existing tests use. Run scripts/gofmt.sh, go vet ./..., scripts/coverage.sh and golangci-lint run, paste the output in the pull request, and document the two routes in readme.md and AGENTS.md. Do not change existing routes or models.",
  "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 with `common/health.go`, `common/health_test.go`, a three-line change to `hello.go` that registers the routes, and the documentation lines in `readme.md` and `AGENTS.md`. The description shows a `curl` of each route from the sandbox with its status and body, and the output of `scripts/gofmt.sh`, `go vet`, `scripts/coverage.sh` and `golangci-lint run` from the branch.

The CI workflow runs the same scripts on the pull request across Go 1.21, 1.22 and 1.23 and uploads coverage, so the new test file shows up in the Coveralls delta rather than being invisible. The agent pushes one commit for the handlers and tests and one for the documentation.

A reviewer checks that `/readyz` cannot be reached through `AuthMiddleware`, that the two-second timeout fits the probe interval they plan to configure, and that the 503 body gives away nothing about the database path. Then they point their orchestrator's liveness probe at `/healthz` and its readiness probe at `/readyz`.

## Run it on every new service without a health route

A platform team that wants every Go service to expose the same two routes starts one run per repository with `POST /o/{org}/sessions`, this prompt and the repository in `resources`. The agent reads each service's own bootstrap and database package, so a service on Postgres gets a `PingContext` on its pool and one with no database gets a `/readyz` that checks whatever it does depend on, and each run opens its own pull request.

The key's `webhook_url` receives `run.completed` for each run with its `session_url`, so a script can post the pull request links to the team channel as they land.

## Why run it on Rush?

The route itself is twenty lines; the care is in where it is registered, what it checks and how the failure is tested. On a laptop the test that makes the database unreachable is the one that gets skipped. In a Rush sandbox the agent writes it, runs it against a throwaway SQLite file, and the test output is kept in the transcript with the `curl` of each route, so the pull request shows the 503 path working, not only the 200 one.

The result is a pull request that passes the repository's own gates, `gofmt`, `go vet`, the coverage script and `golangci-lint`, before a reviewer opens it, and that documents the routes where the next engineer and the next agent will look. The same request works unchanged across every Go service the team owns.

## Frequently asked questions

### Why two routes instead of one?

Because they answer different questions. `/healthz` says the process is alive and should not be restarted; `/readyz` says it can serve traffic, which for this app means the SQLite file at `DB_PATH` answers a ping. Tying liveness to the database would make a database outage restart every instance for no gain.

### Do the probes need a JWT?

No. The routes are registered on the root router in `hello.go` before `users.AuthMiddleware` is attached to the `/api` group, so an orchestrator or a load balancer can call them with no token. The existing `/api/ping` is left exactly as it was.

### What does the readiness check do when the database hangs rather than fails?

`PingContext` runs with a two-second timeout, so a hung pool returns 503 after two seconds instead of holding the probe open until the orchestrator's own timeout fires. The timeout is a constant in `common/health.go` that a reviewer can adjust to their probe settings.

### How is the failing case tested without a second database?

The same way `common/unit_test.go` already tests `Init()`: it opens the test database at `GetTestDBPath()`, changes the file's permissions to `0000`, asserts that `/readyz` returns 503, and restores the permissions afterwards. No new test infrastructure is added.
