予期しないSQLエラーが起きたとき、すぐに検知&調査をしたいものです。そこで、検知方法としてメール送信、調査方法としてエラー情報をDBに保存する方法をcakephpで実装してみました。
cakephpには標準でエラーのログ機能はあるようですが、さらっと見た限りテキストへのログである点と、それだけでは調査する際に充分な情報ではないと思ったため、今回はこの方法で実装しました。
ポイントは以下、
- SQLエラー時に起動する
- そのSQL文とその他関連情報をDBに保存&アラートメール
SQLエラー時に起動する
まず、エラー時に起動させる共通処理を探します。
cakephpでは、主に2種類のSQL処理があるようで、
- find()、read()、save()といった、ほぼ自動的にSQL文が生成される場合
- query()を使ったSQLをべた書きする方法
があります。で、調べてみるとこの二つに用意されているエラー時の処理が共通化されていません。(参考:cakephp1.2でqueryした時のエラーをキャッチしたい)
実際動作を確認してみると、1.の場合は、onError()が呼び出されますが、2.のquery()の場合は、errorプロパティがnullになるだけでした。(ver 1.2.3.8166)(もしかしたら何か呼び出されてるのかな?あったら教えてください><)
そこで、すべてのSQLに対応できるように、2パターンのエラー時の処理をModel(/cake/libs/model/model.php)に加えました。それが以下、
function onError() {
// papettoTV add start
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$this->loggingSqlError($db);
// papettoTV add end
}
まず、1.のパターンで呼び出されるonErrorメソッドはもとは中身が空なので、sql関連の情報がある$dbオブジェクトの参照を呼び出してログメソッド(loggingSqlError)を呼び出します。中身はあとで説明します。次にquery()時ですが、
function query() {
$params = func_get_args();
$db =& ConnectionManager::getDataSource($this->useDbConfig);
// papettoTV modified start
// return call_user_func_array(array(&$db, 'query'), $params);
$results = call_user_func_array(array(&$db, 'query'), $params);
// SQL成功
if (is_array($results)) {
return $results;
// SQL失敗
}else{
// エラーログ用SQLで無い場合
if(!(isset($params[1]) && $params[1] === true)){
$this->loggingSqlError($db);
}
return false;
}
// papettoTV modified end
}
元は、call_user_func_array関数の返り値をreturnしていましたが、一旦、$resultsで受けて、$resultsが配列なら成功で、そうでないなら失敗と判断し、loggingSqlErrorを呼び出しています。先ほどと少し違うのは、loggingSqlError呼び出す際に条件文がついていることです。これも後ほど説明します。
これでSQLエラーをすべてloggingSqlErrorで拾える形ができました。ここまでいけば後は簡単、loggingSqlError内で、ログのDB保存とメール送信をするのみです。
SQL文とその他関連情報をDBに保存&アラートメール
loggingSqlErrorは同じModel内で定義します
function loggingSqlError($db){
// $dbには一度の処理内のすべてのクエリが格納されており、
// エラーがおきるたびに呼び出されるため
// エラーがあり、かつ、最新のsqlエラーのみ取り出す
// クエリ配列を退避
$sql_arr = $db->_queriesLog;
// foreach時に最新のsqlから参照するようにする
arsort($sql_arr);
foreach($sql_arr as $queries){
if($queries["error"] !== null && $db->lastError()==$queries["error"]){
// logging
$this->_loggingSqlError($queries);
// アラートメール
$this->sendSqlError($queries);
}
}
}
コメントにもあるように、最新のSQLのみ保存するようforeachで回しながらエラーが発生した、かつ、最新のSQLであればDB保存(_loggingSqlError)&メール送信(sendSqlError)します。(なんか力技・・・。各エラーSQLにIDとか付いてればな・・・・)
_loggingSqlError詳細
_loggingSqlErrorはエラーログテーブルに$_SERVERや会員番号等の必要な情報をinsertするだけなのですが、じつは、ここで気をつけないといけないことがあります。それは、そのinsert文もまたquery()で実行する点です。
もしこのエラーログSQL文でエラーがおきると、そのエラーでloggingSqlErrorが呼び出され、そのinsertでまたエラーで・・・となってしまい、insertし続ける無限ループに陥いる可能性があります。
「そんなこと、、、ダメ、ぜったい」。
というわけで、エラーログSQL実行時はquery()にもう一つパラメータを渡して、回避します。具体的には、
function _loggingSqlError($queries){
$sql = "insert ・・・・"; // SQL文は省略
$sqlForError = true;
$this->query($sql,$sqlForError);
}
こんな感じで、query()実行時に、エラーログSQLですよフラグ($sqlForError = true)を渡します。で、query()関数の中身のここ、
$params = func_get_args();
・・・・(中略)・・・・・
// エラーログ用SQLで無い場合
if(!(isset($params[1]) && $params[1] === true)){
$this->loggingSqlError($db);
}
・・・・(以下略)・・・・・
はい、この$params[1]に渡るわけですね。普段のquery()はsql文しか渡さないので、$params[1]はセットされていません。で、このエラーログSQLの失敗時は、$params[1]===trueなため、loggingSqlErrorが呼び出されないということになります。
終わりに
cakephpを使っていると、なかなかSQLを意識することがない(意識しないようにコーディングする)ので、いざ調べてみると、どこで動いているのかすぐには分かりませんでした。しかし、参考サイトを見たり実際に動作をみたりして、いろいろと勉強になりました。
また、もっといい方法あるよーとか、元々cakephpにそんな機能有るよー(これが一番へこみますがw)とかあれば、ご連絡いただければと思います。
(追記:2010/01/06)
上の書き方で、問題が2点ほどあったので修正しました。
まず1点目ですが、最新のSQLを取得するforeachですが、その判定条件がエラー文での判定だったので、同じエラー文がある場合に最新でないSQLも通してしまっていました。そこで、配列の順番を最新順に変更(arsort)してからforeachで回すようにしました。
2点目は、DEBUGモードが1 or 0 の場合に、ログが取れていないことが判明しました。調べてみると、$dbに値をセットするlogQueryメソッド(場所はこちら:/cake/libs/model/datasources/dbo_source.php)が実行されてない!・・・ということで、無理やり実行させるように以下のように修正しました。
function execute($sql, $options = array()) {
(・・・中略・・・)
// papettoTV modified 2010/01/06
// debug mode が 0 or 1でもlogQueryを呼び出す
// if ($options['log']) {
$this->logQuery($sql);
// }
(・・・中略・・・)
}
結局コアなファイルを強引に修正する結果になったのが残念。。。。wこういうのをうまくコンポーネント化したいですね。。。。まだまだ勉強不足でできないですが、できるよう精進していきます><
|
タグ: cakePHP
cakephpで、SQLエラーをDB保存&アラートメールする方法