Ruby on Rails (Legacy)
This guide covers Ruby 3.0 (EOL: March 2024), Ruby 2.7 (EOL: March 2023), Rails 6.1 (EOL: April 2024), and Rails 5.2 (EOL: June 2022)
This guide covers OpenTelemetry instrumentation for legacy Rails applications running on end-of-life Ruby or Rails versions. The latest OTel Ruby gems now require Ruby >= 3.1, so users on older versions need pinned gem versions. While instrumentation works, these versions are no longer officially maintained or tested.
⚠️ Production Warning: Legacy versions have known limitations, security vulnerabilities, and reduced performance. We strongly recommend upgrading to supported versions. See Migration Path below.
For Ruby 3.0 and Rails 6.1, pin every OTel gem to its last compatible release
and use use_all() with BatchSpanProcessor. For Ruby 2.7, skip the all-in-one
gem, use individual instrumentation gems with older pinned versions, and switch
to SimpleSpanProcessor to avoid threading issues.
Supported Legacy Versions
This guide covers:
- Ruby 3.0 (EOL: March 2024) with Rails 6.1 (EOL: April 2024)
- Ruby 2.7 (EOL: March 2023)
- Rails 5.2 (Maintenance ended: June 2022)
- Rails 6.0 with Ruby 2.7
Not covered: Ruby 2.6 or earlier, Rails 5.1 or earlier (no OpenTelemetry support)
Ruby 3.0 / Rails 6.1
Ruby 3.0 reached EOL in March 2024 and Rails 6.1 in April 2024. Many production apps still run this combination. The latest OTel Ruby gems require Ruby >= 3.1, so you must pin gem versions to the last compatible releases.
Why Pinned Versions?
Starting in late 2024, the opentelemetry-sdk gem and its dependencies began
requiring Ruby >= 3.1. Running bundle update on a Ruby 3.0 app will pull
incompatible versions and fail. The configuration below pins every OTel gem to
its last Ruby 3.0–compatible release.
Known Limitations
- Gem version ceiling: OTel gems are pinned - no new features or bug fixes from upstream
- Logger 1.4.3 pin: newer
loggerversions breakActiveSupport::LoggerThreadSafeLevelin Rails 6.1 - Bundler 2.3.27: required in Docker to handle default gem replacement correctly
- Bootsnap incompatible: latest bootsnap (1.23.0+) does not support Ruby 3.0
Working Configuration
Gemfile
Pin OTel gems to the last Ruby 3.0–compatible versions:
source "https://rubygems.org"
ruby "~> 3.0.0"
gem "rails", "~> 6.1.0"
gem "mysql2", "~> 0.5"
gem "puma", "~> 5.0"
gem "logger", "1.4.3"
# OpenTelemetry - pinned to last Ruby 3.0 compatible versions
gem "opentelemetry-api", "1.4.0"
gem "opentelemetry-sdk", "1.7.0"
gem "opentelemetry-exporter-otlp", "0.29.1"
gem "opentelemetry-instrumentation-rails", "0.34.1"
gem "opentelemetry-instrumentation-mysql2", "0.28.0"
Swap
mysql2forpgorsqlite3as needed. The OTel core gem versions stay the same.
Initializer
use_all() auto-instruments Rack, ActionPack, ActiveRecord, ActiveSupport, and
your database adapter. The OTLP exporter reads its endpoint from environment
variables, so no hardcoded URLs are needed:
require "opentelemetry/sdk"
require "opentelemetry/exporter/otlp"
OpenTelemetry::SDK.configure do |c|
c.use_all()
end
Unlike Ruby 2.7, Ruby 3.0 works fine with BatchSpanProcessor (the SDK
default), so there is no need to switch to SimpleSpanProcessor.
Custom Spans
Wrap business logic in custom spans for richer traces:
class ItemsController < ApplicationController
def create
permitted = item_params
tracer = OpenTelemetry.tracer_provider.tracer("items-controller")
item = nil
tracer.in_span("item.create",
attributes: { "item.title" => permitted[:title] }) do |span|
item = Item.create!(permitted)
span.set_attribute("item.id", item.id)
end
render json: item, status: :created
end
private
def item_params
params.require(:item).permit(:title, :description)
end
end
Auto-instrumented DB calls nest under the custom span automatically:
HTTP POST /api/items
└── item.create (custom span)
├── Item query (ActiveRecord auto)
├── select (MySQL2 auto)
├── begin (MySQL2 auto)
├── insert (MySQL2 auto)
└── commit (MySQL2 auto)
Error Handling and Log Correlation
Record exceptions on the current span and inject trace context into logs so you can jump from a log line straight to the trace:
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
private
def handle_not_found(exception)
span = OpenTelemetry::Trace.current_span
span.record_exception(exception)
span.status = OpenTelemetry::Trace::Status.error(exception.message)
trace_id = span.context.hex_trace_id
span_id = span.context.hex_span_id
Rails.logger.error(
"[trace_id=#{trace_id} span_id=#{span_id}] " \
"#{exception.class}: #{exception.message}"
)
render json: { error: "Not found", trace_id: trace_id },
status: :not_found
end
end
Docker Setup
Dockerfile
Pin bundler to 2.3.27 and skip bootsnap:
ARG RUBY_VERSION=3.0
FROM docker.io/library/ruby:$RUBY_VERSION-slim
WORKDIR /rails
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
build-essential default-libmysqlclient-dev \
curl git libyaml-dev pkg-config && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
ENV RAILS_ENV="development" \
BUNDLE_PATH="/usr/local/bundle"
RUN gem update --system 3.3.27 && gem install bundler -v 2.3.27
COPY Gemfile ./
RUN bundle install
COPY . .
EXPOSE 3000
CMD ["bin/rails", "server", "-b", "0.0.0.0"]
Docker Compose
The app, database, and OTel Collector run together. The key environment
variables for telemetry are OTEL_SERVICE_NAME and
OTEL_EXPORTER_OTLP_ENDPOINT:
services:
app:
build: .
environment:
OTEL_SERVICE_NAME: ruby30-rails61-app
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
DATABASE_HOST: mysql
depends_on:
mysql:
condition: service_healthy
otel-collector:
condition: service_started
ports:
- "3000:3000"
otel-collector:
image: otel/opentelemetry-collector-contrib:0.144.0
ports:
- "4317:4317"
- "4318:4318"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: rails_otel_dev
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-prootpassword"]
interval: 5s
timeout: 5s
retries: 10
A complete working example is available at ruby30-rails61-mysql.
Ruby 3.0 Troubleshooting
Issue: logger gem conflicts on Rails 6.1
Cause: Newer logger versions remove methods that Rails 6.1's
ActiveSupport::LoggerThreadSafeLevel depends on.
Solution: Pin logger to 1.4.3 in your Gemfile (shown in the
configuration above).
Issue: Bundler fails to replace default gems
Cause: The system bundler in the Ruby 3.0 Docker image doesn't handle default gem replacement correctly.
Solution: Upgrade bundler to 2.3.27 before bundle install:
gem update --system 3.3.27 && gem install bundler -v 2.3.27
Issue: Bootsnap crashes on startup
Cause: Bootsnap 1.23.0+ requires Ruby 3.1.
Solution: Remove bootsnap from your Gemfile. Rails 6.1 runs fine without it — cold boot adds roughly 1–2 seconds.
Issue: No traces appearing
Cause: OTel gems may have been updated past the Ruby 3.0 ceiling.
Solution: Verify pinned versions match the table in Compatibility Matrix and run:
bundle exec ruby -e "require 'opentelemetry/sdk'; puts OpenTelemetry::SDK::VERSION"
# Expected: 1.7.0
Ruby 2.7 Support
Known Limitations
- Performance overhead: 5-10ms per request (vs 1-3ms on Ruby 3.x)
- Threading issues: BatchSpanProcessor may cause thread leaks
- Instrumentation gaps: Some newer gems don't support Ruby 2.7
- Security: No security patches since March 2023
Working Configuration
Gemfile
Lock to compatible OpenTelemetry versions:
source 'https://rubygems.org'
gem 'rails', '~> 6.1.0' # or your Rails version
# OpenTelemetry - use older versions compatible with Ruby 2.7
gem 'opentelemetry-sdk', '~> 1.1.0'
gem 'opentelemetry-exporter-otlp', '~> 0.21.0'
# Use specific instrumentation gems instead of -all
gem 'opentelemetry-instrumentation-rails', '~> 0.28.0'
gem 'opentelemetry-instrumentation-action_pack', '~> 0.9.0'
gem 'opentelemetry-instrumentation-active_record', '~> 0.6.0'
gem 'opentelemetry-instrumentation-rack', '~> 0.23.0'
# Optional: background jobs
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.0'
# Optional: HTTP clients
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.0'
Configuration
Use SimpleSpanProcessor to avoid thread issues:
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
OpenTelemetry::SDK.configure do |c|
c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'rails-app-ruby27')
c.service_version = ENV.fetch('APP_VERSION', '1.0.0')
# Use SimpleSpanProcessor to avoid Ruby 2.7 threading issues
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318')
)
)
)
# Enable only core instrumentations
c.use 'OpenTelemetry::Instrumentation::Rails'
c.use 'OpenTelemetry::Instrumentation::ActionPack'
c.use 'OpenTelemetry::Instrumentation::ActiveRecord'
c.use 'OpenTelemetry::Instrumentation::Rack'
# Optional: enable if you use Sidekiq
# c.use 'OpenTelemetry::Instrumentation::Sidekiq'
end
TRACER = OpenTelemetry.tracer_provider.tracer('rails-app', '1.0.0')
Ruby 2.7 Troubleshooting
Issue: Thread deadlocks or memory leaks
Cause: BatchSpanProcessor has known issues with Ruby 2.7's GVL
Solution: Use SimpleSpanProcessor (shown in config above)
Trade-off: Higher network overhead, but stable
Issue: Missing instrumentation for newer gems
Cause: Newer instrumentation gems require Ruby 3.0+
Solution: Manually instrument using custom spans:
class ApplicationController < ActionController::Base
around_action :trace_request
private
def trace_request
tracer = OpenTelemetry.tracer_provider.tracer('rails-app')
tracer.in_span("#{controller_name}##{action_name}", kind: :server) do |span|
span.set_attribute('http.method', request.method)
span.set_attribute('http.route', "#{controller_name}##{action_name}")
yield
span.set_attribute('http.status_code', response.status)
end
end
end
Rails 5.2 Support
Known Limitations
- ActiveRecord: May miss queries in some edge cases
- ActionCable: Not instrumented
- ActiveJob: Unreliable instrumentation
- Minitest: No test instrumentation
- Compatibility: Requires specific gem versions
Working Configuration
Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 5.2.8'
# OpenTelemetry - lock to Rails 5.2 compatible versions
gem 'opentelemetry-sdk', '~> 1.2.0'
gem 'opentelemetry-exporter-otlp', '~> 0.25.0'
# Rails 5.2 requires manual instrumentation selection
gem 'opentelemetry-instrumentation-rack', '~> 0.23.0'
gem 'opentelemetry-instrumentation-active_record', '~> 0.5.0'
# Note: opentelemetry-instrumentation-rails doesn't fully support Rails 5.2
# Use Rack instrumentation instead
Configuration
Initialize before Rails application boots:
require_relative 'boot'
require 'rails/all'
# Initialize OpenTelemetry before Rails boots
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
OpenTelemetry::SDK.configure do |c|
c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'rails52-app')
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318')
)
)
)
# Only enable instrumentations that work with Rails 5.2
c.use 'OpenTelemetry::Instrumentation::Rack'
c.use 'OpenTelemetry::Instrumentation::ActiveRecord'
end
Bundler.require(*Rails.groups)
module YourApp
class Application < Rails::Application
config.load_defaults 5.2
# ...
end
end
Rails 5.2 Troubleshooting
Issue: Missing request traces
Cause: Rails 5.2 doesn't have full Rails instrumentation support
Solution: Use Rack instrumentation + manual controller tracing:
class ApplicationController < ActionController::Base
before_action :start_trace
after_action :end_trace
private
def start_trace
@tracer = OpenTelemetry.tracer_provider.tracer('rails-app')
@current_span = @tracer.start_span(
"#{controller_name}##{action_name}",
kind: :server,
attributes: {
'http.method' => request.method,
'http.url' => request.original_url,
'http.route' => "#{controller_name}##{action_name}"
}
)
OpenTelemetry::Trace.current_span = @current_span
end
def end_trace
if @current_span
@current_span.set_attribute('http.status_code', response.status)
@current_span.finish
end
end
end
Issue: ActiveRecord queries not appearing
Cause: ActiveRecord instrumentation version mismatch
Solution: Verify gem version and test:
bundle exec rails console
# Test ActiveRecord instrumentation
require 'opentelemetry/sdk'
tracer = OpenTelemetry.tracer_provider.tracer('test')
tracer.in_span('test_query') do
User.first
end
If queries still missing, use manual instrumentation:
module TracedQueries
extend ActiveSupport::Concern
included do
around_save :trace_save
around_destroy :trace_destroy
end
private
def trace_save
tracer = OpenTelemetry.tracer_provider.tracer('active_record')
tracer.in_span("#{self.class.name}.save", kind: :client) do |span|
span.set_attribute('db.operation', 'save')
span.set_attribute('db.table', self.class.table_name)
yield
end
end
def trace_destroy
tracer = OpenTelemetry.tracer_provider.tracer('active_record')
tracer.in_span("#{self.class.name}.destroy", kind: :client) do |span|
span.set_attribute('db.operation', 'destroy')
span.set_attribute('db.table', self.class.table_name)
yield
end
end
end
# Include in your models
class User < ApplicationRecord
include TracedQueries
end
Compatibility Matrix
| Ruby Version | Rails Version | OpenTelemetry SDK | Status | Notes |
|---|---|---|---|---|
| 3.0 | 6.1 | 1.7.0 (pinned) | ⚠️ Works | Pinned gems required |
| 2.7 | 6.1 | 1.3.x | ⚠️ Works | Use SimpleSpanProcessor |
| 2.7 | 6.0 | 1.3.x | ⚠️ Works | Use SimpleSpanProcessor |
| 2.7 | 5.2 | 1.2.x | ⚠️ Limited | Manual instrumentation needed |
| 3.0 | 5.2 | 1.2.x | ⚠️ Limited | Manual instrumentation needed |
Feature Support Comparison
| Feature | Ruby 3.1+ | Ruby 3.0 (pinned) | Ruby 2.7 | Rails 5.2 |
|---|---|---|---|---|
| HTTP request tracing | ✅ Automatic | ✅ Automatic | ✅ Automatic | ⚠️ Manual |
| ActiveRecord queries | ✅ Full | ✅ Full | ✅ Full | ⚠️ Partial |
use_all() | ✅ Latest gems | ✅ Pinned gems | ❌ Individual gems | ❌ Individual gems |
| Background jobs | ✅ Sidekiq, DJ | ✅ Sidekiq, DJ | ✅ Sidekiq, DJ | ❌ No support |
| ActionCable | ✅ Automatic | ✅ Automatic | ✅ Automatic | ❌ No support |
| Custom spans | ✅ Full API | ✅ Full API | ✅ Full API | ✅ Full API |
| BatchSpanProcessor | ✅ Recommended | ✅ Works | ❌ Unstable | ❌ Unstable |
| Performance overhead | 1–3ms | 1–3ms | 5–10ms | 3–8ms |
Migration Path
Recommended Upgrade Order
Priority 1: Ruby Upgrade (Biggest impact)
Ruby 2.7 → Ruby 3.0 → Ruby 3.1 → Ruby 3.2+
Key milestone - Ruby 3.1: This is where you can drop pinned OTel gem
versions and use the latest releases (including
opentelemetry-instrumentation-all).
Benefits of upgrading past 3.0:
- Latest OTel gems with new features and bug fixes
- Security patches
- Stable BatchSpanProcessor (already works on 3.0)
- All instrumentation gems supported without version pins
Rails compatibility:
- Rails 6.1 supports Ruby 3.0+
- Rails 7.0 requires Ruby 2.7+, supports Ruby 3.x
- Rails 7.1 requires Ruby 3.0+
Priority 2: Rails Upgrade (After Ruby is upgraded)
Rails 5.2 → Rails 6.1 (LTS) → Rails 7.1 (Current LTS)
Benefits:
- Better ActiveRecord instrumentation
- ActionCable support
- ActiveJob tracing
- Future-proof
Incremental Migration Strategy
Step 1: Upgrade Ruby to 3.0 (if on 2.7)
rbenv install 3.0.7
rbenv local 3.0.7
bundle install
bundle exec rspec
At this point you can switch from SimpleSpanProcessor to BatchSpanProcessor
and use use_all() with pinned gems (see
Ruby 3.0 / Rails 6.1 above).
Step 2: Upgrade Ruby to 3.1+ (1–2 weeks)
rbenv install 3.1.4
rbenv local 3.1.4
bundle install
bundle exec rspec
# Deploy to staging, monitor for 1 week
Step 3: Update OpenTelemetry
# After Ruby 3.1 upgrade, remove version pins and use latest
gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'
Step 4: Switch to BatchSpanProcessor
If you were on Ruby 2.7 with SimpleSpanProcessor, you can now safely switch to
BatchSpanProcessor (already the default when using use_all() without
explicit processor config):
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT')
)
)
)
Step 5: Upgrade Rails (2–4 weeks)
# Follow Rails upgrade guides
# Test thoroughly at each minor version
Rails 5.2 → 6.0 → 6.1 → 7.0 → 7.1
Production Deployment Recommendations
For Ruby 2.7 Production Apps
If you must run Ruby 2.7 in production:
- Use SimpleSpanProcessor (avoid BatchSpanProcessor)
- Monitor memory usage closely
- Plan Ruby upgrade within 3-6 months
- Disable non-critical instrumentations
OpenTelemetry::SDK.configure do |c|
c.service_name = 'rails-app-ruby27'
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT')
)
)
)
# Only enable critical instrumentations
c.use 'OpenTelemetry::Instrumentation::Rails'
c.use 'OpenTelemetry::Instrumentation::ActiveRecord'
end
For Rails 5.2 Production Apps
If you must run Rails 5.2 in production:
- Test thoroughly in staging (many edge cases)
- Implement manual instrumentation for critical paths
- Monitor for missing traces
- Plan Rails upgrade to 6.1 LTS within 6 months
- Use health check endpoint to verify instrumentation
Rails.application.routes.draw do
get '/health/telemetry', to: 'health#telemetry'
end
class HealthController < ApplicationController
def telemetry
tracer = OpenTelemetry.tracer_provider.tracer('health_check')
tracer.in_span('telemetry_health_check') do |span|
span.set_attribute('rails.version', Rails.version)
span.set_attribute('ruby.version', RUBY_VERSION)
render json: {
status: 'ok',
rails_version: Rails.version,
ruby_version: RUBY_VERSION,
opentelemetry: {
sdk_version: OpenTelemetry::SDK::VERSION,
instrumented: instrumentation_status
}
}
end
end
private
def instrumentation_status
{
rack: defined?(OpenTelemetry::Instrumentation::Rack),
active_record: defined?(OpenTelemetry::Instrumentation::ActiveRecord)
}
end
end
Getting Help
Community Resources
- OpenTelemetry Ruby GitHub: Report issues
- Ruby Upgrade Guides: Rails upgrade guides
- Scout Community: Contact support for legacy version assistance
Common Questions
Q: Can I use the all-in-one instrumentation gem with Ruby 3.0?
A: No. The -all meta-gem pulls latest versions that require Ruby 3.1+. Use
individual instrumentation gems with pinned versions instead (see
Ruby 3.0 / Rails 6.1).
Q: Can I use the all-in-one instrumentation gem with Ruby 2.7?
A: Not recommended. Use individual instrumentation gems to avoid compatibility issues with gems that require Ruby 3.0+.
Q: Will my legacy app slow down with OpenTelemetry?
A: Yes, expect 5-10ms overhead on Ruby 2.7 vs 1-3ms on Ruby 3.x.
Q: Is Rails 5.2 instrumentation production-ready?
A: No. Rails 5.2 support is limited and untested. Upgrade to Rails 6.1+ for production observability.
Q: Can I run Ruby 2.7 with Rails 7.1?
A: No. Rails 7.1 requires Ruby 3.0 or later.
Related Guides
- Rails OpenTelemetry Instrumentation - Main guide for supported versions
- Custom Ruby Instrumentation - Manual instrumentation patterns
- Docker Compose Setup - Local development setup