tj-scale
TJ Scale (tj-scale) is a Ruby gem that automatically monitors your background job queues — Delayed Job or Sidekiq — and your web traffic, and sends metrics to a remote monitoring service for auto-scaling purposes. It integrates seamlessly with Rails applications and Heroku deployments.
Features
- 🔍 Automatic Queue Monitoring: Continuously monitors Delayed Job or Sidekiq queues for pending jobs
- 🔀 Pluggable Job Backend: Flip between Delayed Job and Sidekiq with one env var (
TJ_SCALE_JOB_BACKEND); auto-detected by default - 📊 Metrics Reporting: Sends job counts, queue latency, and web queue/response times to a remote monitoring service
- 🚀 Heroku Integration: Automatically runs on the first web or worker dyno
- ⚙️ Configurable: Customizable priority/queue filters and API endpoints
- 🔔 Slack Notifications (control plane): message your team whenever web or worker dynos scale up/down, or when scaling fails
- 🔒 Error Handling: Robust error handling with comprehensive logging
- 🧪 Well Tested: Comprehensive test suite with RSpec
Control plane (Rails + PostgreSQL)
The control plane lives in its own project, tj-scale-dashboard (a sibling directory / repository to this gem): dashboard, per-app scaling rules (web queue time + worker job counts), metrics ingest API compatible with the gem, Heroku formation updates, and Slack notifications for every scale event (Settings → Slack notifications). It uses PostgreSQL — see its README.md and .env.example in ../tj-scale-dashboard/.
Installation
As a Gem
Add this line to your application's Gemfile:
gem "tj-scale", "~> 1.0"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install tj-scale
From Source
- Clone the repository:
git clone https://github.com/Untechnickle/tj-scale-gem.git
cd tj-scale-gem
- Install dependencies:
bundle install
- Run the setup script:
bin/setup
End-to-end guide
Build the gem, publish or reference it, add it to your Rails app, and set configuration (env / Heroku). Follow these steps in order the first time you ship and adopt the gem.
1. Prerequisites
- Ruby 3.0+ and Bundler 2+
- A Rails 6.1+ app with one job backend:
delayed_job_active_record(and a DB-backeddelayed_jobstable), orsidekiq(and Redis)
- Heroku CLI (if you deploy to Heroku)
- A RubyGems.org account (if you publish publicly)
2. Prepare and build the gem (in this repository)
- Track files for the gem package — the gemspec uses
git ls-files, so the repo must be a git repository and files must be committed:
cd /path/to/tj-scale
git init # if needed
git add .
git commit -m "Release tj-scale"
- Install dependencies and verify quality:
bundle install
bundle exec rspec
bundle exec rake standard
Set the version in
lib/tj_scale_ruby/version.rb(for example1.0.0).Build the package:
gem build tj-scale.gemspec
This produces tj-scale-<version>.gem in the current directory.
3. Publish the gem (choose one)
RubyGems.org (public)
- Create an API key at rubygems.org/api_keys.
- Push the file you built (version must match
VERSION):
gem push tj-scale-1.0.1.gem
Private gem host
gem push tj-scale-1.0.1.gem --host https://your-gem-server.com
Configure Bundler in the app if your host needs authentication (see your provider’s docs).
Without publishing — use the repo directly
In the app’s Gemfile:
gem "tj-scale", git: "https://github.com/Untechnickle/tj-scale-gem.git", branch: "main"
# or path for local development:
# gem "tj-scale", path: "../tj-scale"
Then bundle install.
4. Add the gem to your Rails application
- Open the app’s
Gemfileand add (adjust version or source as above):
gem "tj-scale", "~> 1.0"
- Run:
bundle install
No extra
requireis needed inapplication.rb: the gem registers a Rails Railtie and loads with Rails. Ensure your app already bundles its job backend —delayed_job_active_recordorsidekiq(the gem auto-detects which one is loaded, or setTJ_SCALE_JOB_BACKENDexplicitly).Deploy or run the server as usual. The monitoring thread starts on
web.1andworker.1automatically (process type auto-detected from Heroku'sDYNO), so one app reports both web and worker metrics with no extra config. SetTJ_SCALE_MONITOR_PROCESSto pin a single process type (see below).
5. Configure environment variables
Configuration is entirely via environment variables (and Heroku config vars in production). Copy .env.example to .env for local use if you use dotenv-rails or similar.
| Variable | Required | Purpose |
|---|---|---|
TJ_SCALE_API_TOKEN |
Yes | Sent as LOGPLEX_DRAIN_TOKEN and Authorization: Bearer … on each ingest POST. |
TJ_SCALE_API_URL |
Yes | Full URL of the Agent API ingest path: POST /api/v1/metrics or POST /api/v1/sys-logs (same handler). Example local: http://127.0.0.1:5001/api/v1/metrics. |
TJ_SCALE_TARGET_APP |
No | Heroku app name to scale; defaults to HEROKU_APP_NAME if unset. |
TJ_SCALE_TARGET_PROCESS |
No | web or worker to scale; defaults to TJ_SCALE_MONITOR_PROCESS. |
TJ_SCALE_MONITOR_PROCESS |
No | web or worker to pin the reporter to one process type. Default: auto-detected from DYNO (web.1 reports web, worker.1 reports worker); worker when DYNO is unset (non-Heroku). |
TJ_SCALE_INTERVAL_SECONDS |
No | Seconds between posts (default 20). |
TJ_SCALE_JOB_BACKEND |
No | Queue backend for the worker reporter: delayed_job, sidekiq, or auto (default). auto uses Sidekiq when the sidekiq gem is loaded, otherwise Delayed Job. |
TJ_SCALE_SIDEKIQ_QUEUES |
No | Sidekiq backend only: comma-separated queue names to count (empty = all queues). |
TJ_MIN_PRIORITY / TJ_MAX_PRIORITY |
No | Limit which job priorities are counted (0 = no filter; worker reporter, Delayed Job backend only). |
TJ_SCALE_QUEUE_TIME_MS |
No | When TJ_SCALE_MONITOR_PROCESS=web, supplies queue_time_ms if no middleware snapshot exists. |
TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE |
No | Set to 1, true, yes, or on to enable Rack middleware that records queue_time_ms from X-Request-Start (Heroku). |
Payload by reporter: web — queue_time_ms (when known), job_count (requests since last tick), optional response_time_ms. worker — job_count, queue_time_s (oldest waiting job age / queue latency, or 0), and job_backend (delayed_job or sidekiq). Both send heroku_app and target_process. Scaling limits (min/max dynos) are configured in the control plane's dashboard settings, not by this gem. Autoscaling runs on the control plane after ingest; this gem does not call Heroku.
Heroku sets DYNO (for example web.1, worker.1) and typically HEROKU_APP_NAME; do not set DYNO manually in production.
Example: my-app-web (web) and my-app-workers (workers)
Use the same token on both apps if one backend handles scaling.
# my-app-web — reporter on web.1, scale web dynos
heroku config:set TJ_SCALE_API_TOKEN=your_token \
TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics \
TJ_SCALE_TARGET_APP=my-app-web \
TJ_SCALE_TARGET_PROCESS=web \
TJ_SCALE_MONITOR_PROCESS=web \
-a my-app-web
# my-app-workers — reporter on worker.1, scale worker dynos
heroku config:set TJ_SCALE_API_TOKEN=your_token \
TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics \
TJ_SCALE_TARGET_APP=my-app-workers \
TJ_SCALE_TARGET_PROCESS=worker \
TJ_SCALE_MONITOR_PROCESS=worker \
-a my-app-workers
On the web app, enable TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE=1 (and restart) so queue_time_ms is populated from Heroku’s router; the web dyno does not need a shared Delayed Job DB for that path.
6. Wire your monitoring service (Agent API)
The TJ Scale Agent API accepts the same JSON on either route:
POST /api/v1/metricsPOST /api/v1/sys-logs
Set TJ_SCALE_API_URL to the full URL of one of them (for example http://127.0.0.1:5001/api/v1/metrics in development). Authenticate with the ingest token using LOGPLEX_DRAIN_TOKEN or Authorization: Bearer (same secret as TJ_SCALE_API_TOKEN); the gem sends both headers.
The gem POSTs JSON with timestamp, heroku_app, target_process, plus web metrics (queue_time_ms, job_count, optional response_time_ms) or worker metrics (job_count, queue_time_s). Your control plane ingests samples and applies scaling rules (thresholds, cooldowns) when calling the Heroku Platform API.
7. Verify
- At least one web dyno on
my-app-weband one worker dyno onmy-app-workerssoweb.1andworker.1exist. - Check Rails logs for
TjScaleRubylines (debug/warn/error). - Confirm your API receives periodic requests with the expected payload.
Optional: manual calls in Ruby
TjScaleRuby::JobMonitor.pending_job_count
TjScaleRuby::JobMonitor.oldest_pending_queue_seconds
TjScaleRuby.record_web_queue_time_ms!(42) # optional; middleware does this on each request
TjScaleRuby::JobMonitor.send_log
Configuration
Environment Variables
Create a .env file in your project root (or set these in your Heroku config vars):
# Required: API token and ingest URL for your control plane
TJ_SCALE_API_TOKEN=your_api_token_here
TJ_SCALE_API_URL=http://127.0.0.1:5001/api/v1/metrics
# --- Scaling metadata (sent to your monitoring API; defaults shown) ---
# Heroku app name to scale (defaults to HEROKU_APP_NAME when unset).
TJ_SCALE_TARGET_APP=my-heroku-app
# Process type to scale on that app: "web" or "worker" (defaults to TJ_SCALE_MONITOR_PROCESS).
TJ_SCALE_TARGET_PROCESS=web
# Which process type runs the reporter loop: only the first dyno of this type (web.1 or worker.1).
# Use "web" on the web app, "worker" on the worker app.
TJ_SCALE_MONITOR_PROCESS=worker
# Seconds between metric posts (default 20).
TJ_SCALE_INTERVAL_SECONDS=20
# Optional: Minimum priority filter for jobs (0 = no filter)
# Only count jobs with priority >= this value
TJ_MIN_PRIORITY=0
# Optional: Maximum priority filter for jobs (0 = no filter)
# Only count jobs with priority <= this value
TJ_MAX_PRIORITY=0
Heroku sets HEROKU_APP_NAME for the current app. If you omit TJ_SCALE_TARGET_APP, the payload uses that value so each app reports scaling targets for itself.
Heroku Configuration
Set the environment variables on Heroku:
heroku config:set TJ_SCALE_API_TOKEN=your_api_token_here
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics
The DYNO environment variable is automatically set by Heroku and is used to determine which dynos run the monitoring loop: web.1 and worker.1 each report their own process type (override with TJ_SCALE_MONITOR_PROCESS).
Example: web on my-app-web, workers on my-app-workers
Use two Heroku apps (or one app with both process types—same idea). Configure each app’s config vars so the monitoring service receives heroku_app, target_process, and job_count on each POST. Scaling bounds (min/max dynos) are set in the control plane's dashboard.
App my-app-web (web dynos) — run the reporter on the first web dyno and scale web dynos:
heroku config:set TJ_SCALE_API_TOKEN=your_token -a my-app-web
heroku config:set TJ_SCALE_MONITOR_PROCESS=web -a my-app-web
heroku config:set TJ_SCALE_TARGET_APP=my-app-web -a my-app-web
heroku config:set TJ_SCALE_TARGET_PROCESS=web -a my-app-web
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics -a my-app-web
heroku config:set TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE=1 -a my-app-web
App my-app-workers (worker dynos) — run the reporter on worker.1 and scale worker dynos:
heroku config:set TJ_SCALE_API_TOKEN=your_token -a my-app-workers
heroku config:set TJ_SCALE_MONITOR_PROCESS=worker -a my-app-workers
heroku config:set TJ_SCALE_TARGET_APP=my-app-workers -a my-app-workers
heroku config:set TJ_SCALE_TARGET_PROCESS=worker -a my-app-workers
heroku config:set TJ_SCALE_API_URL=https://your-control-plane.example.com/api/v1/metrics -a my-app-workers
Ensure both apps have the web / worker process types in the Procfile as needed, deploy, and scale at least one dyno of each monitored type so web.1 / worker.1 exist.
Monitoring API: Your receiver at TJ_SCALE_API_URL should read the JSON body and enforce the min/max dyno bounds from its own dashboard settings when calling the Heroku Platform API (or your own scaler). Fields sent include job_count, timestamp, and when set, heroku_app and target_process.
Usage
Automatic Monitoring
Once installed and configured, the gem starts monitoring on the first dyno of each process type (web.1 and worker.1; pin one with TJ_SCALE_MONITOR_PROCESS). The loop runs every TJ_SCALE_INTERVAL_SECONDS (default 20) and POSTs metrics to the configured API.
Manual Usage
You can also use the gem's methods manually in your code:
# Get the count of pending jobs
count = TjScaleRuby::JobMonitor.pending_job_count
puts "Pending jobs: #{count}"
# Send current job count to the monitoring service
response = TjScaleRuby::JobMonitor.send_log
if response
puts "Log sent successfully"
end
Priority Filtering
You can filter jobs by priority using environment variables:
# Only count jobs with priority between 5 and 10
ENV['TJ_MIN_PRIORITY'] = '5'
ENV['TJ_MAX_PRIORITY'] = '10'
How It Works
- Initialization: When your Rails application starts, the
Railtiechecks if it's running on the first dyno of a monitored process type (web.1orworker.1, auto-detected fromDYNO;TJ_SCALE_MONITOR_PROCESSpins one) - Background Thread: If conditions are met, a background thread is started
- Monitoring Loop: Every N seconds (
TJ_SCALE_INTERVAL_SECONDS), the thread POSTs a payload: on worker reporters it counts waiting jobs on the active backend; on web reporters it sends router queue time (middleware / env) plus request volume and average response time for the interval. - Job counting (worker reporters, Delayed Job backend):
pending_job_countfilters jobs based on:- Jobs past their
run_attime (with 5 second buffer) - Jobs that haven't failed (
failed_at IS NULL) - Jobs that aren't locked (
locked_at IS NULL) - Optional priority range filters
- Jobs past their
- Job counting (worker reporters, Sidekiq backend):
pending_job_countsums enqueued jobs across queues (all queues, or only those inTJ_SCALE_SIDEKIQ_QUEUES), andqueue_time_sis the latency of the slowest monitored queue.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Running Tests
# Run all tests
bundle exec rspec
# Run tests with documentation format
bundle exec rspec --format documentation
# Run a specific test file
bundle exec rspec spec/lib/tj_scale_ruby/models/tj_scale_ruby_spec.rb
Code Quality
The project uses Standard for code formatting:
# Check code style
bundle exec rake standard
# Auto-fix code style issues
bundle exec rake standard:fix
Building and Publishing the Gem
For a full checklist (git, tests, version, integrate in the app, Heroku), see End-to-end guide above.
Building the Gem
- Update the version in
lib/tj_scale_ruby/version.rb - Ensure files are tracked by git (the gemspec uses
git ls-files). - Build the gem:
gem build tj-scale.gemspec
This will create a .gem file in the current directory.
Publishing to RubyGems
- Make sure you have a RubyGems account
- Get your API key from https://rubygems.org/api_keys
- Configure your credentials:
gem push --key your_api_key tj-scale-1.0.1.gem
Or use the credentials file:
# Create ~/.gem/credentials
# Add your API key:
# ---
# :rubygems_api_key: your_api_key_here
gem push tj-scale-1.0.1.gem
Publishing to a Private Gem Server
If you're using a private gem server:
gem push tj-scale-1.0.1.gem --host https://your-gem-server.com
Deployment
Heroku Deployment
- Add the gem to your
Gemfile:
gem "tj-scale", "~> 1.0"
- Set the required environment variables:
heroku config:set TJ_SCALE_API_TOKEN=your_token
- Deploy your application:
git push heroku main
The gem will automatically start monitoring on the first worker dyno.
Docker Deployment
- Add the gem to your
Gemfile - Set environment variables in your Docker configuration
- The gem will automatically start when the Rails application initializes
Other Platforms
The gem works with any Rails application. Just ensure:
- Delayed Job or Sidekiq is configured
- Environment variables are set
- The application has worker dynos (for automatic monitoring)
Requirements
- Ruby >= 3.0.0
- Rails >= 6.1
- One job backend (worker reporters only; bring your own gem):
- delayed_job_active_record >= 4.1 and PostgreSQL, or
- sidekiq and Redis
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Untechnickle/tj-scale-gem.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for your changes
- Ensure all tests pass (
bundle exec rspec) - Ensure code style is correct (
bundle exec rake standard) - Commit your changes (
git commit -am 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
The gem is available as open source under the terms of the MIT License.
Changelog
See CHANGELOG.md for a list of changes and version history.
Support
For issues, questions, or contributions, please open an issue on the GitHub repository.
Authors
- Tanuj - Initial work - tanuj@untechnickle.com
Acknowledgments
- Built for Untechnickle
- Works with Delayed Job and Sidekiq for background job processing