Sentry Logsで実現するRailsアプリの構造化ロギング
はじめに
Railsアプリケーションの運用において、ログの管理は問題発生時のデバッグやサービス監視のために重要な構成要素です。しかし、従来のテキストベースのログでは、エラー発生時に複数のツールを行き来しながらログを検索し、原因を特定する必要がありました。
この記事では、Sentry Logs と ActiveSupport::Notifications を組み合わせて、構造化ロギングを実現する方法を解説します。実際のプロジェクトで導入した実装パターンを紹介し、ハマったポイントと解決策も共有します。
この記事で得られること
- Sentry Logsの基本的な使い方
- ActiveSupport::Notificationsを活用した構造化ロギングの実装方法
- ServiceクラスにおけるLogSubscriberの実装パターン
- 実運用で遭遇した問題と解決策
前提条件
- Ruby on Rails 8.0
- ActiveSupport::Notificationsの基本的な知識
Sentry Logsとは
Sentry Logsは、従来のログ管理ツールとは異なり、エラー、トレース、リプレイなど他のテレメトリーと統合されたログ管理機能です。
主要機能
ライブテーリング
ログをリアルタイムにストリーミングし、デプロイ後の動作確認や長時間実行されるジョブの監視ができます。
アラート機能
特定のパターン(支払い失敗、異常な処理時間など)に基づいてアラートを発火し、ユーザーからの報告前に問題を検知できます。
ダッシュボード
ログの傾向を時系列で可視化し、特定の条件下でのエラー率上昇や新機能に関連するスパイクを確認できます。
最大の特徴
Sentry Logsの最大の特徴は、ログが他のテレメトリーと統合されることです。エラー発生時に、そのエラーに関連するログを同じ画面で確認でき、複数ツール間のタブ切り替えが不要になります。
利用条件と料金
すべてのSentryユーザーは月5GBを無料で利用でき、追加分は1GBあたり$0.50です。30日間のログ保持期間が含まれます。
また、Businessプランでは追加で13ヶ月のサンプリング保持もあります。
参考:
AWS CloudWatch Logs(東京リージョン)との比較
参考として、AWS CloudWatch Logsの東京リージョンとの料金比較を示します:
| サービス | 無料枠 | 料金 | 備考 |
|---|---|---|---|
| Sentry Logs | 月5GB | $0.50/GB | 30日保持含む |
| CloudWatch Logs(標準) | 月5GB | 取込: $0.76/GB 保存: $0.033/GB |
- |
| CloudWatch Logs(低頻度) | 月5GB | 取込: $0.38/GB 保存: $0.033/GB |
- |
関連ドキュメント:
Railsでの基本的な使い方
セットアップ
Gemfileに追加して、初期化時にenable_logsをtrueに設定します。
# Gemfile
gem 'sentry-ruby'
gem 'sentry-rails'
# config/initializers/sentry.rb
Sentry.init do |config|
config.dsn = ENV['SENTRY_DSN']
# 構造化ロギングを有効化
config.enable_logs = true
config.rails.structured_logging.enabled = true
# その他の設定...
end
基本的なAPI
明示的にログを送信する場合は、Sentry.logger APIを使用します。
# 基本的な使用例
Sentry.logger.info("ユーザーがログイン")
Sentry.logger.error("支払い処理失敗", order_id: "or_2342")
# 構造化データを含む
Sentry.logger.info(
"インポート処理完了",
{
supply_area_id: 1,
processed_count: 1000,
duration_ms: 3500
}
)
以下のように記録されます。

渡した属性でフィルタリングできます。

自動キャプチャ
Railsフレームワークイベント(ActiveRecordクエリ、コントローラアクションなど)も自動的にキャプチャされます。ただし、デフォルトではActiveRecordとActionControllerのみが有効です。
Sentry.init do |config|
# 省略
config.enable_logs = true
config.rails.structured_logging.enabled = true
config.rails.structured_logging.subscribers = {
active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber,
active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber, # 追加
action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber # 追加
}
end
ActionControllerのみ有効にするなら
Sentry.init do |config|
# 省略
config.enable_logs = true
config.rails.structured_logging.enabled = true
config.rails.structured_logging.subscribers = {
action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber,
}
end
データ保護
Railsのfilter_parameters設定が自動的に適用され、パスワードやクレジットカード番号などの機密データがフィルタリングされます。
Rails.application.config.filter_parameters += [
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]
実践: ApplicationServiceでの構造化ロギング実装
ここからは、実際のプロジェクトでServiceクラスに構造化ロギングを導入した実装パターンを紹介します。
設計方針
Serviceクラスの実行状況をSentry Logsで記録する際、以下の方針で設計しました:
-
記録すべき情報を明確にする
- サービスの実行開始(perform)
- サービスの成功(success)
- サービスの失敗(failure)
- サービスの進捗報告(report)
-
ActiveSupport::Notificationsの活用
Sentry Logsのサブスクライバ
Sentry::Rails::LogSubscriberはRails標準のActiveSupport::LogSubscriberを継承して作られています。ActiveSupport::LogSubscriberはActiveSupport::Notificationsを利用してログ出力するための仕組みで、通知イベントが発行されると自動的に対応するメソッドが呼び出されます。この標準的な仕組みを使用することで、以下の利点があります:
- Railsの標準パターンに従った実装
- ロギングロジックとビジネスロジックの分離
-
Sentry::Rails::LogSubscriberとの自然な統合
-
機密情報のフィルタリング
-
Sentry::Rails::LogSubscribers::ParameterFilterを使用 - Railsの
filter_parameters設定を自動適用
-
ApplicationServiceの拡張
まず、基底クラスのApplicationServiceで、ActiveSupport::Notificationsでイベントを発行するようにします。
class ApplicationService
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations
Result = Data.define(:success, :data, :errors)
class << self
def perform(**attributes, &block)
payload = {
service_class: self,
attributes: attributes
}
service = new(**attributes)
ActiveSupport::Notifications.instrument("perform.application_service", payload) do |payload|
if service.valid?
service.perform(&block).tap do |result|
payload[:result] = result
end
else
payload[:result] = Result.new(success: false, data: nil, errors: service.errors)
end
end
end
protected :new
end
protected
def perform(&block)
raise NoMethodError, "The `call` method must be implemented in #{self.class.name}"
end
private
def success!(**data)
Result.new(success: true, data:, errors:)
end
def fail!(**data)
Result.new(success: false, data:, errors:)
end
def info(**data)
ActiveSupport::Notifications.instrument(
"info.application_service",
service_class: self.class,
attributes: attributes,
data: data
)
end
end
サービスクラスの実装例
基底クラスにロギング用のロジックを実装したので、いくつかのルールに則ってサブクラスを実装することで、ロギングについて意識する必要はなくなります。
- ロジックを
performメソッドに置く - 途中経過を
infoメソッドで伝える - 成功または失敗時に
success!またはfail!で結果を返す
class NetPosition::CalculationService < ApplicationService
attribute :supply_area_id, :integer
attribute :target_month, :date
def perform
ApplicationRecord.transaction do
demand_amount = DemandCalculator.new(supply_area_id, target_month).calculate
info(message: "需要量を取得", demand_amount:)
trade_amount = TradeCalculator.new(supply_area_id, target_month).calculate
info(message: "取引量を取得", trade_amount:)
futures_amount = FuturesCalculator.nwe(supply_area_id, target_month).calculate
info(message: "建玉量を取得", futures_amount:)
net_amount = demand_amount - trade_amount - futures_amount
save_calculation_history(
demand_amount: demand_amount,
trade_amount: trade_amount,
futures_amount: futures_amount,
net_amount: net_amount
)
success!(net_amount: net_amount)
end
rescue => e
errors.add :base, e.message
Sentry.capture_exception(e)
fail!
end
# その他のプライベートメソッド...
end
LogSubscriberの実装
次に、ActiveSupport::Notificationsのイベントを購読して、Sentry Logsに送信するLogSubscriberを実装します。
require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
module LogSubscribers
class ApplicationServiceSubscriber < Sentry::Rails::LogSubscriber
include Sentry::Rails::LogSubscribers::ParameterFilter # filter_sensitive_params
# Handle perform.application_service events
def perform(event)
service_class = event.payload[:service_class]
return unless service_class
return unless service_class < ApplicationService
message = "Service performed: #{service_class.name}"
duration = duration_ms(event)
attributes = event.payload[:attributes]
result = event.payload[:result]
attributes = {
duration_ms: duration,
service_class: service_class.name,
# IMPORTANT: 値として Array や Hash は直接渡すと Sentry Logs 上で表示できないので文字列にした
arguments: filter_sensitive_params(attributes).to_json,
success: result.success,
result: filter_sensitive_params(result.data).to_json,
errors: result.errors.full_messages.join("\n")
}
log_structured_event(
message: message,
level: :info,
attributes: attributes
)
end
# Handle report.application_service events
def info(event)
service_class = event.payload[:service_class]
return unless service_class
return unless service_class < ApplicationService
attributes = event.payload[:attributes]
data = event.payload[:data]
message = "Service performed: #{service_class.name}"
attributes = {
service_class: service_class.name,
arguments: filter_sensitive_params(attributes).to_json,
data: filter_sensitive_params(data).to_json
}
log_structured_event(
message: message,
level: :info,
attributes: attributes
)
end
end
end
Subscriberの登録
LogSubscriberを登録します。
方法はいくつかありますが、今回は Sentry SDK の設定で登録します。
require "log_subscribers/application_service_subscriber"
Sentry.init do |config|
# 省略
config.enable_logs = true
config.rails.structured_logging.enabled = true
config.rails.structured_logging.subscribers = {
active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber,
active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber,
action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber,
# カスタムサブスクライバ
application_service: ::LogSubscribers::ApplicationServiceSubscriber
}
subscribers ハッシュに追加することで、 Sentry::Rails::StructuredLogging によって適切なタイミングで attach_to されます。
実際に記録されたログ

フィルタリング
属性を使ってフィルタリングできます。

Issue画面でログを確認できる
エラーをSentryに送っていると、トランザクション内のログをあわせて確認できます。

ログを条件にアラートを設定できる
フィルタリング条件にマッチするログの出現を報告するアラートを設定することができます。

実装時の注意点と解決策
実装中に遭遇した問題と解決策を紹介します。
問題: ArrayやHashのパラメータが記録されない
当初、以下のようにattributesを設定していました。
# 問題のあるコード: HashやArrayが無視される
attributes = {
duration_ms: duration,
service_class: service_class.name,
args: filter_sensitive_arguments(args), # Array
opts: filter_sensitive_params(opts) # Hash
}
しかし、SentryのLogsでは、argsとoptsが記録されませんでした。
原因
Sentryのattributesは String や Number などの型を想定しており、構造を持ったArrayやHashをそのまま渡すと無視されます。
解決策
to_json するなどして文字列にします。
# 改善後: JSON化することで解決
attributes = {
duration_ms: duration,
service: service.class.name,
args: filter_sensitive_arguments(args).to_json,
opts: filter_sensitive_params(opts).to_json
}
問題: ActiveRecordSubscriberによる容量の急増
Sentry::Rails::LogSubscribers::ActiveRecordSubscriber を有効にすると、すべてのSQLクエリが構造化ログとして記録されます。これにより、ログの容量が急激に増大し、Sentry Logsの月間容量制限やコストに大きな影響を与える可能性があります。
# 問題のある設定: すべてのクエリがログに記録される
config.rails.structured_logging.subscribers = {
active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber, # すべてのクエリを記録
action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber,
# ...
}
原因
ActiveRecordSubscriberは、すべてのデータベースクエリに対してログイベントを発行します。アプリケーションの規模によっては、大量のクエリが実行されることもあり、ログの容量が想定以上に増加します。
また、問題のあるクエリのログはSentry Tracingなどの他の機能ですでに確認できる場合が多く、重複してログを記録する必要性が低いケースもあります。
解決策
必要なサブスクライバのみを有効にします。クエリのログが不要な場合は、ActiveRecordSubscriberを無効にします。
# 改善後: 必要なサブスクライバのみ有効化
config.rails.structured_logging.subscribers = {
# active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
# action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber,
# active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber,
# action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber,
application_service: ::LogSubscribers::ApplicationServiceSubscriber
}
用途とコストを考慮して、適切なサブスクライバを選択することが重要です。
あるいは本番環境では無効、ステージング、開発では有効という設定もよいと思います。
問題: call.component というイベントが拾えない
たとえば call というイベントを call.application_service という名前で購読しようとしても、 ApplicationServiceSubscriber#call というメソッドでは拾うことができませんでした。
原因
ActiveSupport::LogSubscriber#call はすでに存在するため、オーバーライドになってしまう。
解決策
call というイベント名は使用しない。
実際の運用で得られた効果
構造化ロギングを導入することで、以下の効果が得られました。
デバッグ時間の短縮
エラーとログが同じ画面で確認できるため、複数ツール間のタブ切り替えが不要になりました。エラー発生時に、そのエラーに関連する実行ログを即座に確認できます。
また、プレーン文字列やJSON形式のログに比べて、ログの内容を読み取りやすく、追いやすくなりました。
問題の早期発見
アラート機能を活用することで、特定のServiceクラスの失敗率が上昇した際に通知を受け取れます。ユーザーからの報告前に問題を検知できるようになりました。
パフォーマンス分析の容易さ
Sentry Tracing とあわせて、ログにduration_msを記録することで、どのServiceがどの程度時間がかかっているのか一目でわかるので、ボトルネックの特定に役立ちます。
ロギング方法の共通化
ロギングの「普通のやり方」を決めておくことで、ログ設計と実装のルールを作りやすくなります。
まとめ
Sentry LogsとActiveSupport::Notificationsを組み合わせることで、Railsアプリケーションに構造化ロギングを導入できました。
主な利点
- エラーとログの統合により、デバッグ効率が向上
- ActiveSupport::Notificationsによる疎結合な設計
- Railsの
filter_parameters設定による機密情報の自動フィルタリング - リアルタイムでのログ監視とアラート
今後の展開
導入後は、以下の展開を検討できます:
- アラート設定の最適化: 重要な処理に対して適切なアラートを設定
- ダッシュボードの作成: よく使用するクエリをダッシュボード化
-
パフォーマンス監視:
duration_msを活用した継続的なパフォーマンス監視
構造化ロギングは、アプリケーションの可観測性を大きく向上させます。Sentry Logsを活用することで、開発チーム全体で効率的に問題を発見・解決できる環境を構築できました。
Discussion