upstate-rails
Rails app health for Upstate: boot facts, migrations, Puma, jobs, and the slowest actions. Counts only.
The gem adds one endpoint, GET /upstate/health. Upstate's host observer reads it from inside your server, over your private Docker network, every minute. Nothing else is installed, and nothing is sent anywhere by the gem itself.
What is sent
- App version (
APP_VERSION, elseKAMAL_VERSION, elseunknown), Rails and Ruby versions, whether YJIT is on, when the process booted. - Whether the database answers, and whether migrations are pending.
- Active Record pool size, busy and waiting connections.
- Puma workers, threads, backlog and spare capacity.
- Job numbers for Solid Queue, Sidekiq or GoodJob: queue depth, oldest queued age, worker count, jobs processed and failed in the last 5 minutes, and when something last ran.
- Whether your cache (Solid Cache or Redis) answers.
- For the last 15 minutes, the 10 slowest
Controller#actionnames per minute, with request count, error count, a 12-bucket timing histogram, and total database time.
What is never sent
No params, URLs or paths, SQL, IP addresses, user agents, headers, cookies, job arguments, exception classes or messages, logs, or traces. An error is only a count (a 5xx response or an unhandled exception). Job queue names and job class names are not read either.
Install
# Gemfile
gem "upstate-rails"bundle install
bin/rails g upstate:installThe endpoint is off until UPSTATE_HEALTH_TOKEN is set. Without it, /upstate/health returns 404. With a missing or wrong token it returns 401.
Kamal secrets
-
In Upstate, open the environment page and generate a health token. It is shown once.
-
Add it to
.kamal/secrets:UPSTATE_HEALTH_TOKEN=<the token>
-
List it in
config/deploy.yml:env: secret: - UPSTATE_HEALTH_TOKEN
-
kamal deploy.
The generator appends a commented example to config/deploy.yml only when UPSTATE_HEALTH_TOKEN is not already in the file.
Verify
curl -i -H "Authorization: Bearer $UPSTATE_HEALTH_TOKEN" https://your-app.example.com/upstate/healthYou should get 200 and JSON starting with "schema": 1. A 503 means the database is not answering (the JSON says so). Anything with a slow, missing or erroring section reports that section as null and still returns the rest.
Good to know
- The route is added ahead of your routes, so a catch-all route cannot hide it.
- The path is excluded from
config.host_authorizationand from theforce_sslredirect, because the observer calls the container's private IP over plain HTTP. Nothing else changes in those settings. - Action timings are kept in memory per Puma worker and written to
Dir.tmpdir/upstate-rails/<pid>.jsonabout once a minute (not on every request). The endpoint merges the files of live processes. Files older than 20 minutes are ignored. - Sidekiq keeps lifetime counters only, so
processed_last_5mandfailed_last_5marenulluntil the gem has seen two readings at least 30 seconds apart in one process. - In Puma cluster mode, a worker answering the request sees the other workers through the master's last snapshot, so Puma numbers are best effort.
- Requires Ruby 3.1+ and Rails 7.1+. No other runtime gems.
Migrating from the v0 controller
If you copied upstate_health_controller.rb and its route into your app, remove both: delete app/controllers/upstate_health_controller.rb and the get "/upstate/health" line in config/routes.rb. Keep the same UPSTATE_HEALTH_TOKEN. The gem's response is a superset of the v0 response (including the same solid_queue block), so Upstate keeps working through the change.
Development
bundle install
bundle exec rake test
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle install && BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake testCI runs Rails 7.2 and 8.0 on Ruby 3.2 to 3.4.
License
MIT.