James君!〜Avalon Loggerのターゲット(基本)

StreamTargetFactory

まあ、ここでは実際にJames上から使えないターゲットについては省略だ。ConsoleTargetFactory, LF5TargetFactory, ServletTargetFactory に関心のある奴は自分で調べろ。

Log4j の ConsoleAppender に近いのはこの StreamTargetFactory だ。これは標準出力と標準エラー出力のどっちかを選択できるので、

<stream id="foo">
  <stream>System.out</stream>
  <!-- か
  <stream>System.err</stream>
  -->
  <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
</stream>

のようにしてやればいい。

FileTargetFactory

ファイルに出力するおそらく一番使いでのあるターゲット。どっちか言うと Log4j の DailyRollingFileAppender と同様な日付ベースのログローテーションもすれば、サイズベースのローテーションもする。まあ、だからこれは利用ケースごとにまとめた方が判りやすいだろう。

まず、ローテーションせず単にファイルに追加モードで出力するだけならば、

<file id="echoservice">
   <filename>${app.home}/logs/echoservice</filename>
   <format></format>
   <append>true</append>
</file>

だけでいい。これだと、apps/packagename/logs/echoservice に単純にログが追加されていく。言うまでもなく filename タグはファイルを示し(${app.home} で アプリケーション・パスを拾う)、append タグは追加(true)起動のたびに削除(false)を示す。

で厄介なのはローテーション指定である。基本的に、

<file id="echoservice">
   <filename>${app.home}/logs/echoservice</filename>
   <format></format>
   <rotation type="revolving" init="開始番号" max="バックアップ最大数" >
   または
   <rotation type="unique" pattern="日時表記" suffix=".log" >
      ローテーション条件
   </rotation>
   <append>true</append>
</file>

のような書式を取る。問題はこの rotation タグが2つの戦略を取ることである。1つは、「unique戦略」で、ファイル名を変更せずに、「日時表記込みのファイル名」を使うものである。要するに Tomcat 風のログの取り方である。アプリの起動した日時の付いたログファイルを作成し、ローテーション条件が成立すると、「その時点での日時をベースにした新しいログファイル」を作る...というものだ。だから pattern 属性で日時表記をし、suffix 属性を指定すると、

[filenameで指定されたベース名][pattern属性][suffix属性]

を連結したファイル名を作り上げる。だから、

<file id="echoservice">
  <filename>${app.home}/logs/echoservice</filename>
   <format></format>
  <append>true</append>
  <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
      ローテーション条件
  </rotation>
</file>

とすれば、仮に ${app.home} == /usr/local/phoenix/apps/echo とすれば、

/usr/local/phoenix/apps/echo/logs/echoservice-2005-12-22-20-24.log

というファイル名になるわけだ。

もう1つの戦略は「revolving戦略」である。これは「ログファイルをローテートする」タイプのものだが、本当の意味では「ローテート」しない。要するに「ファイル名.00001」というようなファイル名で、あらかじめ確保し、それをリングバッファ状に出力ファイルを切替えていくだけである。だから、

   <rotation type="revolving" init="0" max="5" >

のような指定は、「ファイル名.000000」から始めて、ローテーション条件が成立したら次は「ファイル名.000001」に出力し...というようにやっていって、max="5" なので、「ファイル名.000004」の次は「ファイル名.000000」に戻る、というものである。あまり使えない戦略だな、これ。さらに残念なことに、アプリを再起動すると、「どれが最新のログファイルであるか」忘れてしまう....おいおい。なので、筆者の感覚だと「unique戦略」だけ使った方がいいと思うぞ。

じゃあ、次は具体的な「ローテーション条件」の方の話だ。要するに、

  1. 日付でローテーションする→ <date>
  2. 特定の時間になったらローテーションする→ <time>
  3. ファイルサイズでローテーションする→ <size>

の3通りのローテーション条件が用意されている。で、これらの OR ができる。だから、

  <rotation .....>
     <条件タグ ....>
     <!-- か -->
     <or> 
        <条件タグ ....>
        <条件タグ ....>
        .....
     </or>
  </rotation>

という格好になる。要するに <or> は複数の条件を中に入れるためのコンテナタグに過ぎない。これらの条件タグは属性とかまったくなしで、タグのコンテンツとして値を指定してやればいい。だから、

  <!-- サイズ 10000 byte 以上。単位として「k」(=キロバイト)が使える -->
  <size>10000</size>  
  <!-- 時間が 24:00:00 -->
  <time>24:00:00</time>
  <!-- 日付ローテーションは記号で指定する。pattern 属性と同様なものだ -->
  <!-- 分単位のローテーション -->
  <date>mm</date>

というような感じで指定する。全体を合わせた例は、たとえば James のデフォルトのように、

<file id="connections-target">
   <filename>${app.home}/logs/connections</filename>
   <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
   <append>true</append>
   <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
     <or>
       <!-- 日付が変わるとローテート -->
       <date>dd</date>
       <!-- もしくはサイズが 10485760byte 以上になるとローテート -->
       <size>10485760</size>
     </or>
   </rotation>
</file>

なるわけだ。ちなみに省略して意味のあるオプションは append=false だけだ。rotation タグがないとそもそもローテーションしない。

PriorityFileterTargetFactory

例えば、同じログを、標準出力とファイルの両方に出したいが、標準出力はすべて、ファイルは WARN 以上だけ出したい....なんてことないかな? まあ、実際にはこうすると両方に出るのだが...

<server>
  <logs version="1.1">
    <factories>
      <factory type="file" 
        class="org.apache.avalon.excalibur.logger.factory.FileTargetFactory"/>
      <factory type="console" 
        class="org.apache.avalon.excalibur.logger.factory.StreamTargetFactory"/>
    </factories>

    <categories>
      <category name="EchoService" log-level="DEBUG">
        <!-- 2つダブって指定 -->
        <log-target id-ref="echoservice"/>
        <log-target id-ref="echoservice-console" />
      </category>

    <targets>
      <!-- ファイル出力 -->
      <file id="echoservice">
        <filename>${app.home}/logs/echoservice</filename>
        <!-- 中身は略 -->
      </file>

      <!-- Stream 出力 -->
      <console id="echoservice-console">
        <stream>System.out</stream>
      </console>
    </targets>
  </logs>
</server>

残念なことに、両方のログレベルはまったく同じになってしまい、「ファイルだけ WARN 以上」ということはできない。なぜって..そりゃログレベル指定が category タグの属性だもんな。

....というようなことを解決するアイデアは、要するに「実際に出力処理をするハンドラに閾値を与える」ということに尽きる。なので、たとえば Log4j だと、すべての Appender に共通する属性として、「Threshold オプション」がある。が、Avalon では「入れ子に出来るターゲットを作って...」という面倒なだけの解決法をとったようだ。だから、これはこうすると、ファイル出力だけは WARN 以上でないと出なくなる。

<server>
  <logs version="1.1">
    <!-- see http://jakarta.apache.org/avalon/excalibur/logger/index.html -->
    <factories>
      <factory type="file" 
        class="org.apache.avalon.excalibur.logger.factory.FileTargetFactory"/>
      <factory type="console" 
        class="org.apache.avalon.excalibur.logger.factory.StreamTargetFactory"/>
      <factory type="priority" 
        class="org.apache.avalon.excalibur.logger.factory.PriorityFilterTargetFactory"/>
   </factories>
   ......

    <targets>
      <priority id="echoservice" log-level="WARN">
        <file>
           <filename>${app.home}/logs/default</filename>
           <!-- 中身は略 -->
        </file>
      </priority>
      <console id="echoservice-console">
        <stream>System.out</stream>
      </console>
    </targets>
  </logs>
</server>

あまりスマートなやり口とは思えないが、まあ、こういうやり方しかないかな? ちなみに、<priority> タグの中に書ける子ターゲットの数には制限はない。いくつ書いても結構だ。log-level 属性のデフォルトは INFO である。

AsyncTargetFactory

Log4j でも AsyncAppender があるが、要するにログ出力の最適化を狙った「非同期出力」である。つまり、ログ出力を別スレッドとして動かして、適当にログが貯まったらフラッシュする、というロジックで動作させる。なので「どんなログを出させるか?」はやはり先ほどの PriorityTargetFactory と同じで、子持ちタグにする。こんな具合だ。

<server>
  <logs version="1.1">
    <factories>
      <factory type="console" 
        class="org.apache.avalon.excalibur.logger.factory.StreamTargetFactory"/>
      <factory type="async" 
        class="org.apache.avalon.excalibur.logger.factory.AsyncLogTargetFactory"/>
   </factories>
   .....
    <targets>
      <async id="echoservice-console" queue-size="1000" priority="MIN" >
         <console>
            <stream>System.out</stream>
         </console>
      </async>
   ....

で、オプションの queue-size は一旦ログを貯めておくバッファのサイズ、priority は別にログ専用で走らせるスレッドのプライオリティである。queue-size のデフォルトは -1(使わない)で、priority のデフォルトは MIN である(MIN|NORM|数値が取れる。要するにスレッドの優先度である)。

DatagramTargetFactory

さてここからはネットワーク系である。まずは UDP でパケットを投げる DatagramTargetFactory である。まあ、Log4j でも 1.3 で UDPAppender という同様の仕様のものが追加されるはずである。まあ、こんなん仕様を見た方が早い。

   <udp id="echoservice-aux">
     <address hostname="localhost" port="4444" />
     <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
   </udp>

なので、address タグの hostname 属性にサーバが立っているホスト名を、UDP の接続を受け付けるポートを port 属性で指定すればいい。単に format 属性で指定されたフォーマットで整形されたテキストを内容とするパケットが飛ぶだけである。

SocketTargetFactory

Log4j の SocketAppender とほぼ同様に、ログ・オブジェクト(org.apache.log.LogEvent)を TCP ソケットで流す。ということは少しサーバが重装備だ。こんな感じのサーバを書けば、このターゲットが流す内容を表示できる(かなり手抜き...済まぬ。とはいえ LogEvent の中身が判ってよかろう)。

import java.net.*;
import java.io.*;
import java.util.Date;
import org.apache.log.LogEvent;
import org.apache.log.ContextMap;

public class SocketLogServ {
    public static void main( String [] argv ) throws Exception {
        ServerSocket ss = new ServerSocket( 4444 );
        while( true ) {
            Socket sock = ss.accept();
            InputStream is = sock.getInputStream();
            ObjectInputStream ois = new ObjectInputStream( is );

            InetAddress addr = sock.getInetAddress();
            System.out.println( "from: " + addr.toString() );

            Object log;
            while( null != (log = ois.readObject() ) ) {
                LogEvent le = (LogEvent)log;
                System.out.println( "Priority: " + le.getPriority().getName() );
                System.out.println( "Catagory: " + le.getCategory() );
                long t = le.getTime();
                System.out.println( "Time:" + new Date(t).toString() );
                System.out.println( "RelativeTime: " + le.getRelativeTime() );
                System.out.println( "Message: " + le.getMessage() );
                Throwable e = le.getThrowable();
                if( e != null ) {
                    System.out.println( "Throwable: " + e.getMessage() );
                }
                /* 残念だがキーを列挙できない! あと、phoenix だとホントに
                   値をセットするんだろうか???? */
                ContextMap map = le.getContextMap();
                printContextMap( map, "pricipal" );
                printContextMap( map, "ipaddress" );
                printContextMap( map, "username" );
                System.out.println();
                System.out.flush();
            }
        }
    }

    private static void printContextMap( ContextMap map, String key ) {
        if( map == null ) return;
        Object val = map.get( key );
        if( val == null ) return;
        System.out.println( "Context " + key + ": " + val.toString() );
    }
}

まあ、コンパイル&実行には当然 logkit-*.jar が必要だ。クラスパスに忘れずに含めておいてくれ。で、environment.xml の方の設定は、

  <socket id="echoservice-aux">
    <address hostname="localhost" port="4444" />
  </socket>

こいつは LogEvent オブジェクトをそのまま流すので、<format> 指定なぞ不要だ。

さて、残りは3つ(JMSTargetFactory,JDBCTargetFactory,SMTPTargetFactory)だが、ややこしいのばっかり残ってる。ページを改める。



copyright by K.Sugiura, 1996-2006