Ougai
Well-known choice for structured logging in Ruby. Extends standard Logger class and adds useful features such as pretty printing, child loggers, JSON logging, and exception handling with stack traces. Library specialized in structured logging.
Library
Ougai
Overview
Ougai is a structured logging library designed for Ruby that easily handles messages, custom data, and exceptions, generating logs in JSON or human-readable formats. Implemented as a subclass of Ruby's standard Logger class, it outputs JSON format compatible with Node.js Bunyan and Pino. Provides beautiful formatting using Amazing Print for standard output and achieves efficient log analysis through independent field separation of structured data. A modern Ruby logging solution optimized for advanced log management in production environments.
Details
Ougai is a Ruby library designed around the concept of "structured logging" that overcomes the limitations of traditional text-based logs. While inheriting from Ruby's standard library Logger, it provides context information embedded in messages as independent fields in the log structure. The output format is Bunyan/Pino-compatible JSON format, which is standard in the Node.js ecosystem, enabling browsing with dedicated log viewer commands. Adopts numerical format for log levels for easy analysis, enabling efficient filtering with queries like "level >= 30". When logging exceptions, it automatically adds an err property and records error information in a structured manner with name, message, and stack properties.
Key Features
- Structured JSON Output: Log output in standard JSON format compatible with Bunyan/Pino
- Ruby Logger Inheritance: Supports all features of standard Logger while maintaining compatibility with existing code
- Independent Field Separation: Records context information as independent fields separated from messages
- Numerical Log Levels: Numerical-based level management enabling efficient filtering queries
- Automatic Exception Structuring: Automatic generation of err property when errors occur and detailed information recording
- Dedicated Viewer Support: Log browsing support with Bunyan/Pino dedicated commands
Pros and Cons
Pros
- Provides advanced log analysis and query capabilities through structured logging
- Rich toolchain utilization possible through compatibility with Node.js ecosystem
- Easy introduction to existing projects with complete compatibility with Ruby standard Logger
- Simple integration with modern log management infrastructure through JSON format output
- Dramatically improved efficiency in post-processing analysis through independent context information
- Flexible and efficient filtering through numerical-based log levels
Cons
- Direct human reading can be difficult due to JSON output
- Initial learning cost required for understanding and designing structured logging concepts
- Performance impact from JSON serialization during high-volume log output
- Compatibility issues with legacy text-based log processing tools
- Risk of log complexity increase without proper structured design
- Additional considerations when integrating with non-Ruby language environments
Reference Pages
Code Examples
Basic Setup
# Gemfile
gem 'ougai'
# Installation
bundle install
# Basic usage
require 'ougai'
# Basic logger to standard output
logger = Ougai::Logger.new($stdout)
# File output logger
file_logger = Ougai::Logger.new('application.log')
# Logger with JSON formatter
json_logger = Ougai::Logger.new($stdout)
json_logger.formatter = Ougai::Formatters::Bunyan.new
# Human-readable formatter (for development)
dev_logger = Ougai::Logger.new($stdout)
dev_logger.formatter = Ougai::Formatters::Readable.new
Basic Log Output
require 'ougai'
logger = Ougai::Logger.new($stdout)
# Basic message logging
logger.info('Application started')
logger.debug('Debug information')
logger.warn('Warning message')
logger.error('Error occurred')
logger.fatal('Fatal error')
# Output example (JSON format):
# {"name":"main","hostname":"localhost","pid":12345,"level":30,"time":"2024-01-15T10:30:25.123Z","v":0,"msg":"Application started"}
# Structured data logging
user_id = 12345
action = 'login'
ip_address = '192.168.1.100'
logger.info('User action performed', {
user_id: user_id,
action: action,
ip_address: ip_address,
timestamp: Time.now
})
# Additional data specified with hash after message
logger.info('Order processed', {
order_id: 'ORD-12345',
customer_id: 'CUST-67890',
amount: 150.75,
currency: 'USD',
payment_method: 'credit_card'
})
# Nested structured data
logger.info('API request completed', {
request: {
method: 'POST',
path: '/api/v1/users',
headers: {
'Content-Type' => 'application/json',
'Authorization' => 'Bearer [REDACTED]'
}
},
response: {
status: 201,
size: 1024,
duration_ms: 245
}
})
Advanced Configuration
require 'ougai'
# Application-wide logger configuration
class ApplicationLogger
def self.setup
logger = Ougai::Logger.new($stdout)
# JSON in production, readable format in development
if Rails.env.production?
logger.formatter = Ougai::Formatters::Bunyan.new
else
logger.formatter = Ougai::Formatters::Readable.new
end
# Add application-specific information
logger.before_log = lambda do |data|
data[:app_name] = 'MyRailsApp'
data[:version] = Rails.application.config.version
data[:environment] = Rails.env
end
logger
end
end
# Logger initialization
$logger = ApplicationLogger.setup
# Logging with custom fields
class UserService
def initialize
@logger = $logger.child(service: 'UserService')
end
def create_user(params)
user_id = SecureRandom.uuid
@logger.info('User creation started', {
user_id: user_id,
email: params[:email],
registration_source: params[:source] || 'web'
})
begin
# User creation process
user = User.create!(params)
@logger.info('User created successfully', {
user_id: user.id,
email: user.email,
created_at: user.created_at
})
user
rescue StandardError => e
@logger.error('User creation failed', {
user_id: user_id,
error_class: e.class.name,
error_message: e.message,
params: params.except(:password) # Exclude password
})
raise
end
end
end
# Performance measurement
class PerformanceLogger
def initialize(logger)
@logger = logger.child(component: 'PerformanceMonitor')
end
def measure(operation_name)
start_time = Time.now
@logger.debug('Operation started', operation: operation_name)
result = yield
duration = Time.now - start_time
if duration > 1.0
@logger.warn('Slow operation detected', {
operation: operation_name,
duration_seconds: duration.round(3),
threshold_exceeded: true
})
else
@logger.info('Operation completed', {
operation: operation_name,
duration_seconds: duration.round(3)
})
end
result
rescue StandardError => e
duration = Time.now - start_time
@logger.error('Operation failed', {
operation: operation_name,
duration_seconds: duration.round(3),
error: e.message
})
raise
end
end
# Usage example
perf_logger = PerformanceLogger.new($logger)
result = perf_logger.measure('database_query') do
User.joins(:orders).where(active: true).count
end
Error Handling
require 'ougai'
class ErrorLogger
def initialize
@logger = Ougai::Logger.new($stdout)
@logger.formatter = Ougai::Formatters::Bunyan.new
end
def log_exception(exception, context = {})
# Ougai automatically structures exceptions
@logger.error('Exception occurred', context.merge(
exception_class: exception.class.name,
exception_message: exception.message,
backtrace: exception.backtrace&.first(10) # First 10 lines only
))
end
def log_validation_error(model, context = {})
if model.errors.any?
@logger.error('Validation failed', context.merge(
model_class: model.class.name,
validation_errors: model.errors.full_messages,
invalid_attributes: model.errors.keys
))
end
end
def log_api_error(response, request_context = {})
@logger.error('API request failed', request_context.merge(
status_code: response.status,
response_body: response.body,
headers: response.headers.to_hash
))
end
end
# HTTP request logging
class HttpLogger
def initialize
@logger = Ougai::Logger.new($stdout).child(component: 'HTTP')
end
def log_request(request)
@logger.info('HTTP request started', {
method: request.method,
path: request.path,
remote_ip: request.remote_ip,
user_agent: request.user_agent,
params: sanitize_params(request.params),
session_id: request.session.id
})
end
def log_response(response, duration)
level = response.status >= 400 ? :error : :info
@logger.send(level, 'HTTP request completed', {
status: response.status,
content_type: response.content_type,
content_length: response.body.length,
duration_ms: (duration * 1000).round(2)
})
end
private
def sanitize_params(params)
# Filter sensitive information
filtered = params.deep_dup
filtered.delete('password')
filtered.delete('password_confirmation')
filtered.delete('token')
filtered
end
end
# Database query logging
class DatabaseLogger
def initialize
@logger = Ougai::Logger.new($stdout).child(component: 'Database')
end
def log_query(sql, duration, result_count = nil)
log_level = determine_log_level(duration)
data = {
sql: sql,
duration_ms: duration.round(2),
slow_query: duration > 1000
}
data[:result_count] = result_count if result_count
@logger.send(log_level, 'Database query executed', data)
end
def log_transaction(operation, &block)
start_time = Time.now
@logger.debug('Transaction started', operation: operation)
result = yield
duration = Time.now - start_time
@logger.info('Transaction completed', {
operation: operation,
duration_seconds: duration.round(3),
status: 'committed'
})
result
rescue StandardError => e
duration = Time.now - start_time
@logger.error('Transaction failed', {
operation: operation,
duration_seconds: duration.round(3),
status: 'rolled_back',
error: e.message
})
raise
end
private
def determine_log_level(duration_ms)
case duration_ms
when 0..100
:debug
when 100..500
:info
when 500..1000
:warn
else
:error
end
end
end
Practical Examples
require 'ougai'
# Integration example in Rails application
class ApplicationController < ActionController::Base
before_action :setup_request_logging
after_action :log_request_completion
private
def setup_request_logging
@request_start = Time.now
@request_id = SecureRandom.uuid
# Create logger per request
@logger = Rails.logger.child(
request_id: @request_id,
controller: controller_name,
action: action_name
)
@logger.info('Request started', {
method: request.method,
path: request.path,
remote_ip: request.remote_ip,
user_agent: request.user_agent,
user_id: current_user&.id
})
end
def log_request_completion
duration = Time.now - @request_start
@logger.info('Request completed', {
status: response.status,
duration_ms: (duration * 1000).round(2),
allocations: GC.stat[:total_allocated_objects] - @initial_allocations
})
end
end
# Background job logging
class BaseJob < ApplicationJob
def perform(*args)
job_logger = Rails.logger.child(
job_class: self.class.name,
job_id: job_id,
queue: queue_name
)
job_logger.info('Job started', {
arguments: args,
scheduled_at: scheduled_at,
attempts: executions
})
start_time = Time.now
begin
result = super(*args)
duration = Time.now - start_time
job_logger.info('Job completed successfully', {
duration_seconds: duration.round(3),
result: result
})
result
rescue StandardError => e
duration = Time.now - start_time
job_logger.error('Job failed', {
duration_seconds: duration.round(3),
error_class: e.class.name,
error_message: e.message,
backtrace: e.backtrace&.first(5)
})
raise
end
end
end
# External API client logging
class ExternalApiClient
def initialize(base_url)
@base_url = base_url
@logger = Rails.logger.child(component: 'ExternalApiClient')
@http = Net::HTTP.new(URI(base_url).host, URI(base_url).port)
end
def get(path, params = {})
make_request(:get, path, params)
end
def post(path, data = {})
make_request(:post, path, data)
end
private
def make_request(method, path, data = {})
request_id = SecureRandom.uuid
start_time = Time.now
@logger.info('External API request started', {
request_id: request_id,
method: method.upcase,
url: "#{@base_url}#{path}",
data_size: data.to_json.bytesize
})
# Execute HTTP request
response = execute_http_request(method, path, data)
duration = Time.now - start_time
log_level = response.code.to_i >= 400 ? :error : :info
@logger.send(log_level, 'External API request completed', {
request_id: request_id,
status_code: response.code.to_i,
response_size: response.body.bytesize,
duration_ms: (duration * 1000).round(2),
success: response.code.to_i < 400
})
JSON.parse(response.body)
rescue StandardError => e
duration = Time.now - start_time
@logger.error('External API request failed', {
request_id: request_id,
duration_ms: (duration * 1000).round(2),
error_class: e.class.name,
error_message: e.message
})
raise
end
def execute_http_request(method, path, data)
# Actual HTTP request implementation
case method
when :get
@http.get(path)
when :post
request = Net::HTTP::Post.new(path)
request.body = data.to_json
request['Content-Type'] = 'application/json'
@http.request(request)
end
end
end
# Metrics collection and logging
class MetricsLogger
def initialize
@logger = Rails.logger.child(component: 'Metrics')
end
def log_business_metric(metric_name, value, dimensions = {})
@logger.info('Business metric recorded', {
metric_name: metric_name,
value: value,
dimensions: dimensions,
recorded_at: Time.now.iso8601
})
end
def log_system_metric(metric_name, value)
@logger.debug('System metric recorded', {
metric_name: metric_name,
value: value,
recorded_at: Time.now.iso8601
})
end
end
# Usage examples
metrics_logger = MetricsLogger.new
# Business metrics
metrics_logger.log_business_metric('user_registration', 1, {
source: 'web',
plan: 'premium',
country: 'JP'
})
# System metrics
metrics_logger.log_system_metric('memory_usage_mb', 512)
metrics_logger.log_system_metric('response_time_ms', 150)