Overview
Sinatra integration encompasses connecting Sinatra web applications with external systems, databases, authentication providers, monitoring services, and deployment platforms. Ruby provides multiple integration approaches through Rack middleware, database adapters, HTTP clients, and configuration management libraries.
The Sinatra::Base
class serves as the foundation for integration patterns, offering hooks for middleware registration, configuration management, and request lifecycle customization. Integration typically occurs at three levels: application-level configuration, request-level middleware processing, and route-level service interaction.
require 'sinatra'
require 'sinatra/config_file'
class App < Sinatra::Base
register Sinatra::ConfigFile
config_file 'config.yml'
configure :production do
enable :logging
set :database_url, ENV['DATABASE_URL']
end
get '/health' do
{ status: 'healthy', database: database_connected? }.to_json
end
end
Core integration components include Rack middleware for cross-cutting concerns, database connection management through ActiveRecord or Sequel, authentication integration via Warden or OmniAuth, and monitoring integration through custom middleware or service-specific gems.
Ruby's metaprogramming capabilities enable dynamic integration patterns where services register themselves with the application, middleware chains compose automatically based on configuration, and route handlers adapt behavior based on runtime environment detection.
module DatabaseIntegration
def self.registered(app)
app.helpers DatabaseHelpers
app.before { connect_database unless database_connected? }
app.after { disconnect_database if settings.environment == :development }
end
end
Sinatra::Base.register DatabaseIntegration
Basic Usage
Database integration typically uses ActiveRecord or Sequel for relational databases, with connection configuration managed through environment-specific settings. The integration establishes connections during application initialization and manages connection lifecycle through Sinatra's configuration system.
require 'sinatra'
require 'activerecord'
require 'pg'
class BlogApp < Sinatra::Base
configure do
ActiveRecord::Base.establish_connection(
adapter: 'postgresql',
host: ENV['DB_HOST'] || 'localhost',
database: ENV['DB_NAME'] || 'blog_development',
username: ENV['DB_USER'] || 'postgres',
password: ENV['DB_PASSWORD']
)
end
get '/posts' do
@posts = Post.published.limit(10)
erb :posts
end
post '/posts' do
post = Post.create(params[:post])
redirect "/posts/#{post.id}" if post.persisted?
erb :new_post
end
end
Authentication integration commonly uses Warden middleware for session management and strategy-based authentication. The integration requires middleware registration, strategy configuration, and helper method definition for route-level authentication checks.
require 'warden'
class App < Sinatra::Base
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
use Warden::Manager do |config|
config.serialize_into_session { |user| user.id }
config.serialize_from_session { |id| User.find(id) }
config.scope_defaults :default, strategies: [:password]
config.failure_app = self
end
Warden::Strategies.add(:password) do
def valid?
params['username'] && params['password']
end
def authenticate!
user = User.authenticate(params['username'], params['password'])
user ? success!(user) : fail!('Invalid credentials')
end
end
helpers do
def current_user
env['warden'].user
end
def authenticate!
redirect '/login' unless current_user
end
end
end
External API integration uses HTTP client libraries like Faraday or HTTParty, with connection configuration, timeout management, and response parsing handled through dedicated service objects. Error handling becomes critical when integrating with unreliable external services.
require 'faraday'
require 'json'
class PaymentService
def self.process_payment(amount, token)
connection = Faraday.new(url: ENV['PAYMENT_API_URL']) do |conn|
conn.request :json
conn.response :json
conn.adapter Faraday.default_adapter
conn.options.timeout = 10
end
response = connection.post('/charges') do |req|
req.headers['Authorization'] = "Bearer #{ENV['PAYMENT_API_KEY']}"
req.body = { amount: amount, source: token }
end
response.body
rescue Faraday::TimeoutError
{ error: 'Payment service timeout' }
end
end
post '/checkout' do
result = PaymentService.process_payment(params[:amount], params[:token])
if result[:error]
status 500
{ error: result[:error] }.to_json
else
{ charge_id: result[:id], status: 'completed' }.to_json
end
end
Configuration management integration uses gems like dotenv
for environment variable loading or config
for hierarchical configuration management. The integration occurs during application initialization, with environment-specific overrides and secret management.
require 'dotenv/load'
require 'yaml'
class App < Sinatra::Base
configure do
config_file = File.join(settings.root, 'config', "#{settings.environment}.yml")
set :config, YAML.load_file(config_file) if File.exist?(config_file)
set :redis_url, ENV['REDIS_URL'] || settings.config['redis']['url']
set :smtp_settings, {
address: ENV['SMTP_HOST'] || settings.config['smtp']['host'],
port: ENV['SMTP_PORT'] || settings.config['smtp']['port'],
authentication: :plain,
user_name: ENV['SMTP_USER'],
password: ENV['SMTP_PASSWORD']
}
end
end
Advanced Usage
Middleware composition enables complex integration patterns where multiple services coordinate through request processing pipelines. Custom middleware classes implement specific integration logic while maintaining separation of concerns and request flow control.
class DatabaseTransactionMiddleware
def initialize(app)
@app = app
end
def call(env)
if transactional_request?(env)
ActiveRecord::Base.transaction do
response = @app.call(env)
raise ActiveRecord::Rollback if error_response?(response)
response
end
else
@app.call(env)
end
end
private
def transactional_request?(env)
%w[POST PUT PATCH DELETE].include?(env['REQUEST_METHOD']) &&
!env['PATH_INFO'].start_with?('/api/webhooks')
end
def error_response?(response)
status = response.first
status >= 400 && status < 500
end
end
class App < Sinatra::Base
use DatabaseTransactionMiddleware
post '/users' do
user = User.create!(params[:user])
AuditLog.create!(action: 'user_created', user_id: user.id)
EmailService.send_welcome_email(user)
status 201
user.to_json
end
end
Extension-based integration allows modular functionality through Sinatra's extension system. Extensions encapsulate integration logic, register helpers, and configure middleware while maintaining reusability across applications.
module Sinatra
module CacheIntegration
module Helpers
def cache_key(*parts)
['app', settings.environment, *parts].join(':')
end
def cached(key, expires_in: 3600, &block)
cache = settings.cache_store
cached_value = cache.get(cache_key(key))
return cached_value if cached_value
value = yield
cache.set(cache_key(key), value, expires_in)
value
end
end
def self.registered(app)
app.helpers Helpers
app.set :cache_store, Redis.new(url: ENV['REDIS_URL'])
app.before do
if request.get? && cacheable_path?
@cache_key = request.path_info
@cached_response = settings.cache_store.get(cache_key(@cache_key))
if @cached_response
content_type :json
halt 200, @cached_response
end
end
end
app.after do
if @cache_key && response.status == 200
settings.cache_store.set(
cache_key(@cache_key),
response.body.join,
300
)
end
end
end
def cacheable_path?
request.path_info.start_with?('/api/') && !request.path_info.include?('user')
end
end
register CacheIntegration
end
Service object integration separates business logic from route handling while maintaining clean interfaces for external service communication. Service objects handle complex integration workflows, error recovery, and state management.
class OrderProcessingService
def initialize(payment_service:, inventory_service:, notification_service:)
@payment_service = payment_service
@inventory_service = inventory_service
@notification_service = notification_service
end
def process_order(order_params)
order = Order.create!(order_params)
inventory_result = @inventory_service.reserve_items(order.items)
raise InventoryError, inventory_result[:error] unless inventory_result[:success]
payment_result = @payment_service.charge_payment(
order.total_amount,
order_params[:payment_token]
)
if payment_result[:success]
order.update!(
status: 'paid',
payment_id: payment_result[:payment_id]
)
@notification_service.send_confirmation(order)
{ success: true, order_id: order.id }
else
@inventory_service.release_reservation(inventory_result[:reservation_id])
order.update!(status: 'failed', error: payment_result[:error])
{ success: false, error: payment_result[:error] }
end
rescue StandardError => e
order&.update!(status: 'failed', error: e.message)
@inventory_service.release_reservation(inventory_result[:reservation_id]) if inventory_result
{ success: false, error: e.message }
end
end
class App < Sinatra::Base
configure do
set :order_service, OrderProcessingService.new(
payment_service: PaymentService.new,
inventory_service: InventoryService.new,
notification_service: NotificationService.new
)
end
post '/orders' do
result = settings.order_service.process_order(params[:order])
if result[:success]
status 201
{ order_id: result[:order_id] }.to_json
else
status 422
{ error: result[:error] }.to_json
end
end
end
WebSocket integration enables real-time communication through Rack middleware or dedicated WebSocket servers. The integration requires connection management, message routing, and state synchronization between WebSocket handlers and HTTP routes.
require 'faye/websocket'
require 'thread'
class WebSocketManager
def initialize
@clients = {}
@mutex = Mutex.new
end
def add_client(ws, user_id)
@mutex.synchronize do
@clients[user_id] ||= []
@clients[user_id] << ws
end
end
def remove_client(ws, user_id)
@mutex.synchronize do
@clients[user_id]&.delete(ws)
@clients.delete(user_id) if @clients[user_id]&.empty?
end
end
def broadcast_to_user(user_id, message)
@mutex.synchronize do
@clients[user_id]&.each do |ws|
ws.send(message.to_json) if ws.ready_state == Faye::WebSocket::API::OPEN
end
end
end
end
class App < Sinatra::Base
set :websocket_manager, WebSocketManager.new
get '/ws' do
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env)
user_id = authenticate_websocket_user(request.params['token'])
ws.on :open do |event|
settings.websocket_manager.add_client(ws, user_id)
end
ws.on :message do |event|
data = JSON.parse(event.data)
handle_websocket_message(user_id, data)
end
ws.on :close do |event|
settings.websocket_manager.remove_client(ws, user_id)
end
ws.rack_response
else
status 400
'WebSocket connection required'
end
end
post '/notifications/:user_id' do
settings.websocket_manager.broadcast_to_user(
params[:user_id].to_i,
{ type: 'notification', data: params[:message] }
)
status 200
'Notification sent'
end
end
Error Handling & Debugging
Integration error handling requires comprehensive exception management across service boundaries, with specific strategies for network failures, timeout conditions, and external service degradation. Ruby's exception hierarchy supports granular error classification and recovery strategies.
class IntegrationError < StandardError; end
class ServiceUnavailableError < IntegrationError; end
class AuthenticationError < IntegrationError; end
class ValidationError < IntegrationError; end
class ExternalServiceClient
def initialize(base_url, api_key, timeout: 10, retries: 3)
@base_url = base_url
@api_key = api_key
@timeout = timeout
@retries = retries
end
def make_request(endpoint, params = {})
attempt = 0
begin
attempt += 1
response = http_client.post(endpoint, params.to_json)
case response.status
when 200..299
JSON.parse(response.body)
when 401, 403
raise AuthenticationError, "Authentication failed: #{response.body}"
when 422
raise ValidationError, "Validation failed: #{response.body}"
when 500..599
raise ServiceUnavailableError, "Service error: #{response.status}"
else
raise IntegrationError, "Unexpected response: #{response.status}"
end
rescue Net::TimeoutError, Net::OpenTimeout => e
if attempt <= @retries
sleep(2 ** attempt)
retry
end
raise ServiceUnavailableError, "Service timeout after #{@retries} attempts"
rescue JSON::ParserError => e
raise IntegrationError, "Invalid response format: #{e.message}"
end
end
private
def http_client
@http_client ||= Net::HTTP.new(URI(@base_url).host).tap do |http|
http.read_timeout = @timeout
http.open_timeout = @timeout
end
end
end
Application-level error handling provides consistent error responses and logging while maintaining service stability during integration failures. The error handling strategy differentiates between recoverable and fatal errors, implementing appropriate fallback behaviors.
class App < Sinatra::Base
configure do
set :show_exceptions, false
set :raise_errors, false
end
error IntegrationError do
error = env['sinatra.error']
logger.error "Integration error: #{error.class} - #{error.message}"
logger.error error.backtrace.join("\n") if settings.development?
status 503
{
error: 'Service temporarily unavailable',
type: 'integration_error',
retry_after: calculate_retry_delay(error)
}.to_json
end
error AuthenticationError do
error = env['sinatra.error']
logger.warn "Authentication error: #{error.message}"
status 401
{
error: 'Authentication required',
type: 'auth_error'
}.to_json
end
error ValidationError do
error = env['sinatra.error']
logger.info "Validation error: #{error.message}"
status 422
{
error: 'Invalid request data',
type: 'validation_error',
details: parse_validation_details(error.message)
}.to_json
end
not_found do
content_type :json
{ error: 'Resource not found' }.to_json
end
private
def calculate_retry_delay(error)
case error
when ServiceUnavailableError
60 # seconds
else
30
end
end
def parse_validation_details(message)
JSON.parse(message)
rescue JSON::ParserError
{ message: message }
end
end
Database integration error handling manages connection failures, transaction rollbacks, and data consistency issues. The error handling strategy includes connection pool management, deadlock detection, and automatic retry logic for transient database errors.
class DatabaseMiddleware
def initialize(app)
@app = app
@max_retries = 3
@deadlock_retry_delay = 0.1
end
def call(env)
attempt = 0
begin
attempt += 1
@app.call(env)
rescue ActiveRecord::ConnectionNotEstablished => e
logger.error "Database connection lost: #{e.message}"
ActiveRecord::Base.establish_connection
if attempt <= @max_retries
retry
else
[503, {}, ['Database unavailable']]
end
rescue ActiveRecord::Deadlocked => e
logger.warn "Database deadlock detected: #{e.message}"
if attempt <= @max_retries
sleep(@deadlock_retry_delay * attempt)
retry
else
[409, {}, ['Resource conflict - please retry']]
end
rescue ActiveRecord::RecordInvalid => e
logger.info "Validation failed: #{e.record.errors.full_messages}"
[422, { 'Content-Type' => 'application/json' }, [{
error: 'Validation failed',
details: e.record.errors.as_json
}.to_json]]
rescue ActiveRecord::RecordNotFound => e
[404, { 'Content-Type' => 'application/json' }, [{
error: 'Resource not found'
}.to_json]]
end
end
end
post '/users/:id' do
user = User.find(params[:id])
ActiveRecord::Base.transaction do
user.update!(params[:user])
AuditLog.create!(
user: user,
action: 'profile_updated',
changes: user.previous_changes
)
end
user.to_json
end
Circuit breaker pattern implementation prevents cascade failures when external services become unavailable. The circuit breaker monitors failure rates, implements automatic recovery detection, and provides fallback responses during service outages.
class CircuitBreaker
STATES = [:closed, :open, :half_open].freeze
def initialize(failure_threshold: 5, recovery_timeout: 60, success_threshold: 3)
@failure_threshold = failure_threshold
@recovery_timeout = recovery_timeout
@success_threshold = success_threshold
@failure_count = 0
@success_count = 0
@last_failure_time = nil
@state = :closed
end
def call
case @state
when :closed
execute_with_monitoring { yield }
when :open
check_recovery_timeout
raise CircuitBreakerOpenError, 'Circuit breaker is open'
when :half_open
execute_recovery_attempt { yield }
end
end
private
def execute_with_monitoring
result = yield
reset_failure_count
result
rescue StandardError => e
record_failure
raise e
end
def execute_recovery_attempt
result = yield
record_success
result
rescue StandardError => e
record_failure
raise e
end
def record_failure
@failure_count += 1
@last_failure_time = Time.now
if @failure_count >= @failure_threshold
@state = :open
end
end
def record_success
@success_count += 1
if @success_count >= @success_threshold
@state = :closed
reset_counters
end
end
def check_recovery_timeout
if Time.now - @last_failure_time >= @recovery_timeout
@state = :half_open
@success_count = 0
end
end
def reset_failure_count
@failure_count = 0
end
def reset_counters
@failure_count = 0
@success_count = 0
end
end
class PaymentService
def initialize
@circuit_breaker = CircuitBreaker.new(
failure_threshold: 3,
recovery_timeout: 120,
success_threshold: 2
)
end
def process_payment(amount, token)
@circuit_breaker.call do
make_payment_request(amount, token)
end
rescue CircuitBreakerOpenError
{ error: 'Payment service temporarily unavailable', retry_after: 120 }
end
end
Testing Strategies
Integration testing requires isolated test environments, service mocking, and database transaction management. Ruby's testing frameworks provide comprehensive tools for testing external service integration while maintaining test speed and reliability.
require 'minitest/autorun'
require 'rack/test'
require 'webmock/minitest'
require 'database_cleaner'
class IntegrationTest < Minitest::Test
include Rack::Test::Methods
def setup
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.start
WebMock.disable_net_connect!(allow_localhost: true)
# Setup test database state
@user = User.create!(
email: 'test@example.com',
api_key: 'test-api-key-123'
)
end
def teardown
DatabaseCleaner.clean
WebMock.reset!
end
def app
TestApp
end
def test_successful_payment_integration
# Mock external payment service
stub_request(:post, 'https://api.payment-provider.com/charges')
.with(
body: { amount: 1000, source: 'tok_123' },
headers: { 'Authorization' => 'Bearer test-key' }
)
.to_return(
status: 200,
body: { id: 'ch_123', status: 'succeeded' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
post '/payments', {
amount: 1000,
token: 'tok_123'
}, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
assert_equal 201, last_response.status
response_body = JSON.parse(last_response.body)
assert_equal 'ch_123', response_body['charge_id']
assert_equal 'succeeded', response_body['status']
# Verify database state
payment = Payment.find_by(charge_id: 'ch_123')
assert_not_nil payment
assert_equal @user.id, payment.user_id
assert_equal 1000, payment.amount
end
def test_payment_service_timeout
stub_request(:post, 'https://api.payment-provider.com/charges')
.to_timeout
post '/payments', {
amount: 1000,
token: 'tok_123'
}, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
assert_equal 503, last_response.status
response_body = JSON.parse(last_response.body)
assert_equal 'Service temporarily unavailable', response_body['error']
assert response_body['retry_after'].is_a?(Integer)
end
def test_database_rollback_on_external_service_failure
# Mock successful payment service
stub_request(:post, 'https://api.payment-provider.com/charges')
.to_return(
status: 200,
body: { id: 'ch_123', status: 'succeeded' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
# Mock failing notification service
stub_request(:post, 'https://api.notification-service.com/send')
.to_return(status: 500, body: 'Internal server error')
assert_no_difference 'Payment.count' do
post '/payments', {
amount: 1000,
token: 'tok_123',
send_notification: true
}, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
end
assert_equal 503, last_response.status
end
end
Service object testing isolates business logic from framework dependencies, enabling fast unit tests with comprehensive service interaction coverage. Test doubles provide controlled external service responses while maintaining test determinism.
require 'minitest/autorun'
require 'minitest/mock'
class OrderProcessingServiceTest < Minitest::Test
def setup
@payment_service = Minitest::Mock.new
@inventory_service = Minitest::Mock.new
@notification_service = Minitest::Mock.new
@service = OrderProcessingService.new(
payment_service: @payment_service,
inventory_service: @inventory_service,
notification_service: @notification_service
)
@order_params = {
user_id: 1,
items: [
{ product_id: 1, quantity: 2, price: 500 },
{ product_id: 2, quantity: 1, price: 1000 }
],
payment_token: 'tok_test_123',
total_amount: 2000
}
end
def test_successful_order_processing
order = Minitest::Mock.new
order.expect :id, 123
order.expect :items, @order_params[:items]
order.expect :total_amount, @order_params[:total_amount]
order.expect :update!, true, [Hash]
Order.stub :create!, order do
@inventory_service.expect :reserve_items,
{ success: true, reservation_id: 'res_123' },
[@order_params[:items]]
@payment_service.expect :charge_payment,
{ success: true, payment_id: 'pay_123' },
[2000, 'tok_test_123']
@notification_service.expect :send_confirmation, true, [order]
result = @service.process_order(@order_params)
assert result[:success]
assert_equal 123, result[:order_id]
end
assert_mock @payment_service
assert_mock @inventory_service
assert_mock @notification_service
end
def test_payment_failure_releases_inventory
order = Minitest::Mock.new
order.expect :items, @order_params[:items]
order.expect :total_amount, @order_params[:total_amount]
order.expect :update!, true, [Hash]
Order.stub :create!, order do
@inventory_service.expect :reserve_items,
{ success: true, reservation_id: 'res_123' },
[@order_params[:items]]
@payment_service.expect :charge_payment,
{ success: false, error: 'Insufficient funds' },
[2000, 'tok_test_123']
@inventory_service.expect :release_reservation, true, ['res_123']
result = @service.process_order(@order_params)
refute result[:success]
assert_equal 'Insufficient funds', result[:error]
end
assert_mock @payment_service
assert_mock @inventory_service
end
def test_inventory_failure_prevents_payment
order = Minitest::Mock.new
order.expect :items, @order_params[:items]
Order.stub :create!, order do
@inventory_service.expect :reserve_items,
{ success: false, error: 'Insufficient stock' },
[@order_params[:items]]
# Payment service should not be called
assert_raises InventoryError do
@service.process_order(@order_params)
end
end
assert_mock @inventory_service
end
end
WebSocket integration testing requires connection simulation, message exchange verification, and asynchronous event handling. Testing frameworks provide utilities for WebSocket connection mocking and real-time communication verification.
require 'minitest/autorun'
require 'eventmachine'
require 'faye/websocket'
class WebSocketIntegrationTest < Minitest::Test
def setup
@received_messages = []
@connection_established = false
end
def test_websocket_authentication_and_messaging
EM.run do
# Connect to WebSocket endpoint with authentication token
ws = Faye::WebSocket::Client.new(
'ws://localhost:4567/ws?token=valid-user-token'
)
ws.on :open do |event|
@connection_established = true
# Send test message
ws.send({
type: 'chat_message',
content: 'Hello, world!',
channel: 'general'
}.to_json)
end
ws.on :message do |event|
message = JSON.parse(event.data)
@received_messages << message
# Verify message echo
if message['type'] == 'message_confirmation'
assert_equal 'Hello, world!', message['original_content']
assert_equal 'general', message['channel']
ws.close
end
end
ws.on :close do |event|
assert @connection_established
assert_equal 1, @received_messages.length
EM.stop
end
# Set timeout to prevent test hanging
EM.add_timer(5) do
flunk 'WebSocket test timed out'
EM.stop
end
end
end
def test_websocket_broadcast_functionality
user_connections = {}
EM.run do
# Simulate multiple user connections
%w[user1 user2].each do |user|
ws = Faye::WebSocket::Client.new(
"ws://localhost:4567/ws?token=#{user}-token"
)
user_connections[user] = {
websocket: ws,
received_messages: []
}
ws.on :message do |event|
message = JSON.parse(event.data)
user_connections[user][:received_messages] << message
# Check if both users received the broadcast
if user_connections.values.all? { |conn|
conn[:received_messages].any? { |msg| msg['type'] == 'broadcast' }
}
# Verify broadcast content
user_connections.each do |username, connection|
broadcast_msg = connection[:received_messages].find { |msg|
msg['type'] == 'broadcast'
}
assert_equal 'System announcement', broadcast_msg['content']
end
# Close all connections
user_connections.each { |_, conn| conn[:websocket].close }
end
end
end
# Wait for connections to establish, then trigger broadcast
EM.add_timer(0.1) do
# Simulate server-side broadcast trigger
Net::HTTP.post_form(
URI('http://localhost:4567/admin/broadcast'),
message: 'System announcement'
)
end
# Cleanup timer
EM.add_timer(5) { EM.stop }
end
end
end
Production Patterns
Production deployment patterns for Sinatra applications require careful consideration of process management, load balancing, and monitoring integration. Ruby provides deployment tools like Puma, Unicorn, and Passenger that offer different concurrency models and operational characteristics.
# config/puma.rb
workers ENV.fetch('WEB_CONCURRENCY', 2).to_i
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5).to_i
threads threads_count, threads_count
preload_app!
port ENV.fetch('PORT', 3000)
environment ENV.fetch('RACK_ENV', 'development')
before_fork do
# Disconnect from database and Redis before forking
ActiveRecord::Base.connection_pool.disconnect!
Redis.current.disconnect! if defined?(Redis)
end
on_worker_boot do
# Reconnect to external services after forking
ActiveRecord::Base.establish_connection
Redis.current = Redis.new(url: ENV['REDIS_URL']) if ENV['REDIS_URL']
end
# Graceful shutdown handling
on_worker_shutdown do
ActiveRecord::Base.connection_pool.disconnect!
end
Health check endpoints provide monitoring systems with application status information, including database connectivity, external service availability, and resource utilization metrics. The implementation includes both shallow and deep health checks for different monitoring scenarios.
class HealthCheck
def initialize(app)
@app = app
end
def call(env)
if env['PATH_INFO'] == '/health'
perform_health_check
elsif env['PATH_INFO'] == '/health/deep'
perform_deep_health_check
else
@app.call(env)
end
end
private
def perform_health_check
checks = {
status: 'healthy',
timestamp: Time.now.utc.iso8601,
version: ENV['APP_VERSION'] || 'unknown',
uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC)
}
[200, { 'Content-Type' => 'application/json' }, [checks.to_json]]
end
def perform_deep_health_check
checks = {
status: 'healthy',
timestamp: Time.now.utc.iso8601,
checks: {}
}
# Database connectivity check
checks[:checks][:database] = check_database_connection
# Redis connectivity check
checks[:checks][:redis] = check_redis_connection
# External service checks
checks[:checks][:payment_service] = check_external_service(
'https://api.payment-provider.com/health'
)
# Memory usage check
checks[:checks][:memory] = check_memory_usage
overall_status = checks[:checks].values.all? { |check| check[:status] == 'healthy' }
checks[:status] = overall_status ? 'healthy' : 'unhealthy'
status_code = overall_status ? 200 : 503
[status_code, { 'Content-Type' => 'application/json' }, [checks.to_json]]
end
def check_database_connection
ActiveRecord::Base.connection.execute('SELECT 1')
{ status: 'healthy', response_time: measure_response_time {
ActiveRecord::Base.connection.execute('SELECT 1')
}
}
rescue StandardError => e
{ status: 'unhealthy', error: e.message }
end
def check_redis_connection
Redis.current.ping
{ status: 'healthy', response_time: measure_response_time { Redis.current.ping } }
rescue StandardError => e
{ status: 'unhealthy', error: e.message }
end
def check_external_service(url)
response = Net::HTTP.get_response(URI(url))
{
status: response.code.to_i < 400 ? 'healthy' : 'unhealthy',
response_code: response.code.to_i,
response_time: measure_response_time { Net::HTTP.get_response(URI(url)) }
}
rescue StandardError => e
{ status: 'unhealthy', error: e.message }
end
def check_memory_usage
# Ruby memory usage in MB
memory_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024
{
status: memory_mb < 1024 ? 'healthy' : 'warning',
memory_mb: memory_mb,
threshold_mb: 1024
}
end
def measure_response_time
start_time = Time.now
yield
((Time.now - start_time) * 1000).round(2) # milliseconds
end
end
Logging integration provides structured logging for production monitoring, with correlation ID tracking, performance metrics, and integration event logging. The logging strategy includes request tracing, error context capture, and audit trail generation.
require 'logger'
require 'json'
require 'securerandom'
class StructuredLogger
def initialize(app)
@app = app
@logger = Logger.new(STDOUT)
@logger.formatter = method(:json_formatter)
end
def call(env)
request_id = SecureRandom.uuid
env['HTTP_X_REQUEST_ID'] = request_id
start_time = Time.now
log_request_start(env, request_id)
status, headers, response = @app.call(env)
end_time = Time.now
duration = ((end_time - start_time) * 1000).round(2)
log_request_end(env, status, duration, request_id)
[status, headers, response]
rescue StandardError => e
end_time = Time.now
duration = ((end_time - start_time) * 1000).round(2)
log_request_error(env, e, duration, request_id)
raise e
end
private
def log_request_start(env, request_id)
@logger.info({
event: 'request_start',
request_id: request_id,
method: env['REQUEST_METHOD'],
path: env['PATH_INFO'],
query_string: env['QUERY_STRING'],
user_agent: env['HTTP_USER_AGENT'],
remote_ip: env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR']
})
end
def log_request_end(env, status, duration, request_id)
@logger.info({
event: 'request_end',
request_id: request_id,
method: env['REQUEST_METHOD'],
path: env['PATH_INFO'],
status: status,
duration_ms: duration
})
end
def log_request_error(env, error, duration, request_id)
@logger.error({
event: 'request_error',
request_id: request_id,
method: env['REQUEST_METHOD'],
path: env['PATH_INFO'],
error_class: error.class.name,
error_message: error.message,
backtrace: error.backtrace&.first(10),
duration_ms: duration
})
end
def json_formatter(severity, datetime, progname, msg)
log_entry = {
timestamp: datetime.utc.iso8601,
level: severity,
hostname: ENV['HOSTNAME'] || 'unknown',
service: ENV['SERVICE_NAME'] || 'sinatra-app'
}
case msg
when Hash
log_entry.merge!(msg)
else
log_entry[:message] = msg
end
"#{log_entry.to_json}\n"
end
end
# Application-level logging helpers
module ApplicationLogging
def log_integration_event(service:, action:, status:, duration: nil, **metadata)
logger.info({
event: 'integration_event',
service: service,
action: action,
status: status,
duration_ms: duration,
request_id: env['HTTP_X_REQUEST_ID'],
**metadata
})
end
def log_business_event(event_type:, entity_type:, entity_id:, **metadata)
logger.info({
event: 'business_event',
event_type: event_type,
entity_type: entity_type,
entity_id: entity_id,
request_id: env['HTTP_X_REQUEST_ID'],
user_id: current_user&.id,
**metadata
})
end
end
class App < Sinatra::Base
helpers ApplicationLogging
post '/orders' do
start_time = Time.now
begin
result = OrderService.create_order(params[:order])
duration = ((Time.now - start_time) * 1000).round(2)
log_integration_event(
service: 'order_service',
action: 'create_order',
status: 'success',
duration: duration,
order_id: result[:order_id]
)
log_business_event(
event_type: 'order_created',
entity_type: 'order',
entity_id: result[:order_id],
amount: params[:order][:total_amount]
)
status 201
result.to_json
rescue StandardError => e
duration = ((Time.now - start_time) * 1000).round(2)
log_integration_event(
service: 'order_service',
action: 'create_order',
status: 'error',
duration: duration,
error: e.message
)
raise e
end
end
end
Configuration management for production environments requires secure secret handling, environment-specific settings, and dynamic configuration updates. Ruby provides libraries for hierarchical configuration management with environment variable override capabilities.
require 'yaml'
require 'erb'
class ConfigurationManager
def initialize(env = ENV['RACK_ENV'] || 'development')
@env = env
@config = load_configuration
@secrets = load_secrets
end
def get(key_path)
keys = key_path.split('.')
value = keys.reduce(@config) { |config, key| config&.dig(key) }
# Check for environment variable override
env_key = key_path.upcase.gsub('.', '_')
ENV[env_key] || value
end
def secret(key)
@secrets[key] || ENV[key.upcase]
end
def database_config
{
adapter: get('database.adapter'),
host: get('database.host'),
port: get('database.port'),
database: get('database.name'),
username: secret('database_user'),
password: secret('database_password'),
pool: get('database.pool_size')&.to_i || 5,
checkout_timeout: get('database.checkout_timeout')&.to_i || 5
}
end
def redis_config
{
url: secret('redis_url') || build_redis_url,
pool_size: get('redis.pool_size')&.to_i || 5,
timeout: get('redis.timeout')&.to_f || 1.0
}
end
def external_service_config(service_name)
base_config = get("external_services.#{service_name}") || {}
{
base_url: base_config['base_url'],
api_key: secret("#{service_name}_api_key"),
timeout: base_config['timeout']&.to_i || 10,
retries: base_config['retries']&.to_i || 3,
circuit_breaker: {
failure_threshold: base_config.dig('circuit_breaker', 'failure_threshold')&.to_i || 5,
recovery_timeout: base_config.dig('circuit_breaker', 'recovery_timeout')&.to_i || 60
}
}
end
private
def load_configuration
config_file = File.join(settings_root, 'config', "#{@env}.yml")
if File.exist?(config_file)
erb_template = ERB.new(File.read(config_file))
YAML.safe_load(erb_template.result, aliases: true)
else
default_configuration
end
end
def load_secrets
secrets_file = File.join(settings_root, 'config', 'secrets.yml')
if File.exist?(secrets_file)
all_secrets = YAML.load_file(secrets_file)
all_secrets[@env] || {}
else
{}
end
end
def build_redis_url
host = get('redis.host') || 'localhost'
port = get('redis.port') || 6379
database = get('redis.database') || 0
"redis://#{host}:#{port}/#{database}"
end
def settings_root
ENV['APP_ROOT'] || Dir.pwd
end
def default_configuration
{
'database' => {
'adapter' => 'postgresql',
'host' => 'localhost',
'port' => 5432,
'pool_size' => 5
},
'redis' => {
'host' => 'localhost',
'port' => 6379,
'database' => 0,
'pool_size' => 5
}
}
end
end
class App < Sinatra::Base
configure do
set :config_manager, ConfigurationManager.new
# Database configuration
ActiveRecord::Base.establish_connection(
settings.config_manager.database_config
)
# Redis configuration
redis_config = settings.config_manager.redis_config
set :redis, Redis.new(redis_config)
# External service configuration
set :payment_service_config,
settings.config_manager.external_service_config('payment_service')
end
end
Reference
Core Integration Classes
Class | Purpose | Key Methods |
---|---|---|
Sinatra::Base |
Base application class | configure , use , helpers , before , after |
Rack::Builder |
Middleware composition | use , map , run |
ActiveRecord::Base |
Database ORM integration | establish_connection , connection_pool |
Warden::Manager |
Authentication middleware | authenticate! , user , logout |
Faraday::Connection |
HTTP client integration | get , post , put , delete |
Configuration Methods
Method | Parameters | Returns | Description |
---|---|---|---|
configure(env = nil, &block) |
env (Symbol), block |
nil |
Environment-specific configuration |
set(setting, value = nil) |
setting (Symbol), value (Any) |
value |
Set application setting |
enable(setting) |
setting (Symbol) |
true |
Enable boolean setting |
disable(setting) |
setting (Symbol) |
false |
Disable boolean setting |
use(middleware, *args, &block) |
middleware (Class), args, block |
nil |
Register Rack middleware |
Database Integration Methods
Method | Parameters | Returns | Description |
---|---|---|---|
establish_connection(config) |
config (Hash) |
Connection | Create database connection |
connection_pool.disconnect! |
None | nil |
Disconnect all connections |
transaction(&block) |
block | Block result | Execute in database transaction |
with_connection(&block) |
block | Block result | Execute with database connection |
HTTP Client Configuration
Option | Type | Default | Description |
---|---|---|---|
:timeout |
Integer | 10 | Request timeout in seconds |
:open_timeout |
Integer | 10 | Connection timeout in seconds |
:retries |
Integer | 3 | Number of retry attempts |
:retry_delay |
Float | 1.0 | Delay between retries in seconds |
:headers |
Hash | {} |
Default request headers |
Authentication Configuration
Setting | Type | Description |
---|---|---|
:session_secret |
String | Secret key for session encryption |
:failure_app |
Class | Application to handle auth failures |
:default_strategies |
Array | Default authentication strategies |
:scope_defaults |
Hash | Per-scope authentication configuration |
Error Handling Classes
Exception | Parent | Usage |
---|---|---|
IntegrationError |
StandardError |
Base class for integration errors |
ServiceUnavailableError |
IntegrationError |
External service unavailable |
AuthenticationError |
IntegrationError |
Authentication failures |
ValidationError |
IntegrationError |
Request validation errors |
CircuitBreakerOpenError |
IntegrationError |
Circuit breaker is open |
Production Configuration Settings
Setting | Type | Default | Description |
---|---|---|---|
:environment |
Symbol | :development |
Application environment |
:logging |
Boolean | false |
Enable request logging |
:dump_errors |
Boolean | true |
Show error details |
:show_exceptions |
Boolean | true |
Show exception pages |
:raise_errors |
Boolean | false |
Raise unhandled exceptions |
Middleware Registration Patterns
# Basic middleware registration
use Rack::Logger
use Rack::CommonLogger
# Middleware with configuration
use Rack::Session::Cookie, {
key: 'session_id',
secret: ENV['SESSION_SECRET'],
expire_after: 86400
}
# Conditional middleware registration
use Rack::SSL if production?
# Custom middleware with initialization
use CustomMiddleware.new(option: 'value')
Environment Detection Methods
Method | Returns | Description |
---|---|---|
development? |
Boolean | True if development environment |
production? |
Boolean | True if production environment |
test? |
Boolean | True if test environment |
settings.environment |
Symbol | Current environment symbol |
Request Lifecycle Hooks
Hook | Execution | Parameters |
---|---|---|
before |
Before route handling | Filter pattern (optional) |
after |
After route handling | Filter pattern (optional) |
error |
On exception | Exception class |
not_found |
On 404 responses | None |
Common Integration Patterns
# Database connection with connection pooling
ActiveRecord::Base.establish_connection(
adapter: 'postgresql',
pool: 25,
checkout_timeout: 5,
reaping_frequency: 10
)
# Redis connection with connection pooling
Redis.current = ConnectionPool::Wrapper.new(size: 25, timeout: 5) do
Redis.new(url: ENV['REDIS_URL'])
end
# HTTP client with retry logic
Faraday.new do |conn|
conn.request :retry, max: 3, interval: 0.5
conn.request :timeout, timeout: 10
conn.response :json
conn.adapter Faraday.default_adapter
end
# Circuit breaker integration
circuit_breaker = CircuitBreaker.new(
failure_threshold: 5,
recovery_timeout: 60,
success_threshold: 3
)