この記事について
本記事は、2023年8月に社内で実施した勉強会の内容を基に、社外向けに再編集したものです。
記載の内容は執筆当時の情報であり、現在の仕様やベストプラクティスと異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認いただくようお願いいたします。
はじめに
あるプロジェクトでaudited
を導入した際、本番環境で大量の「stack level too deep」エラーが発生しました。
原因の特定には時間を要しましたが、本記事ではその原因の概要と、実際に行った対処方法をまとめています。
RubyGems.org – audited
https://rubygems.org/gems/audited/versions/4.2.1
audited
は、モデルの変更を検知して変更履歴を自動で記録するRubyのgemのこと。
auditedの導入
1. gemファイルへの追加
gem "audited"
2. auditedテーブルの作成
$ rails generate audited:install
$ rake db:migrate
3. 対象のmodelsにauditedを設定
class User < ApplicationRecord
audited
〜
end
4. 現在のユーザの設定
# config/initializers/audited.rbに下記を記述
Audited.current_user_method = :authenticated_user
# ApplicationControllerに現在のユーザを判定するメソッドを設定
class ApplicationController < ActionController::Base
def authenticated_user
if current_user
current_user
else
'Alexander Fleming'
end
end
end
プロジェクトでの導入初期時点での実装例
# config/initializers/audited.rb
Audited.current_user_method = :audited_update_by
# ApplicationController
class ApplicationController < ActionController::Base
def audited_update_by
if member_masquerade?
current_staff
else
if current_member.present?
current_member
else
current_staff
end
end
end
〜
対策1:devise系のカラムをauditの管理から外す
class Member < User
audited except: %i[
**last_sign_in_at** **current_sign_in_at** **last_sign_in_ip** **current_sign_in_ip**
**sign_in_count** last_accessed_on session_token confirmed_at
reset_password_sent_at reset_password_token unconfirmed_email
remember_created_at notify_count
], if: :must_be_audited?
def must_be_audited?
# updated_by以外の更新対象が存在すること
# audited_options[:except]は上でaudited except:に指定している内容、 Audited.ignored_attributesはModule側で指定されているデフォルトの値(updated_atなど)
changed_attribute_names_to_save.empty? || (changed_attribute_names_to_save - (%w[updated_by] | audited_options[:except] | Audited.ignored_attributes)).present?
end
※sign in
のたびに更新が発生するカラムをaudited
の対象とすると、不要なレコードが大量に作成されてしまいます。
そのため、devise
系カラムに限らず、不要なカラムは極力外すようにしています。
対策2:member_masquerade?
を使用しない形に修正する
# config/initializers/audited.rb
Audited.current_user_method = :audited_update_by
# ApplicationController
class ApplicationController < ActionController::Base
def audited_update_by
current_staff.present? ? current_staff : current_member
end
〜
以下のようなエラーログが出力されていたことから、member_masquerade?
メソッドが原因である可能性を疑いました。
app/controllers/base_controller.rb:75:in `member_masquerade?'
app/controllers/application_controller.rb:47:in `audited_update_by'
後述する対策3にも通ずる部分となるのですが、member_masquerade?
メソッド内でcurrent_member
を呼び出していた点が、問題の要因であったと考えられます。
def #{name}_masquerade?
return false if current_#{name}.blank?
return false if session[#{name}_helper_session_key].blank?
if Devise.masquerade_storage_method_session?
session[#{name}_helper_session_key].present?
else
::Rails.cache.exist?(#{name}_helper_session_key).present?
end
end
対策3:current_member
、current_staff
ではなくインスタンス変数を使うよう修正する
# config/initializers/audited.rb
Audited.current_user_method = :audited_update_by
# ApplicationController
class ApplicationController < ActionController::Base
def audited_update_by
@current_staff.present? ? @current_staff : @current_member
end
〜
current_member
を使用することで、warden.authenticate
が呼び出され、結果としてメンバー情報の更新 → audited
による監視 → 再びcurrent_member
の参照 → warden.authenticate
呼び出し、という循環が発生していたと考えられます。
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end