James君!〜Avalon Loggerの代わりにLog4j を使う!

conf/kernel.xml

とはいえ、James が使いづらい Avalon Logger を使う...というのは何とかしたいな。やっぱねえ、Log4j が使えたらイイよね! というわけで、いろいろ調べたが、簡単なやり方は残念ながらない。Phoenix のローダが Avalon Logger を使うように出来ており、その代わりに Log4j を使えるようになっているわけではない。

それじゃあ、あんまりだ....というわけで、チョイとハックした。これで Phoenix のデフォルト・ロガーを Log4j のものに差し替えてしまう、という荒技だ。だから今まで通り、Mailet の上から、getLogger() して info() とか呼べば、log4j に出力を任せることが出来ちゃう....というものである。まあ、これを解説するのは、今まで触れなかった Phoenix 自体の説明にもなるからなんだが、はっきり言ってハックである。

実際、Phoenix 自体の設定ファイルは james-2.2.0/conf/kernel.xml であり、これが「Phoenix カーネル」の構成を指定することになる。「Phoenix の上で動くアプリの構成」が assembly.xml だったのと同じように、Phoenix カーネル自体の構成もこういった XML ファイルで指定するわけである。意外に凄いことに、これらの「Phoenix カーネルの構成要素」も Avalon コンポーネントだったりするのである。だから、たとえば「使うクラスローダー」なんかもここで指定するものだが、はやりロガーの設定もあるんである。これを見てみると、

<?xml version="1.0"?>
<phoenix>
  <embeddor role="org.apache.avalon.phoenix.interfaces.Embeddor"
     class="org.apache.avalon.phoenix.components.embeddor.DefaultEmbeddor">

    <component role="org.apache.avalon.phoenix.interfaces.Deployer"
        class="org.apache.avalon.phoenix.components.deployer.DefaultDeployer"
        logger="deployer"/>
	    
    <component role="org.apache.avalon.phoenix.interfaces.LogManager"
        class="org.apache.avalon.phoenix.components.logger.DefaultLogManager"
        logger="logs"/>

となって、何となく、

    <component role="org.apache.avalon.phoenix.interfaces.LogManager"
       class="jp.or.nurs.sug.manager.Log4jLogManager"
       logger="logs"/>

とでもすれば出来るのでは?という風に思えちゃうのである。が、残念ながら、

org.apache.avalon.phoenix.components.logger.Log4jLogManager

などという洒落たものはないので、これを自前で用意するんである。

phoenix のロガーの代理クラス Log4jLogManager

中身は当然 DefaultLogManager をカンニングして書く。こんなところだ。要するに org.apache.avalon.phoenix.interface.LogManger を implements したクラスなら、何でも DefaultLogManager の代わりに使えちゃう、ということである。

package jp.or.nurs.sug.manager;

import org.apache.avalon.excalibur.logger.LoggerManager;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.container.ContainerUtil;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Logger;

import org.apache.avalon.phoenix.BlockContext;
import org.apache.avalon.phoenix.interfaces.LogManager;
import org.apache.avalon.phoenix.metadata.SarMetaData;

public class Log4jLogManager
    extends AbstractLogEnabled
    implements LogManager
{

    public Logger createHierarchy( final SarMetaData metaData,
                                   final Configuration logs,
                                   final ClassLoader classLoader )
        throws Exception
    {
        final String sarName = metaData.getName();

	/* DefaultContext のセット */
        final DefaultContext context = new DefaultContext();
        context.put( BlockContext.APP_NAME, sarName );
        context.put( BlockContext.APP_HOME_DIR, metaData.getHomeDirectory() );
        context.put( "classloader", classLoader );
	/* もし、session-context が必要なら、ここで定義すべき 
	 see SMTPTargetFactory */

	/* 実際にターゲットを生成する Manager を作って返す */
        final LoggerManager loggerManager = new Log4jLoggerManager();
	/* こいつも Avalon の規約に沿っている  */
        ContainerUtil.enableLogging( loggerManager, getLogger() );
        ContainerUtil.contextualize( loggerManager, context );
        ContainerUtil.configure( loggerManager, logs );
	/* Log4j 用の LoggingManager を返す */
        return loggerManager.getDefaultLogger();
    }
}

としてみると、SMTPTargetFactoryのところで謎だった、例の「DefaultContext の謎」が解決だ。要するに、ここで生成された DefaultContext のインスタンスが、TargetFactory の m_context として渡されてくるわけである。まあ、結論としては「わざわざ LogManager を書いてやらないと session-context を拾えない!」ということなので、SMTPTargetFactory を使いたいだけなら、あそこで紹介したやり方でやるべきだな。

Avalon-excalibur のロガーの代理 Log4jLoggerManager

で、実際の Log4j 用のManager は org.apache.avalon.excalibur.logger.Log4JLoggerManager というクラスがあるので、「これが使えるのか?」と試してみたが、どうも古い Log4j の構成を想定しているようで、うまく動かない。だから、こいつも自分で書くと、こんな定義になる。クラスパスの認識ができない Phoenix だから、設定ファイルはどっかで指定してやらないと、いろいろヤヤコシイので、environment.xml をこんな感じのものにしてやればいいんではなかろうか。そこらへんの仕様も入れておこう。

<?xml version="1.0"?>

<server>
  <logs version="1.1">
     <log4jConfig>conf/log4j.xml</log4jConfig>
     <!-- かあるいは
     <log4jConfig>conf/log4j.properties</log4jConfig>
     -->
  </logs>
</server>

実際には、environment.xml で指定する設定ファイルについて、ベースとなるディレクトリは james-2.2.0/apps/james になるので、設定ファイルの置き場所は

james-2.2.0/apps/james/conf/log4j.xml(.properties)

としておこう。これもやはり、org.apache.avalon.excalibur.logger.LoggerManager を implements して、常識的な Avalon コンポーネントとして定義されているものならばどうでもOKだ。

package jp.or.nurs.sug.manager;

import org.apache.avalon.excalibur.logger.LoggerManager;

import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;

import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Log4JLogger;
import org.apache.avalon.framework.logger.Logger;

import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;

import java.io.File;

public class Log4jLoggerManager
    extends AbstractLogEnabled
    implements LoggerManager, Contextualizable, Configurable
{
    private org.apache.log4j.Logger rootLogger;
    private static Logger m_logger;

    String m_baseDirectory;

    public void contextualize( final Context context )
        throws ContextException
    {
        /* Log4jLogManager でセットした context の値をここで使う */
        m_baseDirectory = ((File)context.get( "app.home" )).toString();
    }

   public void configure( final Configuration configuration )
        throws ConfigurationException
    {
	/* environment.xml の、設定内容が configuration で渡る */
	/* たとえば、
	   <server>
             <log>
                <log4jConfig>設定ファイルのパス</log4jConfig>
	     </log>
           </server>
	   というような構造を前提にしよう。*/

	try {
	    Configuration log4jconf = configuration.getChild( "log4jConfig" );
	    String conffile = log4jconf.getValue();
	    if( conffile.endsWith( ".xml" ) ) {
		/* .xml で設定ファイルが終れば XML 形式 */
		DOMConfigurator.configure( m_baseDirectory + 
					   "/" + conffile );	
	    } else {
		/* そうでなければ Property 形式 */
		PropertyConfigurator.configure( m_baseDirectory + 
						"/" + conffile );	
	    }
	} catch( Exception e ) {
	    throw new ConfigurationException( "cannot configure Log4j", e );
	}
	/* Log4j のルートロガーを生成する */
	rootLogger = org.apache.log4j.Logger.getLogger( "" );
	/* それを使って、avalon-framework のロガーを初期化する */
	m_logger  = new Log4JLogger( rootLogger );
    }

    /* カテゴリーに応じたロガーを返すのは、単に Log4j に任せればよい */
    public org.apache.avalon.framework.logger.Logger
        getLoggerForCategory( final String categoryName )
    {
        return m_logger.getChildLogger( categoryName );
    }

    public org.apache.avalon.framework.logger.Logger getDefaultLogger()
    {
        return m_logger;
    }
}

まあ、ここで使っている avalon-framework の Log4jLogger(org.apache.avalon.framework.logger.Log4jLogger) は、本当に Log4j のロガーのラッパーに過ぎないものなので、これで十分だ。あとは avalon-framework の Log4jLogger に任せればいい。

build.xml とインストール

で、この2つのファイルをコンパイルして、適当なファイル名で固める。build.xml はこんなところだ。

<project name="phoenix" default="compile" basedir=".">
   <property name="debug.flag" value="yes" />

   <property name="phoenix.dir" 
        value="適当なPhoenixベースシステムのディレクトリ" />
   <property name="phoenix.lib.dir"
      value="${phoenix.dir}/lib" />
   <property name="lib.dir" value="./lib" />
   <property name="src" value="./src" />
   <property name="classes" value="./classes" />

  <!-- 使うライブラリはこれだけ -->
  <path id="lib.classpath">
      <pathelement path="${classes}" />
      <pathelement path="${phoenix.lib.dir}/avalon-framework-4.1.3.jar" />
      <pathelement path="${phoenix.lib.dir}/excalibur-logger-1.0.jar" />
      <pathelement path="${phoenix.lib.dir}/phoenix-client.jar" />
      <pathelement path="${phoenix.dir}/bin/lib/phoenix-engine.jar" />
      <pathelement path="${lib.dir}/log4j-1.2.8.jar" />
  </path>

  <target name="compile" >
     <javac srcdir="${src}" destdir="${classes}" debug="${debug.flag}" 
        classpathref="lib.classpath"/>
  </target>

  <target name="package" depends="compile" >
      <jar jarfile="./suglog4j.jar" basedir="${classes}" />
  </target>

  <target name="clean">
     <delete>
         <fileset dir="${classes}" includes="**/*.class" />
     </delete>
  </target>
</project>

言うまでもなく、コンパイルには log4j-*.jar が必要なので、lib/ に入れておいてくれたまえ。でコンパイルすれば suglog4j.jar が出来上がる。たとえば James に Log4j を使わせるのならば、

  1. 出来た suglog4j.jar を james-2.2.0/bin/lib/ に放りこむ。
  2. james-2.2.0/conf/kernel.xml の、
      <component role="org.apache.avalon.phoenix.interfaces.LogManager"
          class="org.apache.avalon.phoenix.components.logger.DefaultLogManager"
          logger="logs"/>
    
    を、
      <component role="org.apache.avalon.phoenix.interfaces.LogManager"
          class="jp.or.nurs.sug.manager.Log4jLogManager"
          logger="logs"/>
    
    に修正する。
  3. james-2.2.0/apps/james/SAR-INF/environment.xml を次の内容にする。
    <?xml version="1.0"?>
    
    <server>
      <logs version="1.1">
         <log4jConfig>conf/log4j.properties</log4jConfig>
      </logs>
    </server>
    
  4. james-2.2.0/apps/james/conf/log4j.properties を作成する。まあ、テストなら、こんなところで十分だろう。
    ### direct log messages to stdout ###
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.Target=System.out
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d %5p %c - %m%n
    
    log4j.rootLogger=info, stdout
    
  5. 忘れずに、log4j-*.jar を、james-2.2.0/lib に放りこんでおく。
  6. で、James を起動する....と、

2006-01-12 14:47:11,469 INFO .dnsserver - Autodiscovery is enabled - trying to discover your system's DNS Servers
2006-01-12 14:47:11,482 INFO .dnsserver - No DNS servers have been specified or found by autodiscovery - adding 127.0.0.1
2006-01-12 14:47:11,484 INFO .dnsserver - DNS Server is: 127.0.0.1
2006-01-12 14:47:11,729 INFO .objectstorage - Registering Repository org.apache.james.mailrepository.filepair.File_Persistent_Object_Repository
2006-01-12 14:47:11,731 INFO .objectstorage - for file,OBJECT,SYNCHRONOUS
2006-01-12 14:47:11,731 INFO .objectstorage - for file,OBJECT,ASYNCHRONOUS
2006-01-12 14:47:11,732 INFO .objectstorage - for file,OBJECT,CACHE
2006-01-12 14:47:11,733 INFO .objectstorage - Registering Repository org.apache.james.mailrepository.filepair.File_Persistent_Stream_Repository
2006-01-12 14:47:11,734 INFO .objectstorage - for file,STREAM,SYNCHRONOUS
2006-01-12 14:47:11,735 INFO .objectstorage - for file,STREAM,ASYNCHRONOUS
2006-01-12 14:47:11,736 INFO .objectstorage - for file,STREAM,CACHE
2006-01-12 14:47:11,833 INFO .mailstore - JamesMailStore init...
2006-01-12 14:47:11,835 INFO .mailstore - Registering Repository instance of class org.apache.james.mailrepository.AvalonMailRepository to handle file protocol requests for repositories of type MAIL
2006-01-12 14:47:11,836 INFO .mailstore - Registering Repository instance of class org.apache.james.mailrepository.AvalonSpoolRepository to handle file protocol requests for repositories of type SPOOL

という具合に、James のINFO以上のすべてのログがずらずらとコンソールに出ることになる。勿論これは Avalon Logkit の代わりなので、今までログが出ていた james-2.2.0/apps/james/logs 以下へのファイル保存はまったく無視である。

environment.xml 相当の log4j.properties を書く

まあ、少し実用的に次の仕様でログファイルを出すように log4j.properties を書いてみようか。

  1. 出力ディレクトリは /var/log/james
  2. カテゴリーごとに、ログファイルを分ける。
  3. ログローテーションは考えず、James を起動するごとに新しいログファイルを使う。

とすると、こんな感じか。ログファイルのベースディレクトリになるのはどうやら「Jamesを起動したディレクトリ」ということになるので、フルパス指定が要りそうだ。

# Appender 定義
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %5p %c - %m%n

### direct messages to file james.log ###
log4j.appender.filejames=org.apache.log4j.FileAppender
log4j.appender.filejames.File=/var/log/james/james.log
log4j.appender.filejames.Append=false
log4j.appender.filejames.layout=org.apache.log4j.PatternLayout
log4j.appender.filejames.layout.ConversionPattern=%d %5p %c - %m%n

### direct messages to file mailet.log ###
log4j.appender.filemailet=org.apache.log4j.FileAppender
log4j.appender.filemailet.File=/var/log/james/mailet.log
log4j.appender.filemailet.Append=false
log4j.appender.filemailet.layout=org.apache.log4j.PatternLayout
log4j.appender.filemailet.layout.ConversionPattern=%d %5p %c - %m%n

### direct messages to file spoolmanager.log ###
log4j.appender.filespool=org.apache.log4j.FileAppender
log4j.appender.filespool.File=/var/log/james/spoolmanager.log
log4j.appender.filespool.Append=false
log4j.appender.filespool.layout=org.apache.log4j.PatternLayout
log4j.appender.filespool.layout.ConversionPattern=%d %5p %c - %m%n

# というような調子で filedns, fileremote, filepop3, filesmtp
# filenntp, filenntpstore, filemailstore, fileusersstore, 
# fileobjectstore, fileconnections, filesockets, filethreadmanager
# filescheduler, filefetchpop, filefetchmail を定義する 

# カテゴリー定義
log4j.logger..James=info, filejames
log4j.logger..James.Mailet=info, filemailet
log4j.logger..spoolmanager=info, filespool
log4j.logger..dnsserver=info, filedns
log4j.logger..remotemanager=info, fileremote
log4j.logger..pop3server=info, filepop3
log4j.logger..smtpserver=info, filesmtp
log4j.logger..nntpserver=info, filenntp
log4j.logger..nntp-repository=info, filenntpstore
log4j.logger..mailstore=info, filemailstore
log4j.logger..users-store=info, fileusersstore
log4j.logger..objectstorage=info, fileobjectstore
log4j.logger..connections=info, fileconnections
log4j.logger..sockets=info, filesockets
log4j.logger..thread-manager=info, filethreadmanager
log4j.logger..scheduler=info, filescheduler
log4j.logger..fetchpop, filefetchpop
log4j.logger..fetchmail, filefetchmail

これで environment.xml でいつも書いていたのと大体同等のログファイルを出せるようになるわけだ。まあ、カテゴリーが「.カテゴリー名」になるあたり、ちょいとイカレているが、これはJames の側でそういう風にロガーを使っているんで仕方がない。あと、これは Log4j の仕様だが、「定義されているカテゴリーにないログ」だけを「default」で出す手段がないので、これも仕方がないな(Avalon だと default カテゴリーだが...)。だから、thread-manager の定義は environment.xml にはないが、ここには追加しておく。

とはいえ、「Phoenix 自体のログ」はやはり james-2.2.0/logs/phoenix.log に出てしまう。まあ、これは許してくれ。



copyright by K.Sugiura, 1996-2006