Active Supportが提供するInstrumentation APIを使ってフックを開発すると、他の開発者がそこにフックできるようになります。Railsフレームワーク内部にはさまざまなフックが用意されています。このAPIをアプリケーションで実装すると、アプリケーション(またはRubyコード片)内部でイベントが発生したときに通知を受け取れるよう他の開発者が設定できます。
たとえばActive Recordには、データベースへのSQLクエリが発行されるたびに呼び出されるフックが用意されています。このフックをサブスクライブ(購読)すると、特定のアクションでのクエリ実行数を追跡できます。他に、コントローラのアクション実行中に呼び出されるフックもあります。このフックは、たとえば特定のアクション実行に要した時間のトラッキングに利用できます。
アプリケーション内に独自のイベントを作成し、後で自分でサブスクライブして測定することも可能です。
通知をリッスンするには、ActiveSupport::Notifications.subscribeをブロック付きで利用します。ブロックが受け取る引数の個数に応じて、さまざまなデータを受け取ります。
イベントをサブスクライブするときに最初に使う方法は、単一の引数を持つブロックを使うことです。この引数は、ActiveSupport::Notifications::Eventのインスタンスになります。
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event| event.name # => "process_action.action_controller" event.duration # => 10 (in milliseconds) event.allocations # => 1826 event.payload # => {:extra=>information} Rails.logger.info "#{event} Received!" end Eventオブジェクトによって記録されたデータをすべて使わなくてもよい場合は、以下の5つの引数を取るブロックを指定することも可能です。
- イベント名
- イベントの開始時刻
- イベントの終了時刻
- イベントを発火させたinstrumenterのユニークID
- イベントのペイロード
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, payload| # 自分のコードをここに書く Rails.logger.info "#{name} Received! (started: #{started}, finished: #{finished})" # process_action.action_controller Received! (started: 2019-05-05 13:43:57 -0800, finished: 2019-05-05 13:43:58 -0800) end 経過時間を正確に算出するうえでstartedとfinishedの精度が気になる場合は、ActiveSupport::Notifications.monotonic_subscribeをお使いください。ここに渡すブロックで使える引数は上述と同じですが、startedとfinishedの値に通常のクロック時刻(wall-clock time)ではなく単調増加する精密な時刻が使われるようになります。
ActiveSupport::Notifications.monotonic_subscribe "process_action.action_controller" do |name, started, finished, unique_id, payload| # 自分のコードをここに書く duration = finished - started # 1560979.429234 - 1560978.425334 Rails.logger.info "#{name} Received! (duration: #{duration})" # process_action.action_controller Received! (duration: 1.0039) end 正規表現に一致するイベントだけをサブスクライブすることも可能です。これはさまざまなイベントを一括でサブスクライブしたい場合に便利です。以下は、ActionControllerのイベントをすべて登録する場合の例です。
ActiveSupport::Notifications.subscribe(/action_controller/) do |event| # ActionControllerの全イベントをチェック end
Ruby on Railsでは、フレームワーク内の主なイベント向けのフックが多数提供されています。イベントとペイロードについて詳しくは以下をご覧ください。
| キー | 値 |
:channel_class | チャネルのクラス名 |
:action | アクション |
:data | データ(ハッシュ) |
| キー | 値 |
:channel_class | チャネルのクラス名 |
:action | アクション |
:via | 経由先 |
| キー | 値 |
:channel_class | チャネルのクラス名 |
| キー | 値 |
:channel_class | チャネルのクラス名 |
| キー | 値 |
:broadcasting | 名前付きブロードキャスト |
:message | メッセージ(ハッシュ) |
:coder | コーダー |
| キー | 値 |
:controller | コントローラ名 |
:action | アクション |
:request | ActionDispatch::Requestオブジェクト |
:params | リクエストパラメータのハッシュ(フィルタされたパラメータは含まない) |
:headers | リクエストヘッダー |
:format | html/js/json/xmlなど |
:method | HTTPリクエストメソッド(verb) |
:path | リクエストパス |
{ controller: "PostsController", action: "new", params: { "action" => "new", "controller" => "posts" }, headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, format: :html, method: "GET", path: "/posts/new" } | キー | 値 |
:controller | コントローラ名 |
:action | アクション |
:params | リクエストパラメータのハッシュ(フィルタされたパラメータは含まない) |
:headers | リクエストヘッダー |
:format | html/js/json/xmlなど |
:method | HTTPリクエストメソッド(verb) |
:path | リクエストパス |
:request | ActionDispatch::Requestオブジェクト |
:response | ActionDispatch::Responseオブジェクト |
:status | HTTPステータスコード |
:view_runtime | ビューでかかった合計時間(ms) |
:db_runtime | データベースへのクエリ実行にかかった時間(ms) |
{ controller: "PostsController", action: "index", params: {"action" => "index", "controller" => "posts"}, headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, format: :html, method: "GET", path: "/posts", request: #<ActionDispatch::Request:0x00007ff1cb9bd7b8>, response: #<ActionDispatch::Response:0x00007f8521841ec8>, status: 200, view_runtime: 46.848, db_runtime: 0.157 } 呼び出し側でキーが追加される可能性があります。
ActionControllerはペイロードに特定の情報を追加しません。オプションは、すべてペイロード経由で渡されます。
{ status: 302, location: "http://localhost:3000/posts/new", request: <ActionDispatch::Request:0x00007ff1cb9bd7b8> } | キー | 値 |
:filter | アクションを停止させたフィルタ |
{ filter: ":halting_filter" } | キー | 値 |
:keys | 許可されていないキー |
:context | 以下のキーを持つハッシュ: :controller、:action、:params、:request |
| キー | 値 |
:filename | ファイル名 |
:type | HTTP Content-Typeヘッダー |
:disposition | HTTP Content-Dispositionヘッダー |
{ filename: "subscribers.csv", type: "text/csv", disposition: "attachment" } | キー | 値 |
:request | ActionDispatch::Requestオブジェクト |
:count | 受け取ったリクエストの回数 |
:to | 許可される最大リクエスト数 |
:within | レート制限の時間ウィンドウ |
:by | レート制限の識別子(例:IP) |
:name | レート制限の名前 |
:scope | レート制限のスコープ |
:cache_key | レート制限を保存するために使われるキャッシュキー |
{ key: 'posts/1-dashboard-view' } { key: 'posts/1-dashboard-view' } { key: 'posts/1-dashboard-view' } { key: 'posts/1-dashboard-view' } | キー | 値 |
:status | HTTPレスポンスコード |
:location | リダイレクト先URL |
:request | ActionDispatch::Requestオブジェクト |
:source_location | リダイレクトのソースロケーション(ルーティング) |
{ mailbox: #<RepliesMailbox:0x00007f9f7a8388>, inbound_email: { id: 1, message_id: "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", status: "processing" } } | キー | 値 |
:mailer | メーラークラス名 |
:message_id | Mail gemが生成したメッセージID |
:subject | メールの件名 |
:to | メールの宛先(複数可) |
:from | メールの差出人 |
:bcc | メールのBCCアドレス(複数可) |
:cc | メールのCCアドレス(複数可) |
:date | メールの日付 |
:mail | メールのエンコード形式 |
:perform_deliveries | このメッセージが配信されたかどうか |
{ mailer: "Notification", message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", subject: "Rails Guides", to: ["users@rails.com", "dhh@rails.com"], from: ["me@rails.com"], date: Sat, 10 Mar 2012 14:18:09 +0100, mail: "...", # (省略) perform_deliveries: true } | キー | 値 |
:mailer | メーラーのクラス名 |
:action | アクション |
:args | 引数 |
{ mailer: "Notification", action: "welcome_email", args: [] } | キー | 値 |
:identifier | テンプレートへの完全なパス |
:layout | 該当のレイアウト |
:locals | テンプレートに渡されるローカル変数 |
{ identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb", layout: "layouts/application", locals: { foo: "bar" } } | キー | 値 |
:identifier | テンプレートへの完全なパス |
:locals | テンプレートに渡されるローカル変数 |
{ identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb", locals: { foo: "bar" } } | キー | 値 |
:identifier | テンプレートへのフルパス |
:count | コレクションのサイズ |
:cache_hits | キャッシュからフェッチしたパーシャルの個数 |
:cache_hitsは、cached: trueをオンにしてレンダリングしたときだけ含まれます。
{ identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb", count: 3, cache_hits: 0 } | キー | 値 |
:identifier | テンプレートへのフルパス |
{ identifier: "/Users/adam/projects/notifications/app/views/layouts/application.html.erb" } | キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
| キー | 値 |
:job | Jobオブジェクト |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:error | リトライが原因で発生したエラー |
:wait | リトライの遅延 |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:jobs | Jobオブジェクトの配列 |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
:db_runtime | データベースクエリの総実行時間(ms) |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
:error | リトライが原因で発生したエラー |
| キー | 値 |
:adapter | ジョブを処理するQueueAdapterオブジェクト |
:job | Jobオブジェクト |
:error | リトライが原因で発生したエラー |
| キー | 値 |
:sql | SQL文 |
:name | 操作の名前 |
:binds | バインドするパラメータ |
:type_casted_binds | 型キャストされたバインドパラメータ |
:async | trueの場合、クエリが非同期読み込みされる |
:allow_retry | trueの場合、クエリの再試行を許可する |
:connection | コネクションオブジェクト |
:transaction | 現在のトランザクション(存在する場合) |
:affected_rows | クエリによって影響を受けた行数 |
:row_count | クエリが返した行数 |
:cached | キャッシュされたクエリが使われるとtrueが追加される |
:statement_name | SQL文の名前(PostgreSQLのみ) |
アダプタが独自のデータを追加する可能性もあります。
{ sql: "SELECT \"posts\".* FROM \"posts\" ", name: "Post Load", binds: [<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>], type_casted_binds: [11], async: false, allow_retry: true, connection: <ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>, transaction: <ActiveRecord::ConnectionAdapters::RealTransaction:0x0000000121b5d3e0> affected_rows: 0 row_count: 5, statement_name: nil, } このクエリがトランザクションのコンテキスト内で実行されなかった場合、:transactionはnilになります。
このイベントはconfig.active_record.action_on_strict_loading_violationが:logに設定されている場合にのみ発火します。
| キー | 値 |
:owner | strict_loadingが有効化されたモデル |
:reflection | 読み込もうとしている関連付けのリフレクション |
| キー | 値 |
:record_count | レコードのインスタンス数 |
:class_name | レコードのクラス |
{ record_count: 1, class_name: "User" } このイベントは、トランザクションが開始されたときにトリガーされます。
| キー | 値 |
:transaction | トランザクションオブジェクト |
:connection | コネクションオブジェクト |
ただし、Active Recordは必要になるまで実際のデータベーストランザクションを作成しない点にご注意ください。
ActiveRecord::Base.transaction do # トランザクションブロック内だが、ここではイベントはまだ何もトリガーされない # 以下の行によってActive Recordがトランザクションを開始する User.count # イベントはここでトリガーされる end
通常のネステッド呼び出しは、新しいトランザクションを作成しないことを思い出しましょう。
ActiveRecord::Base.transaction do |t1| User.count # これはt1のイベントをトリガーする ActiveRecord::Base.transaction do |t2| # 以下の行はt2のイベントを何もトリガーしない # (理由: この例では本物のデータベーストランザクションはt1しかないため) User.first.touch end end
ただし、requires_new: trueが渡されると、ネストしたトランザクションのイベントも取得されます。これは、内部的にはセーブポイントである可能性があります。
ActiveRecord::Base.transaction do |t1| User.count # これはt1のイベントをトリガーする ActiveRecord::Base.transaction(requires_new: true) do |t2| User.first.touch # これはt2のイベントをトリガーする end end
このイベントは、データベーストランザクションが終了したときにトリガーされます。トランザクションのステートは、:outcomeキーで確認できます。
| キー | 値 |
:transaction | トランザクションオブジェクト |
:outcome | :commit、:rollback、:restart、または:incomplete |
:connection | コネクションオブジェクト |
実際には、トランザクションオブジェクトで実行可能な操作はあまりありませんが、データベースアクティビティのトレースには役立つ場合があります(例: transaction.uuidをトラッキングする)。
関連付けの非推奨化モードが:notifyに設定されている状態で、非推奨化されている関連付けにアクセスすると、このイベントがトリガーされます。
| キー | 値 |
:reflection | 関連付けのリフレクション |
:message | アクセスに関する詳しいメッセージ |
:location | アクセスが行われたアプリケーションレベルの場所 |
:backtrace | :backtraceがtrueの場合のみ出力される出力される |
:locationはThread::Backtrace::Locationオブジェクトです。:backtraceが存在する場合は、Thread::Backtrace::Locationオブジェクトの配列です。これらはActive Recordのバックトレースクリーナーを使って算出されます。Railsアプリケーションでは、これはRails.backtrace_cleanerと同じです。
| キー | 値 |
:analyzer | アナライザ名(ffprobeなど) |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
:checksum | 完全性を担保するチェックサム |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
:range | 読み取りを試行するバイト範囲 |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
| キー | 値 |
:prefix | キーのプレフィックス |
:service | サービス名 |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
:exist | ファイルまたはblobが存在するかどうか |
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
:url | 生成されたURL |
このイベントはGoogle Cloud Storageサービスを使っている場合にのみ発火します。
| キー | 値 |
:key | セキュアトークン |
:service | サービス名 |
:content_type | HTTP Content-Typeフィールド |
:disposition | HTTP Content-Dispositionフィールド |
| キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
:hit | ヒットしたかどうか |
:super_operation | fetchで読み出された場合は:fetch |
| キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
:hits | ヒットしたかどうか |
:super_operation | fetch_multiで読み出された場合はfetch_multiを追加 |
このイベントは、fetchをブロック付きで呼び出した場合にのみ使われます。
| キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
#fetchに渡されたオプションは、ストアへの書き込み時にペイロードとマージされます。
{ key: "name-of-complicated-computation", store: "ActiveSupport::Cache::MemCacheStore" } このイベントは、fetchをブロック付きで呼び出した場合にのみ使われます。
| キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
fetchに渡されたオプションは、ペイロードとマージされます。
{ key: "name-of-complicated-computation", store: "ActiveSupport::Cache::MemCacheStore" } | キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
キャッシュストアが独自のデータを追加する可能性もあります。
{ key: "name-of-complicated-computation", store: "ActiveSupport::Cache::MemCacheStore" } | キー | 値 |
:key | ストアで使われるキーと値 |
:store | ストアクラス名 |
| キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
:amount | インクリメントする量 |
{ key: "bottles-of-beer", store: "ActiveSupport::Cache::RedisCacheStore", amount: 99 } | キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
:amount | デクリメントする量 |
{ key: "bottles-of-beer", store: "ActiveSupport::Cache::RedisCacheStore", amount: 1 } | キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
{ key: "name-of-complicated-computation", store: "ActiveSupport::Cache::MemCacheStore" } | キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
このイベントは、RedisCacheStore、FileStore、またはMemoryStoreを使った場合にのみ発火します。
| キー | 値 |
:key | 使われるキーのパターン |
:store | ストアクラス名 |
{ key: "posts/*", store: "ActiveSupport::Cache::RedisCacheStore" } このイベントは、MemoryStoreを使った場合にのみ発火します。
| キー | 値 |
:store | ストアクラス名 |
:size | クリーンアップ前のキャッシュにあるエントリ数 |
{ store: "ActiveSupport::Cache::MemoryStore", size: 9001 } このイベントは、MemoryStoreを使った場合にのみ発火します。
| キー | 値 |
:store | ストアクラス名 |
:key | キャッシュのターゲットサイズ(バイト単位) |
:from | prune(刈込)前のキャッシュサイズ(バイト単位) |
{ store: "ActiveSupport::Cache::MemoryStore", key: 5000, from: 9001 } | キー | 値 |
:key | ストアで使われるキー |
:store | ストアクラス名 |
{ key: "name-of-complicated-computation", store: "ActiveSupport::Cache::MemCacheStore" } | キー | 値 |
:serializer | プライマリ(意図した)シリアライザ |
:fallback | フォールバック(実際の)シリアライザ |
:serialized | シリアライズされた文字列 |
:deserialized | デシリアライズされた値 |
{ serializer: :json_allow_marshal, fallback: :marshal, serialized: "\x04\b{\x06I\"\nHello\x06:\x06ETI\"\nWorld\x06;\x00T", deserialized: { "Hello" => "World" }, } | キー | 値 |
:message | 非推奨機能の警告メッセージ |
:callstack | 非推奨警告の発生元 |
:gem_name | 非推奨警告で報告されたgem名 |
:deprecation_horizon | 非推奨の振る舞いが削除されるバージョン |
| キー | 値 |
:initializer | config/initializersで読み込まれたイニシャライザへのパス |
instrumentationの途中で例外が発生すると、ペイロードにその情報が含まれます。
| キー | 値 |
:exception | 2個の要素(例外クラス名とメッセージ)を持つ配列 |
:exception_object | 例外オブジェクト |
独自のイベントを自由に追加できます。Active Supportは、面倒な作業を代行してくれます。イベント追加は、name、payload、ブロックを指定してActiveSupport::Notifications.instrumentを呼び出すだけで完了します。 通知は、ブロックが戻ってから送信されます。Active Supportでは、開始時刻、終了時刻、InstrumenterのユニークIDが生成されます。instrument呼び出しに渡されるすべてのデータがペイロードに含まれます。
以下に例を示します。
ActiveSupport::Notifications.instrument "my.custom.event", this: :data do # 自分のコードをここに書く end
これで、次のようにイベントをリッスンできるようになります。
ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data| puts data.inspect # {:this=>:data} end 以下のように、ブロックを渡さずにinstrumentを呼び出すことも可能です。これにより、instrumentationインフラストラクチャを他のメッセージング用途に活用できます。
ActiveSupport::Notifications.instrument "my.custom.event", this: :data ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data| puts data.inspect # {:this=>:data} end 独自のイベントを作成するときは、Railsの規約に従ってください。形式は「event.library」を使います。 たとえば、アプリケーションがツイートを送信するのであれば、イベント名はtweet.twitterとなります。
🖋 GitHubで編集を提案する / 📕 英語で読む
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。
- Star
-
-