ActiveRecordで発行するSQLのログをDBに保存する方法
管理ページのSQL(更新系のUPDATEやINSERT等)を保存しておきたい要望があるだろう。
(管理者の操作ログを詳細にとる代わりの代替案として等)
そんなときのやり方です。
ActiveRecordのソースを見た限りそういった機能はなさそうだったので
ActiveRecordの定義を書き換えで対応してます。
ちなみにクラス変数使ってるので非スレッドッセーフです。
そのうちrailsがマルチスレッド対応した場合はスレッドローカルストレージを使ってスレッドセーフにする必要があります。(rubyでできるか知らないですけど)
DB定義
テーブル:sql_logs
id int ID sql longtext SQL文 url text 発行元URL client_id int クライアントのID(ユーザーIDや管理者ID等を保存する) run_time float 実行時間 created_at datetime 作成日 updated_at timestamp 更新日(いらないですが一応。。)
rubyソース
environment.rbに追加
# sqlのログをDBに保存 module ActiveRecord module ConnectionAdapters class AbstractAdapter def log_info_with_db_log(sql, name, runtime) if SqlLogManager.instance.enabled # last_insert_idがSqlLogのidになってしまうので退避しておく if @db_log_write_before_insert_id_enabled @db_log_write_before_insert_id = @connection.insert_id @db_log_write_before_insert_id_enabled = false end SqlLogManager.instance.save_log(sql, runtime) end return log_info_without_db_log(sql, name, runtime) end alias_method_chain :log_info, :db_log end class MysqlAdapter < AbstractAdapter def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) if SqlLogManager.instance.enabled @db_log_write_before_insert_id_enabled = true end super sql, name insert_id = @db_log_write_before_insert_id # SqlLogをとらない場合はinsert_idがとれてないので取得する unless SqlLogManager.instance.enabled insert_id = @connection.insert_id end # 退避しておいたlast_insert_idを返す。 id_value || insert_id end end class MysqlReplicationAdapter < MysqlAdapter def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: if SqlLogManager.instance.enabled @db_log_write_before_insert_id_enabled = true end ensure_master execute(sql, name = nil) insert_id = @db_log_write_before_insert_id # SqlLogをとらない場合はinsert_idがとれてないので取得する unless SqlLogManager.instance.enabled insert_id = @connection.insert_id end # 退避しておいたlast_insert_idを返す。 id_value || insert_id end end end end
モデル
class SqlLog < ActiveRecord::Base end class SqlLogManager include Singleton attr_accessor :enabled, :client_id, :url def save_log(sql, runtime) # showとselectの場合はログに書かない。 # sqlの実行でエラーが出た場合にもログが書かれるため、Mysql::Errorも外す。ここは各DBにあわせて書き換える必要あり if sql.starts_with?("SHOW") || sql.starts_with?("SELECT") || sql.starts_with?("Mysql::Error") return end # sqllogをsaveしたときのinser文をまたsqllogとして保存しようとして無限ループするため、一時的に保存を解除する enabled_tmp = self.enabled self.enabled = false log = SqlLog.new(:sql=>sql, :run_time=>runtime, :client_id=>client_id, :url=>url) log.save! self.enabled = enabled_tmp end end
コントローラ
class ApplicationController < ActionController::Base before_filter :bf_application def bf_application logm = SqlLogManager.instance logm.enabled = false #adminページのみSQLを保存する場合のif文。 #ApplicationControllerでなくadminコントローラに書く場合等は不要 if controller_path.starts_with?("admin") logm.enabled = true logm.url = request.request_uri logm.client_id = 1#ユーザーIDや管理者ID等をいれれる end end end
追記:2008/11/7
last_insert_idがSqlLogのidになってしまう不具合があったので修正しました。
きちゃない修正ですが。。
そもそもですが、この方法をとるのではなく
Railsレシピという本のレシピ59の
実行者と実行内容の記録(ActiveRecordのオブザーバを利用する方法)を採用するほうがスマートだと思いましたorz