Ougai
Rubyにおける構造化ログの有名な選択肢。標準Loggerクラスを拡張し、プリティプリント、子ロガー、JSONロギング、スタックトレース付き例外処理などの便利な機能を追加。構造化ログに特化したライブラリ。
ライブラリ
Ougai
概要
Ougaiは、Ruby向けに設計された構造化ログライブラリで、メッセージ、カスタムデータ、例外を簡単に処理し、JSONまたは人間が読みやすい形式でログを生成します。Ruby標準のLoggerクラスのサブクラスとして実装され、Node.jsのBunyanやPinoと互換性のあるJSON形式を出力。標準出力にはAmazing Printを使用した美しいフォーマットを提供し、構造化データの独立フィールド化により効率的なログ分析を実現。本番環境での高度なログ管理に最適化された現代的なRubyロギングソリューションです。
詳細
Ougaiは「構造化ログ」のコンセプトを中心に設計されたRubyライブラリで、従来のテキストベースログの制約を克服します。Ruby標準ライブラリのLoggerを継承しながら、メッセージに埋め込まれたコンテキスト情報を独立したログ構造のフィールドとして提供。出力形式はNode.jsエコシステムの標準であるBunyan/Pino互換のJSONフォーマットで、専用ログビューアコマンドでのブラウジングが可能。ログレベルには分析しやすい数値形式を採用し、「level >= 30」のようなクエリで効率的なフィルタリングを実現。例外ログ時には自動的にerr プロパティを追加し、name、message、stackプロパティでエラー情報を構造化して記録します。
主な特徴
- 構造化JSON出力: Bunyan/Pino互換の標準JSON形式でのログ出力
- Ruby Logger継承: 標準Loggerの全機能をサポートし既存コードとの互換性維持
- 独立フィールド化: コンテキスト情報をメッセージから分離し独立フィールドとして記録
- 数値ログレベル: 効率的なフィルタリングクエリを可能にする数値ベースレベル管理
- 例外自動構造化: エラー発生時のerr プロパティ自動生成と詳細情報記録
- 専用ビューア対応: Bunyan/Pino専用コマンドでのログブラウジングサポート
メリット・デメリット
メリット
- 構造化ログによる高度なログ分析とクエリ機能を提供
- Node.jsエコシステムとの互換性で豊富なツールチェーン活用が可能
- Ruby標準Loggerとの完全互換性で既存プロジェクトへの導入が容易
- JSON形式出力により現代的なログ管理基盤との統合が簡単
- コンテキスト情報の独立化で後処理分析の効率性が大幅向上
- 数値ベースログレベルによる柔軟で効率的なフィルタリング
デメリット
- JSON出力により人間による直接読み取りが困難な場合がある
- 構造化ログの概念理解と設計に初期学習コストが必要
- 大量ログ出力時のJSON シリアライゼーションによるパフォーマンス影響
- レガシーなテキストベースログ処理ツールとの互換性問題
- 適切な構造化設計なしでは逆にログの複雑化を招くリスク
- Ruby以外の言語環境との統合時に追加考慮事項が発生
参考ページ
書き方の例
基本セットアップ
# Gemfile
gem 'ougai'
# インストール
bundle install
# 基本的な使用法
require 'ougai'
# 標準出力への基本ロガー
logger = Ougai::Logger.new($stdout)
# ファイル出力ロガー
file_logger = Ougai::Logger.new('application.log')
# JSON フォーマッター付きロガー
json_logger = Ougai::Logger.new($stdout)
json_logger.formatter = Ougai::Formatters::Bunyan.new
# 人間が読みやすいフォーマッター(開発用)
dev_logger = Ougai::Logger.new($stdout)
dev_logger.formatter = Ougai::Formatters::Readable.new
基本的なログ出力
require 'ougai'
logger = Ougai::Logger.new($stdout)
# 基本的なメッセージログ
logger.info('Application started')
logger.debug('Debug information')
logger.warn('Warning message')
logger.error('Error occurred')
logger.fatal('Fatal error')
# 出力例(JSON形式):
# {"name":"main","hostname":"localhost","pid":12345,"level":30,"time":"2024-01-15T10:30:25.123Z","v":0,"msg":"Application started"}
# 構造化データ付きログ
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
})
# メッセージ後にハッシュで追加データを指定
logger.info('Order processed', {
order_id: 'ORD-12345',
customer_id: 'CUST-67890',
amount: 150.75,
currency: 'USD',
payment_method: 'credit_card'
})
# ネストした構造化データ
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
}
})
高度な設定
require 'ougai'
# アプリケーション全体のロガー設定
class ApplicationLogger
def self.setup
logger = Ougai::Logger.new($stdout)
# 本番環境ではJSON、開発環境では読みやすい形式
if Rails.env.production?
logger.formatter = Ougai::Formatters::Bunyan.new
else
logger.formatter = Ougai::Formatters::Readable.new
end
# アプリケーション固有情報を追加
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 = ApplicationLogger.setup
# カスタムフィールド付きログ
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 = 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) # パスワードを除外
})
raise
end
end
end
# パフォーマンス測定
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
# 使用例
perf_logger = PerformanceLogger.new($logger)
result = perf_logger.measure('database_query') do
User.joins(:orders).where(active: true).count
end
エラーハンドリング
require 'ougai'
class ErrorLogger
def initialize
@logger = Ougai::Logger.new($stdout)
@logger.formatter = Ougai::Formatters::Bunyan.new
end
def log_exception(exception, context = {})
# Ougaiは例外を自動的に構造化
@logger.error('Exception occurred', context.merge(
exception_class: exception.class.name,
exception_message: exception.message,
backtrace: exception.backtrace&.first(10) # 最初の10行のみ
))
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 リクエストのログ記録
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)
# 機密情報をフィルタリング
filtered = params.deep_dup
filtered.delete('password')
filtered.delete('password_confirmation')
filtered.delete('token')
filtered
end
end
# データベースクエリのログ記録
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
実用例
require 'ougai'
# Rails アプリケーションでの統合例
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
# リクエストごとのロガーを作成
@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
# バックグラウンドジョブのログ記録
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
# API クライアントのログ記録
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
})
# HTTPリクエスト実行
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)
# 実際のHTTPリクエスト実装
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
# メトリクス収集とログ記録
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
# 使用例
metrics_logger = MetricsLogger.new
# ビジネスメトリクス
metrics_logger.log_business_metric('user_registration', 1, {
source: 'web',
plan: 'premium',
country: 'JP'
})
# システムメトリクス
metrics_logger.log_system_metric('memory_usage_mb', 512)
metrics_logger.log_system_metric('response_time_ms', 150)