cakephpで、SQLエラーをDB保存&アラートメールする方法cakephpで、SQLエラーをDB保存&アラートメールする方法

2010/01/06

予期しないSQLエラーが起きたとき、すぐに検知&調査をしたいものです。そこで、検知方法としてメール送信、調査方法としてエラー情報をDBに保存する方法をcakephpで実装してみました。
cakephpには標準でエラーのログ機能はあるようですが、さらっと見た限りテキストへのログである点と、それだけでは調査する際に充分な情報ではないと思ったため、今回はこの方法で実装しました。
ポイントは以下、

  1. SQLエラー時に起動する
  2. そのSQL文とその他関連情報をDBに保存&アラートメール

SQLエラー時に起動する

まず、エラー時に起動させる共通処理を探します。
cakephpでは、主に2種類のSQL処理があるようで、

  1. find()、read()、save()といった、ほぼ自動的にSQL文が生成される場合
  2. 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こういうのをうまくコンポーネント化したいですね。。。。まだまだ勉強不足でできないですが、できるよう精進していきます><

タグ:

関連があるかもしれないエントリー

コメントをどうぞ