CodeIgniterの学習 43 - ログ出力をZend_Logに置き換える

今日はCodeIgniterのログ出力をZend FrameworkのZend_Logに置き換える作業をする。

興味無い人には価値のない作業。
既に学習というより拡張の段階に入ってるかも。

(好みで拡張できるのがCIの良いところだと思うけど、
幕の内弁当が大好きな人には向かないかもね。一長一短だな。)

ログ機能を置き換える理由

1)興味があるから
CodeIgniterは最低限部分のみのフレームワーク、足りない機能はZend Frameworkで補強するパタンを作りたい。
ついでにZend Frameworkも使い倒せるようになりたい。


2)ログ機能を柔軟にしたいから
CodeIgniter標準のログ機能が貧弱だから。
ALERT以上のエラーはメール送信とか、ログをsyslog化するとか拡張したい。
自力で実装よりは、log4phpなりZend_Logなりを使った方がいいよね。

log4phpは以前使ったのだけど、結構ソース量(ステップ数)も多いし、部分的に修正が必要だったりして面倒だった記憶がある。

既にZend Frameworkを使えるようにしているので、
(CodeIgniterの学習 15 - Zend Framework を CodeIgniter上で使う http://d.hatena.ne.jp/dix3/20081003/1222980188
今回はZend_Logでいく。


3)ログ記録の閾値($config['log_threshold'])の指定方法が貧弱だから
CI標準だと、

0 = Disables logging, Error logging TURNED OFF
1 = Error Messages (including PHP errors)
2 = Debug Messages
3 = Informational Messages
4 = All Messages

しか選べない。

こうじゃなくて、
開発時は、

$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR','WARN','NOTICE','INFO','DEBUG');

運用時は、

$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR');

みたいに閾値を柔軟にしたい。


(似たような実装は、http://codeigniter.com/wiki/MY_Log/ でもされている。)



ソース

手順のみ書く。いつもながら無保証。

手順0:前提
(CodeIgniterの学習 15 - Zend Framework を CodeIgniter上で使う http://d.hatena.ne.jp/dix3/20081003/1222980188
とかで既にZend Framework導入済のこと。




手順1:フック部分(system/application/config/hooks.php)でのini_set( 'include_path', APPPATH . 'my_classes/' )をやめる

手順0:前提でのやり方だと、
system/application/config/hooks.phpの$hook['pre_controller']経由で、application/hooks/MyClasses.php を呼びだし、
その中で

ini_set( 'include_path', APPPATH . 'my_classes/' );

とやっているのだが、
ini_setされる順序が、CI_Logが読み込まれた後になってしまうので、CI_Logを置き換えるMY_Log側でZend_Logが読み込めない。
なので、hookのpre_controller部分はコメントアウトした。

(他のhook部分は過去の別エントリでのhookなので今回は関係ない)

system/application/config/hooks.php

<?php 
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| Hooks
| -------------------------------------------------------------------------
| This file lets you define "hooks" to extend CI without hacking the core
| files.  Please see the user guide for info:
|
|	http://codeigniter.com/user_guide/general/hooks.html
|
*/

//for my_classes
//Zend Framework、その他独自移植ライブラリ用のパスの読み込み
/*
$hook['pre_controller'][] = array(
'class' => 'MyClasses',
'function' => 'index',
'filename' => 'MyClasses.php',
'filepath' => 'hooks'
);
*/
//開発時に自動的にプロファイラを有効にする
$hook['post_controller_constructor'][] = array(
'class' => 'MyClasses',
'function' => 'enable_profiler',
'filename' => 'MyClasses.php',
'filepath' => 'hooks'
);

//HTTPレスポンスヘッダの調整
$hook['post_controller'][] = array(
'class' => 'MyClasses',
'function' => 'set_header',
'filename' => 'MyClasses.php',
'filepath' => 'hooks'
);

//for my_classes

/* End of file hooks.php */
/* Location: ./system/application/config/hooks.php */
 ?>




手順2:index.php でini_set( 'include_path', APPPATH . 'my_classes/' )する
手順1:でコメントアウトした代わりに、ドキュメントルート/index.phpでinclude_pathを追加する。

ドキュメントルート/index.php

<?php
 //上略
//独自のインクルードパスを追加するようにする
ini_set( 'include_path', APPPATH . 'my_classes/' );

/*
|---------------------------------------------------------------
| LOAD THE FRONT CONTROLLER
|---------------------------------------------------------------
|
| And away we go...
|
*/
require_once BASEPATH.'codeigniter/CodeIgniter'.EXT;

/* End of file index.php */
/* Location: ./index.php */
 ?>

てなかんじ。(index.phpの終わりに近い部分にini_setを差し込む。)
極力コアに近い部分は書き換えたくないのでhookで読み込んでいたのだが、今回は仕方がない。




手順3:application/libraries/MY_Log.php を新設する
system/libraries/Log.phpの代わりに、application/libraries/MY_Log.phpを使う事が出来る。

とりあえずZend_Logへ置き換えただけなので中身の解説はしません。
後でもっと細かく制御できるように改良するつもり。

(Zend_Logの使い方は、http://framework.zend.com/manual/ja/zend.log.html

application/libraries/MY_Log.php

<?php 
if ( ! defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );
// ------------------------------------------------------------------------
/**
 * Logging Class
 */
class MY_Log extends CI_Log {
  var $log_path;
  /**
   * CI標準のロガーの区分けと機能が貧弱なので Zend_Logを使って拡張する
   *  http://framework.zend.com/manual/ja/zend.log.html
   * EMERG  = 0 ; // 緊急事態 (Emergency): システムが使用不可能です
   * ALERT  = 1 ; // 警報 (Alert): 至急対応が必要です
   * CRIT   = 2 ; // 危機 (Critical): 危機的な状況です
   * ERR    = 3 ; // エラー (Error): エラーが発生しました (CI標準の'ERROR'も同義として扱う)
   * WARN   = 4 ; // 警告 (Warning): 警告が発生しました
   * NOTICE = 5 ; // 注意 (Notice): 通常動作ですが、注意すべき状況です
   * INFO   = 6 ; // 情報 (Informational): 情報メッセージ (CI標準の'INFO'も同義として扱う)
   * DEBUG  = 7 ; // デバッグ (Debug): デバッグメッセージ (CI標準の'DEBUG'も同義として扱う)
   * (CI標準のALLは、上記の全組み合わせ、またlog_message('level', 'message')の引数levelがALLで呼び出された時は、INFO扱いで常に記録する)
   */
  var $_threshold = array( 'EMERG', 'ALERT', 'CRIT', 'ERR', 'ERROR', 'WARN', 'NOTICE', 'INFO', 'DEBUG');
  //var $_date_fmt = 'Y-m-d H:i:s';
  var $_enabled = TRUE;
  var $logger;
  /**
   * Constructor
   *
   * @access public
   */
  function MY_Log()
  {
    $config = &get_config();
    $this -> log_path = ( $config['log_path'] != '' ) ? $config['log_path'] : BASEPATH . 'logs/';

    if ( ! is_dir( $this -> log_path ) OR ! is_really_writable( $this -> log_path ) ) {
      $this -> _enabled = FALSE;
    }

    //if ( $config['log_date_format'] != '' ) {
      //$this -> _date_fmt = $config['log_date_format'];
    //}

    if ( $config['log_threshold'] != '' ) {
      $this -> _threshold = $config['log_threshold'];
    }

    if ( $this -> _enabled ) {
      // Zend_Logを使うようにする
      require_once( 'Zend/Log.php' );
      require_once( 'Zend/Log/Writer/Stream.php' );
      // TODO:ログレベルによってログファイルを分けられるにする
      $filepath = $this -> log_path . 'log-' . date( 'Y-m-d' ) . EXT;
      $writer = new Zend_Log_Writer_Stream( $filepath );
      $this -> logger = new Zend_Log( $writer );
    }
  }
  // --------------------------------------------------------------------
  /**
   * Write Log File
   *
   * Generally this function will be called using the global log_message() function
   *
   * @access public
   * @param string $ the error level
   * @param string $ the error message
   * @param bool $ whether the error is a native PHP error
   * @return bool
   */
  function write_log( $level = 'err', $msg, $php_error = FALSE )//$php_errorは使われていないけど引数はそのままにしておく
  {

    if ( $this -> _enabled === FALSE ) {
      return FALSE;
    }
    $level = strtoupper( $level );
    if ( 'ERROR' === $level ) {//ERRORはERRと同義
      $level = 'ERR';
    }

    //ALL以外は、$config['log_threshold']を見てログを記録するか判定
    if ( 'ALL' !== $level && !in_array($level , $this -> _threshold) ) {
      return FALSE;
    }
    if ( 'ALL' === $level ) {//ALLはINFOとして常に記録する
      $level = 'INFO';
    }
    $this -> logger -> log( $msg, constant("Zend_Log::$level") );
    return TRUE;
  }
}
// END Log Class

/* End of file MY_Log.php */
/* Location: ./application/libraries/MY_Log.php */
 ?>




手順4:system/application/config/config.phpの$config['log_threshold']を変更する

$config['log_threshold'] = 4; 

とかしか書けないところを、

$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR','WARN','NOTICE','INFO','DEBUG');

とか、

$config['log_threshold'] = array('EMERG','ALERT','CRIT');

とか書けるようにした。


log_thresholdの書き方が変わっても既存ソースの互換性を考えて、

log_message('error','えらーだよ'); → log_message('ERR','えらーだよ');
log_message('info','いんふぉだよ'); → log_message('INFO','いんふぉだよ');
log_message('debug','でばっぐだよ'); → log_message('DEBUG','でばっぐだよ');
log_message('all','おーるだよ'); → log_message('ALL','おーるだよ');

として扱えるように、手順3のソース内で吸収しているつもり。


ちなみにlog_message('all','おーるだよ');の場合は、
log_thresholdの設定に関わらず、INFO扱いでログを吐くようにしている。
($config['log_threshold'] =''の時を除く、$config['log_threshold'] =array();の時は吐かれる。)



system/application/config/config.php

<?php
 //上略
/*
|--------------------------------------------------------------------------
| Error Logging Threshold
|--------------------------------------------------------------------------
|
| If you have enabled error logging, you can set an error threshold to
| determine what gets logged. Threshold options are:
| You can enable error logging by setting a threshold over zero. The
| threshold determines what gets logged. Threshold options are:
|
|	0 = Disables logging, Error logging TURNED OFF
|	1 = Error Messages (including PHP errors)
|	2 = Debug Messages
|	3 = Informational Messages
|	4 = All Messages
|
| For a live site you'll usually only enable Errors (1) to be logged otherwise
| your log files will fill up very fast.
|
*/
//$config['log_threshold'] = 4;
/**
 * CI標準のロガーの区分けと機能が貧弱なので Zend_Logを使って拡張する
 *  http://framework.zend.com/manual/ja/zend.log.html
 * EMERG  = 0 ; // 緊急事態 (Emergency): システムが使用不可能です
 * ALERT  = 1 ; // 警報 (Alert): 至急対応が必要です
 * CRIT   = 2 ; // 危機 (Critical): 危機的な状況です
 * ERR    = 3 ; // エラー (Error): エラーが発生しました (CI標準の'ERROR'も同義として扱う)
 * WARN   = 4 ; // 警告 (Warning): 警告が発生しました
 * NOTICE = 5 ; // 注意 (Notice): 通常動作ですが、注意すべき状況です
 * INFO   = 6 ; // 情報 (Informational): 情報メッセージ (CI標準の'INFO'も同義として扱う)
 * DEBUG  = 7 ; // デバッグ (Debug): デバッグメッセージ (CI標準の'DEBUG'も同義として扱う)
 * (CI標準のALLは、上記の全組み合わせ、またlog_message('level', 'message')の引数levelがALLで呼び出された時は、INFO扱いで常に記録する)
 */
$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR','WARN','NOTICE','INFO','DEBUG');//数値指定ではなく、出したいログの種類を指定する(数値では指定しない事)
//下略
 ?>


動作確認

適当なコントローラとかで、

log_message('emerg','emergだよ');
log_message('alert','alertだよ');
log_message('crit','critだよ');
log_message('err','errだよ1');
log_message('error','errだよ2');
log_message('warn','warnだよ');
log_message('notice','noticeだよ');
log_message('info','infoだよ');
log_message('debug','debugだよ');
log_message('all','allだよ');

を貼って、ログを吐いてみる。
なおログファイルは、今のところはCI標準のログファイルと同じ、system/logs/log-yyyy-mm-dd.php に保存している。

1)$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR','WARN');の時

こんなかんじ。


2)$config['log_threshold'] = array('EMERG','ALERT','CRIT','ERR','WARN','NOTICE','INFO','DEBUG');の時

こんなかんじ。

差し替えてもパフォーマンスへの影響は誤差の範囲位に感じる。


今日はここまで。今後さらに拡張していきたい。