Log4J徹底解説

Log4cxxと小物ツール

目次

Log4cxx のインストール

さて、今度は Log4cxx である。Log4cxx は Log4J の C++ 版ライブラリであり、基本的に Log4J と同様に動作する。log4j.properties や log4j.xml のような設定ファイルもほとんどそのまま動作するのである。テストしたのは log4cxx-0.9.7 だが、これは多少新しめのコンパイル環境を要求する。また Java と違って一部機能は標準ライブラリにないために、コンパイル時にオプションで適当なライブラリを指定してやらなくては有効にならない。

コンパイルは次のようにする。

% ./autogen.sh
% ./configure
% make
% make check
% su
# make install

である。筆者は Fedora Core 1 でやってみたが、オプションである ODBC 機能の有効化(./configure --with-ODBC=unixODBC)と、SMTP 機能の有効化(./configure --with-SMTP=libsmtp) はライブラリがなかった。それゆえ筆者環境では ODBCAppender(JDBCのわけがない....その代理)、STMPAppender は実行できない、ということになる。

まあ、新しめの Autoconf があるような UNIX環境だと、コンパイル自体はたいして問題ではなかろう。一応次の Appender と Layout が用意されている、と README ファイルにある。

* Appenders:
  AsyncAppender, ConsoleAppender, DailyRollingFileAppender, FileAppender,
  NTEventLogAppender, ODBCAppender, RollingFileAppender, SMTPAppender,
  SocketAppender, SocketHubAappender, SyslogAppender, TelnetAppender,
  XMLSocketAppender
 
* Layouts:
  HTMLLayout, PatternLayout, SimpleLayout, TTCCLayout, XMLLayout

まあ、Log4J と比較してみれば、無いのは JDBCAppender(代りに ODBCAppenderがある)、JMSAppender(無理)、LF5Appender(これも無理)、NullAppender(意味無し)、ExternallyRolledFileAppender(出来なくもないが...) であり、なぜか XMLSocketAppender がある。これは SocketAppender の変形で、XMLLayout に従って XML 形式で出力するものである。ただし、先に触れたように、SMTPAppender と ODBCAppender は、オプションのライブラリを要求するために、コンパイル時にちゃんとフラグを立て指定してやらなければ有効にならない。

インストールされるのは、

${prefix}/bin/simplesocketserver
${prefix}/lib/log4cxx.a
${prefix}/lib/log4cxx.la
${prefix}/lib/log4cxx.so
${prefix}/lib/log4cxx.so.9
${prefix}/lib/log4cxx.so.9.0.0
${prefix}/include/log4cxx/*.h
${prefix}/man/man?/*

である。

Log4cxx のテスト

さっそくテストだ。こんなコードである。

#include <stdio.h>
#include <log4cxx/logger.h>
#include <log4cxx/basicconfigurator.h>
#include <log4cxx/propertyconfigurator.h>
#include <log4cxx/helpers/exception.h>

using namespace log4cxx;
using namespace log4cxx::helpers;

// Define a static logger variable so that it references the
// Logger instance named "MyApp".
//自前でカテゴリー名は管理する必要がある。C++ にはパッケージがないからね。&
LoggerPtr logger = Logger::getLogger(_T("RemoteLog.Client") );

int main(int argc, char **argv)
{
  int result = EXIT_SUCCESS;
  char buff[256];
  try
    {
      // プロパティファイルを指定するんならこうする。
      // String propertyFileName = _T("./log4j.properties");
      // PropertyConfigurator::configure(propertyFileName);
      while( fgets( buff, 255, stdin ) ) {
        char *p;
        buff[strlen(buff)-1] = '\0';
        for( p = buff; *p && *p != ','; p++ );
        if( *p == '\0' ) {
          logger->info(_T( buff ));
        } else {
          *p++ = '\0';
          if( strcmp( buff, "fatal" ) == 0 ) {
            logger->fatal( _T( p ) );
          } else if( strcmp( buff, "error" ) == 0 ) {
            logger->error( _T( p ) );
          } else if( strcmp( buff, "warn" ) == 0 ) {
            logger->warn( _T( p ) );
          } else if( strcmp( buff, "info" ) == 0 ) {
            logger->info( _T( p ) );
          } else if( strcmp( buff, "debug" ) == 0 ) {
            logger->debug( _T( p ) );
          } else {
            logger->error( "illegal command" );
            logger->error( buff );
          }
        }
      }
    }
  catch(Exception&)
    {
      result = EXIT_FAILURE;
    }

  return result;
}

注意すべきことは、C++ なんでパッケージとかは特にないし、それがディレクトリ階層と結びついているわけでもない。だから、Logger::getLogger() の引数に文字列を渡して自前で「カテゴリー名」を管理しなければならない。勿論、「.」で区切られた階層関係は有効なので、適切な階層関係を考えて自分で管理するのである。

コンパイルは次のようなもんである。勿論、新たに /usr/local/lib/ にライブラリを入れた時には、/etc/ld.so.conf に /usr/local/lib を追加して /sbin/ldconfig を実行すべきことは、UNIX ユーザならご承知のことと思う。

% g++ -o TestCxx TestCxx.cpp -llog4cxx

で、重要なのは、lo4j.properties は別に Java 用から別に変更しなくていいんである! 次のものを試してみよう。

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{1} - %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=test.log
log4j.appender.file.Apppend=true
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d %5p %c{1} - %m%n

log4j.rootLogger=debug, stdout, file
log4j.debug=true

調子こいて log4j.xml である。「RemoteLog」というカテゴリーを追加して、そこでファイル出力をしていることに注意。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
  <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
     <param name="Target" value="System.out" /> 
     <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d %5p %c{1} - %m%n" />
     </layout>
  </appender>
  <appender name="file" class="org.apache.log4j.FileAppender">
     <param name="File" value="mylog.log" />
     <param name="Append" value="true" />
     <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d %5p %c{1} - %m%n" />
     </layout>
  </appender>

  <category name="RemoteLog" >
    <priority value ="info" />
    <appender-ref ref="file" />
  </category>  
  <root>
    <priority value ="debug" />
    <appender-ref ref="stdout"/>
  </root>
</log4j:configuration>

さらに調子こいて、${prefix}/bin/simplesocketserver を実験してみよう。log4j.xml は次の通り。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
  <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
     <param name="Target" value="System.out" /> 
     <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d %5p %c{1} - %m%n" />
     </layout>
  </appender>
  <appender name="remote" class="org.apache.log4j.net.SocketAppender">
     <param name="RemoteHost" value="localhost" />
     <param name="Port" value="1753" />
     <param name="LocationInfo" value="true" />
     <param name="ReconnectionDelay" value="1000" />
  </appender>

  <category name="RemoteLog" >
    <priority value ="info" />
    <appender-ref ref="remote" />
  </category>  
  <root>
    <priority value ="debug" />
    <appender-ref ref="stdout"/>
  </root>
</log4j:configuration>

で、simplesocketserver を起動するディレクトリに置く log4j.properties はこんなもん。

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{1} - %m%n

log4j.rootLogger=debug, stdout

で、${prefix}/bin/simplesocketserver の起動は、Log4J の時と同じである。

% ${prefix}/bin/simplesocketserver 1753 log4j.properties
2004-09-15 03:05:52,576  INFO SimpleSocketServer - Listening on port 1753
2004-09-15 03:05:52,577  INFO SimpleSocketServer - Waiting to accept a new client.
2004-09-15 03:05:56,988  INFO SimpleSocketServer - Connected to client at bizet.kobe-du.ac.jp/127.0.0.1
2004-09-15 03:05:56,988  INFO SimpleSocketServer - Starting new socket node.
2004-09-15 03:05:56,989  INFO SimpleSocketServer - Waiting to accept a new client.
2004-09-15 03:06:04,276  INFO Client - test
Caught EOFException. Closing connection.
closing socket
Thread destroyed.

Log4cxx を使った小物ツール

Log4cxx は見てきたように、Log4J と大体同じものである。で、何でわざわざこのページで Log4cxx なんて紹介するのか、というとちゃんとワケがあるのだ。そのワケはこれだ。

% ps aux
USER       PID %CPU %MEM    VSZ  RSS TTY      STAT START   TIME COMMAND
root      9633  0.0  0.9   6268  2020 pts/2   S    12:21   0:00 ./TestCxx2
root      9634  1.3  5.1 218632 11476 pts/3   S    12:21   0:00 java TestLog4j

一目瞭然だ...java はシステムリソースを浪費しまくっているんである。まあ、これはインタプリタである Java の宿命なのだが、サーバから頻繁に起動されて、仕事が終ればすぐ消える「ちょっとした小物ツール」を実装するには、ちょっとためらわれる「宿命」なのである。

これじゃ、「小物ツール」は Java で書く気がしないな...

で、実際にやろうと思うことは、勿論大したことではない。汎用的に使うために、「標準入力からメッセージを受けて、固定のログレベルでそれを Log4cxx によってログとして流す」というものである。たとえば swatch から起動されて、ログ出力を「どこか」に転送する、といったノリで使うものだ。

#include <stdio.h>
#include <iostream>
#include <log4cxx/logger.h>
#include <log4cxx/level.h>
#include <log4cxx/basicconfigurator.h>
#include <log4cxx/xml/domconfigurator.h>
#include <log4cxx/propertyconfigurator.h>
#include <log4cxx/helpers/exception.h>

using namespace log4cxx;
using namespace log4cxx::helpers;

// Define a static logger variable so that it references the
// Logger instance named "MyApp".
LoggerPtr logger = Logger::getLogger(_T("LogTool"));

#define ONESHOT_MODE    0
#define CONTINUOUS_MODE 1

#define LEVEL_AUTO  0
#define LEVEL_FIXED 1

/* 使い方...よく読んでね */
void usage( void ) {
  tcerr << "LogTool [-f conffile] [-o | -c] [-l DEBUG|INFO|WARN|ERROR|FATAL]\n";
  tcerr << "usage: --f conffile\n";
  tcerr << "       --file conffile : set configure file like log4.properties 
                                                or log4j.xml\n";
  tcerr << "       -l DEBUG|INFO|WARN|ERROR|FATAL\n";
  tcerr << "       --level DEBUG|INFO|WARN|ERROR|FATAL\n";
  tcerr << "            : set default output level\n";
  tcerr << "       -a\n";
  tcerr << "       --autolevel : find output level from message\n";
  tcerr << "       -o\n";
  tcerr << "       --oneshot  :  end once read from stdin(default)\n";
  tcerr << "       -c\n";
  tcerr << "       --continuous : continue reading from stdin\n";
}

int main(int argc, char **argv)
{
  char *property = NULL;
  int mode = ONESHOT_MODE;
  int levelMode = LEVEL_FIXED;
  int result = EXIT_SUCCESS;

  /* デフォルトのログレベルを作っておく */
  LevelPtr level = Level::toLevel( "DEBUG" );

  /* オプション解析 */
  while( --argc > 0 ) {
    if( strcmp( *++argv, "-f" ) == 0 ){
      property = *++argv; argc--;
    } else if( strcmp( *argv, "-o" ) == 0 || 
               strcmp( *argv, "--oneshot" ) == 0 ) {
      mode = ONESHOT_MODE;
    } else if( strcmp( *argv, "-c" ) == 0 ||
               strcmp( *argv, "--continuous" ) == 0 ) {
      mode = CONTINUOUS_MODE;
    } else if( strcmp( *argv, "-l" ) == 0 || 
               strcmp( *argv, "--level" ) ==0 ) {
      String levelstr = _T(*++argv); argc--;
      level = Level::toLevel(levelstr);
    } else if( strcmp( *argv, "-a" ) == 0 ||
               strcmp( *argv, "--autolevel" ) == 0 ) {
      levelMode = LEVEL_AUTO;
    } else {
      usage();
      return EXIT_FAILURE;
    }
  }

  /* 追加の設定ファイルがあれば読み込む */
  String propertyFileName;
  if( property != NULL ) {
    propertyFileName = _T(property);
    if( strstr( property, ".xml" ) == NULL ) {
      PropertyConfigurator::configure(propertyFileName);
    } else {
      xml::DOMConfigurator::configure(propertyFileName);
    }
  }

  /* メインループ */
  try {
    char buffstr[1024];
    while( fgets( buffstr, 1023, stdin ) ) {
      if( buffstr[strlen(buffstr)-1] == '\n' ) {
        buffstr[strlen(buffstr)-1] = '\0';
      }
      if( levelMode == LEVEL_FIXED ) {
        /* 動的にレベルを決める呼び方もできる */
        logger->log( level, _T(buffstr) );
      } else {
        if( strstr( buffstr, "DEBUG" ) != NULL ) {
          logger->debug( _T(buffstr) );
        } else if( strstr( buffstr, "INFO" ) != NULL ) {
          logger->info( _T(buffstr) );
        } else if( strstr( buffstr, "WARN" ) != NULL ) {
          logger->warn( _T(buffstr) );
        } else if( strstr( buffstr, "ERROR" ) != NULL ) {
          logger->error( _T(buffstr) );
        } else if( strstr( buffstr, "FATAL" ) != NULL ) {
          logger->fatal( _T(buffstr) );
        } else {
          logger->log( level, _T(buffstr) );
        }
      }
      if( mode == ONESHOT_MODE ) {
        break;
      }
    }
  } catch( Exception& ) {
    result = EXIT_FAILURE;
  }

  return result;
}

気になる log4cxx ライブラリのサイズだが...

% ps aux
USER       PID %CPU %MEM   VSZ  RSS TTY      STAT START   TIME COMMAND
root     10179  0.6  0.8  6040 1992 pts/2    S    16:04   0:00 ./LogTool -c

くらいなものなので、十分使い捨てツールとして使える。

swatch で「合わせて一本!」

これは例えば swatch と連動させて使うのも良かろう。swatch はログファイルを監視して、リアルタイムで指定のログイベントが起きた時に、何かをさせるためにある。応用として、サーバにエラーが生じてアボートしたケースを検出してメールを送ることもできるし、ログインがあった時にメールで確認を送る、あるいは「不正アクセス?」と疑わしいユーザのログインがあったときに、それに注意を促すための警報を出す、といった使い方も可能だ。

これもやはり Perl スクリプトである。動作原理は指定したログファイルへの新しい行の追加を監視し、新しい行が「監視すべき文字列の正規表現パターン」とマッチした時に、「アクション」が実行される、というものだ。だから swatch は、テキストファイルでありさえすれば、どんなログファイルにでも仕掛けることが可能だ。

swatch は開発元の http://www.oit.ucsb.edu/~eta/swatch からダウンロードするのが良かろう。最新版は swatch-3.1.1.tar.gz だったが、いろいろあってこれを薦める。ただし、いくつか CPAN(フリーの Perl モジュールをコレクションしているサイト) にアーカイヴされているフリーPerlモジュールが必要なので、ドキュメンテーションをしっかり読んでインストールしてくれ。4つほど追加モジュールが必要になるだろう。

通常 /usr/bin/swatch に実行のためのスクリプト本体がインストールされる。デフォルト動作もあるが、「どういう動作をさせるのか」を指定するスクリプトは、明示的にコマンドラインで指定するのが筆者の趣味だ。じゃ、やってみよう。起動はこうだ。

# /usr/bin/swatch --config-file=設定ファイル --tail-file=監視するログファイル &

のかたちでバックグラウンドで起動するのが良かろうが、筆者の魔法が一つある。それは「--use-cpan-file-tail」オプションを追加するんである。これはログローテーション問題でもほとんど問題なく、旧ファイルがバックアップされて新しいファイルに切り替わったことを認識するし、いろいろと反応も速い。ていうのか、これを指定しないと阿呆なことに /usr/bin/tail を起動してファイルの末尾を監視する、ということになるので、こっちの方がキレイだ。

まず例として、ログインした時に、それをメールで通知する場合だ。設定ファイルはこう。mail の前の空白はありがちなことだがタブなので気をつけるように。

watchfor        /login:/
                mail=admin@local.domain

このファイルで /var/log/secure を監視してやれば、ログイン時に /var/log/secure に現われるパターン

Aug 10 23:04:23 berio login: LOGIN ON 2 BY sug FROM localhost

に反応して、その行をメールで送るという寸法だ。

では、今度は Apache のログに「不正アクセス者ではないか?」と疑わしいアクセスが現われた時に、それをメールで送るのと同時に、ベルを鳴らして同じ部屋で作業している管理者に注意を促すというゴツイことをしてみよう。設定ファイルはこうだ。

watchfor     /^197\.156\.32\.35/ # 攻撃者のIPアドレスの正規表現
	     bell 5              # ベルを鳴らす
	     mail=admin@my.domain

そういや「カッコーはコンピュータに卵を産む」でクリフ・ストールのやってたこともこんなことだ。今時メアドは勿論携帯のメアドで構わんのだから、ちょっとしたセキュリティ対応チーム気分を味わえるぞ。

で、本題がこれが。任意のプログラムを起動したり、任意のプログラムに対してパイプで出力を渡すこともできる。

watchfor     /^.+$/  # 空行以外のすべての行を表す正規表現
             pipe /root/myscript/someCommand

で、このパイプ渡しをするコマンドに、先程の LogTool を指定してやるのだ!

watchfor /^.+$/  
        pipe /usr/local/src/RemoteLog/LogTool/watch.sh

ちょとカレントディレクトリを明示してやりたいので、単に cd するだけのスクリプトを噛ましておく。

#!/bin/sh
cd /usr/local/src/RemoteLog/LogTool
./LogTool

で /usr/local/src/RemoteLog/LogTool/log4j.properties はこうだ。とりあえず標準出力とファイルに出すだけしかしてないが、これを読むような奴は、SocketAppender だろうが TelnetAppender だろうが好きなことをするだけの実力があるはずだ。レイアウトパターンは、特にここではメッセージのみとして、ログのメッセージをそのまま出すことにするのがウルサくなくて良かろう。

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=%m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=messages.bak
log4j.appender.file.Append=true
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%m%n

log4j.rootLogger=debug, stdout, file



copyright by K.Sugiura, 1996-2006