Ruby on Rails offers powerful tools for implementing robust error tracking and monitoring systems. As a developer, I’ve found these techniques invaluable for maintaining healthy, high-performing applications.
Exception handling forms the foundation of error tracking in Rails. The framework provides built-in mechanisms to catch and process exceptions. We can use begin-rescue blocks to handle specific errors:
begin
# Potentially risky operation
result = some_operation()
rescue StandardError => e
Rails.logger.error("An error occurred: #{e.message}")
# Handle the error gracefully
end
For more comprehensive error handling, we can create custom error classes:
class CustomError < StandardError
attr_reader :data
def initialize(message = "A custom error occurred", data = {})
@data = data
super(message)
end
end
begin
raise CustomError.new("Something went wrong", { user_id: 123 })
rescue CustomError => e
Rails.logger.error("Custom error: #{e.message}, Data: #{e.data}")
end
Rails’ built-in logging system is a powerful tool for tracking errors and application behavior. We can use different log levels (debug, info, warn, error, fatal) to categorize messages:
Rails.logger.debug("Debug message")
Rails.logger.info("Info message")
Rails.logger.warn("Warning message")
Rails.logger.error("Error message")
Rails.logger.fatal("Fatal error message")
To enhance logging capabilities, we can use gems like lograge to generate more concise and structured log output:
# config/initializers/lograge.rb
Rails.application.configure do
config.lograge.enabled = true
config.lograge.custom_options = lambda do |event|
exceptions = %w(controller action format id)
{
params: event.payload[:params].except(*exceptions)
}
end
end
For more advanced error tracking, integrating with external services like Sentry or Rollbar can provide detailed error reports and analytics. Here’s an example of setting up Sentry in a Rails application:
# Gemfile
gem 'sentry-ruby'
gem 'sentry-rails'
# config/initializers/sentry.rb
Sentry.init do |config|
config.dsn = 'YOUR_SENTRY_DSN'
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
end
These services often provide automatic error capturing, but we can also manually report errors:
begin
1 / 0
rescue ZeroDivisionError => e
Sentry.capture_exception(e)
end
Performance monitoring is another crucial aspect of maintaining a healthy Rails application. We can use gems like scout_apm or New Relic to track application performance metrics:
# Gemfile
gem 'scout_apm'
# config/scout_apm.yml
common: &defaults
name: YOUR_APP_NAME
key: YOUR_SCOUT_KEY
production:
<<: *defaults
monitor: true
development:
<<: *defaults
monitor: false
These tools provide insights into database query performance, memory usage, and other critical metrics.
For database-specific monitoring, we can use gems like bullet to detect N+1 queries and unused eager loading:
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
Implementing custom instrumentation can provide deeper insights into application-specific operations. We can use ActiveSupport::Notifications for this purpose:
# Custom instrumentation
ActiveSupport::Notifications.instrument("process.important_task", extra: :information) do
# Important task logic
end
# Subscribing to the event
ActiveSupport::Notifications.subscribe("process.important_task") do |name, start, finish, id, payload|
duration = finish - start
Rails.logger.info("Important task took #{duration} seconds. Extra: #{payload[:extra]}")
end
For monitoring background jobs, tools like Sidekiq provide built-in web interfaces and monitoring capabilities. We can enhance this with custom metrics:
class ImportantJob
include Sidekiq::Worker
sidekiq_options queue: 'critical'
def perform(*args)
Sidekiq.redis do |conn|
conn.incr("jobs:important:count")
end
# Job logic
end
end
Error tracking in API endpoints requires special attention. We can create custom error classes and handlers for API-specific errors:
module Api
class Error < StandardError; end
class AuthenticationError < Error; end
class AuthorizationError < Error; end
class ErrorHandler
def self.call(error)
case error
when AuthenticationError
{ json: { error: 'Unauthorized' }, status: :unauthorized }
when AuthorizationError
{ json: { error: 'Forbidden' }, status: :forbidden }
else
{ json: { error: 'Internal Server Error' }, status: :internal_server_error }
end
end
end
end
# In your API controller
rescue_from Api::Error, with: Api::ErrorHandler
Monitoring database health is crucial for application performance. We can use gems like pg_query to analyze and log slow queries:
# config/initializers/db_query_analyzer.rb
ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, payload|
duration = (finish - start) * 1000
if duration > 100 # Log queries taking more than 100ms
query = PgQuery.parse(payload[:sql]).deparse
Rails.logger.warn("Slow query (#{duration.round(2)}ms): #{query}")
end
end
For monitoring external service dependencies, we can implement circuit breakers using gems like circuitbox:
# config/initializers/circuitbox.rb
Circuitbox.configure do |config|
config.default_circuit_store = Circuitbox::MemoryStore.new
end
circuit = Circuitbox.circuit(:api_service, exceptions: [Timeout::Error])
circuit.run do
# API call logic
end
Implementing health check endpoints can help in monitoring overall application health:
# config/routes.rb
Rails.application.routes.draw do
get '/health', to: 'health#check'
end
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def check
health_status = {
database: database_connected?,
redis: redis_connected?,
sidekiq: sidekiq_running?
}
if health_status.values.all?
render json: { status: 'ok' }, status: :ok
else
render json: { status: 'error', details: health_status }, status: :service_unavailable
end
end
private
def database_connected?
ActiveRecord::Base.connection.active?
rescue StandardError
false
end
def redis_connected?
Redis.new.ping == 'PONG'
rescue StandardError
false
end
def sidekiq_running?
Sidekiq::ProcessSet.new.size > 0
rescue StandardError
false
end
end
Monitoring application boot time can help identify potential issues early:
# config/application.rb
module YourApplication
class Application < Rails::Application
config.before_initialize do
@app_init_start_time = Time.now
end
config.after_initialize do
init_time = Time.now - @app_init_start_time
Rails.logger.info "Application initialized in #{init_time} seconds"
end
end
end
For monitoring memory usage, we can use the get_process_mem gem:
# Gemfile
gem 'get_process_mem'
# config/initializers/memory_monitor.rb
require 'get_process_mem'
module MemoryMonitor
def self.log_memory_usage
mem = GetProcessMem.new
Rails.logger.info "Memory usage: #{mem.mb.round(2)} MB"
end
end
# Use in your application
after_action :log_memory_usage, only: [:memory_intensive_action]
def log_memory_usage
MemoryMonitor.log_memory_usage
end
Implementing these techniques has significantly improved my ability to track and respond to errors in Rails applications. By combining built-in Rails features with external tools and custom solutions, we can create a comprehensive monitoring system that ensures our applications remain robust and performant.
Remember, the key to effective error tracking and monitoring is not just implementing these techniques, but also regularly reviewing and acting on the data they provide. This proactive approach helps in identifying potential issues before they become critical problems, ultimately leading to more stable and reliable Rails applications.