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.

RubyStructured LoggingJSONBunyan CompatibleLog AnalysisHigh Performance

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)