目次


Eclipseプラグインのロギング・フレームワークをプラグイン

Eclipseのロギングを向上させる2つのアプローチ

Comments

なぜ、ログなのでしょうか?

有能な開発者でしたら、念入りな設計、テスト、そしてデバッグの重要性を理解しています。Eclipseはこれらのタスクを支援しますが、ロギングに関してはどうなのでしょうか?ロギングが良質のソフトウェア開発のプラクティスに必要不可欠だと多くの技術者たちが信じています。他の既にデプロイされたアプリケーションでの問題解決を経験済みでしたら、疑いなくこの見解に賛成されることでしょう。幸運なことに、大抵の場合ログ・ステートメントがパフォーマンスに対して持つインパクトは最小限であるか存在しません。ロギング・ツールは簡単に扱えますので、学習曲線は緩やかです。高性能のツールが入手可能ですので、アプリケーションにログ・ステートメントを包括できないと言う言い訳は通用しません。

ツールが使い放題

Eclipseのプラグインを作成しているのであれば、PluginクラスからのgetLog()メソッドを介してアクセスされたorg.eclipse.core.runtime.ILogに提供されるサービスを使用できます。正しい情報でorg.eclipse.core.runtime.Statusのインスタンスを作成し、ILogにてlog()メソッドを呼び出すだけです。

このログ・オブジェクトは複数のログ・リスナーのインスタンスを受け入れます。Eclipseはリスナーを2つ追加します。

  • "Error Log" Viewにログを書き込むListener
  • “${workspace}/.metadata/.log"に配置されたログ・ファイルにログを書き込むListener

独自のログ・リスナーを作成することも可能です。ただ単に、org.eclipse.core.runtime.ILogListenerインターフェースを実装し、addLogListener()メソッドを介してそれをログ・オブジェクトに追加するだけです。クラス内のlogging()メソッドは、それぞれのログ・イベントにて呼び出されます。

一見全てが何となくいい感じで収まっているのですが、このアプローチには問題がいくつか伴います。既にデプロイされたプラグインのログの宛先を変更したいのであれば、どうなのでしょうか?ログ記録された情報の量を調節したいのであれば、どうなのでしょうか?それから、全てのリスナーにログ・イベントを常時送信しますので、パフォーマンスにこの実装は影響を及ぼします。ですから、(例えばエラー状態のように)極端な場合にのみログを見かけることになるかも知れません。

他方では、ロギング専用のツールが2種類あります。ひとつはJava 2 SDK 1.4と一緒にjava.util.logging のパッケージとして提供されます。もうひとつはLog4jと呼ばれ、Apacheから提供されます。

(Log4jにてLayoutsと呼ばれる)Formatter にメッセージ・フォーマット設定を委任する(Log4jにてAppenderと呼ばれる)Handler何個にでもログ・イベントを送信できるLoggerオブジェクトの階層の概念を両方とも持ちます。両方ともプロパティー・ファイルにより構成されます。Log4j はxmlファイルを構成に使用したりもします。

ロガー(logger)は名前を所持し、Levelと関連できます。ロガーは親からその設定(レベル、ハンドラー)を継承できます。「org」と名付けられたロガーは自動的に「org.eclipse」と名付けられたロガーの親になりますので、構成ファイルにて「org」に設定したものは「org.eclipse」ロガーに継承されます。

どちらがより好ましいのでしょうか?両方とも試しましたが、個人的にはLog4jの方に傾いています。かなり簡素なアプリケーションがありそしてlog4j.jarを追加したくはない場合にのみ、java.util.loggingを使います。両方の完全な説明をお求めでしたら、Java文書とApacheのサイトを参照してください(参考文献にリンクがあります)。

進化を遂げたログ

Eclipseのログが進化を遂げる方法があれば素晴らしいと思いませんか?それは下記の2件の問題を伴います。

  • 外部構成ファイルの欠落
  • 振る舞いの細分化された制御が存在しないと言う事実と関連する、パフォーマンスの問題

この難題と直面したとき、Eclipseにロギング・ツールを統合する方法について考えを巡らせました。ただ単にJSDK1.4の配布物の中にてすでに存在していると言う理由から、最初の候補はjava.util.loggingでした。

プラグインのライターが構成ファイルを介してロギングをカスタマイズできるようにし、既に入手可能などのハンドラーにも行けるようにログ・イベントを促したいのです。2つの追加的なハンドラーの作成を計画します。ひとつは「Error Log」のviewにログ・イベントを送信し、別のものはPlug-inのstate location(${workspace}/.metadata/.plugins/${plugin.name})に

これら全てがPlug-in Log Managerに含まれています。それをプラグインのdependenciesに追加して、ロガー・オブジェクトをそれから得るのみです。

これは経験から得られた教訓ですが、この理由のためにjava.util.loggingを使用することをお奨めできません。その実装はただひとつのLogManagerインスタンスを保管するために徹底的に何でもします。それはシステム・クラス・ローダーを使用して達成しようとします。したがって、全てのユーザーにただひとつのロガーの階層しかありません。ここで分離性(isolation)が失われます。もしも複数のアプリケーションがロガーを使用すれば、それらは設定を共有し、とあるアプリケーションのロガーのインスタンスは潜在的に別のアプリケーションのロガーから設定を継承することもあり得ます。

それならば、どうして自身でLogManager を拡張しインスタンス化をしないのでしょうか?構成ファイルからクラスをインスタンス化するためにシステム・クラス・ローダーをLogManagerインスタンスが使用することが、このアプローチの問題点となります。プラグインの長所のひとつとして、様々なクラス・ローダーの使用を介して分離性を提供することが挙げられます。ログ・マネジャーに分離性が必要なのであれば、アーキテクチャーの限界のためjava.util.loggingは適切とは言えません。

他方では、Log4jがかなり使えることがわかりました。そのロガーの階層は(信じがたいのですが)Hierarchy(階層)と名付けられたオブジェクトにて保管されています。つまり、それぞれのプラグインにひとつの階層を作成できるのです。これにて一件落着です。「Error Log」ビューへイベントを送信するためにカスタムのappender (ハンドラー)を作成し、また別のイベントをプラグインの state location へ送信するために別のカスタムのappender (ハンドラー)を作成することもできます。人生悪いことばかりではありません。

プラグインの作成者の視点から始め、やり方を反復しましょう。単にプラグインを作成し、com.tools.loggingをその従属リストに追加し、そしてLog4j構成ファイルを作成するだけです。PluginLogManager をインスタンス化し、このファイルと共に構成します。プラグインが開始される時点にてできるように、それを一度のみ実行すればよいのです。ログ・ステートメントに関して言えば、Log4jに対して行なったことをそのまますればよいだけです。リスト1に例を示します。

リスト1. TestPlugin プラグイン・クラスでのPluginLogManager 構成
private static final String LOG_PROPERTIES_FILE = "logger.properties";
public void start(BundleContext context) throws Exception {
   super.start(context);
   configure();
}
private void configure() {
   try {
      URL url = getBundle().getEntry("/" + LOG_PROPERTIES_FILE);
      InputStream propertiesInputStream = url.openStream();
      if (propertiesInputStream != null) {
         Properties props = new Properties();
         props.load(propertiesInputStream);
         propertiesInputStream.close();
         this.logManager = new PluginLogManager(this, props);
         this.logManager.hookPlugin(
          TestPlugin.getDefault().getBundle().getSymbolicName(),
          TestPlugin.getDefault().getLog()); }	
   } catch (Exception e) {
      String message = "Error while initializing log properties." +
                       e.getMessage();
      IStatus status = new Status(IStatus.ERROR,
      getDefault().getBundle().getSymbolicName(),
      IStatus.ERROR, message, e);
      getLog().log(status);
      throw new RuntimeException(
           "Error while initializing log properties.",e);
   }         
}

プラグインをデプロイすれば、いつでも誰でもそのログ構成を変更しそしてロギングにフィルターをかけられ、またはコード一行触れずにその出力を変更できます。さらに嬉しいことに、Log4jにとってパフォーマンスは設計段階での主要な考慮事項のひとつでしたので、ログが使用不可になればその全てのステートメントはパフォーマンスに影響を与えません。ロガー・メソッドをどこにでも必要を感じた場合にちりばめることができます。

実行あるのみ

使用方法に関してはここまでにしましょう。com.tools.loggingでのその実装に注目しましょう。

まずはPluginLogManagerクラスです。それぞれのプラグインには1つログ・マネジャーがあります。リスト2にて示されるとおり、それは階層オブジェクトそしてカスタムのappenderに必要なデータを含みます。エンド・ユーザーに公開しないようにするために、それはHierarchy クラスから直接派生させないようにします。これは実装により高い自由度を提供します。コンストラクターはDEBUGのデフォルト・レベル付きの階層オブジェクトを作成し、与えられたプロパティーで構成します。xml プロパティーに許可を下すのは簡単です。Xerces プラグインにdependencyを追加しPropertyConfiguratorの代わりにDOMConfigurator を使用することだけを必要とします。練習として、これを実際に実践されることをおすすめいたします。

リスト2. PluginLogManagerコンストラクター
public PluginLogManager(Plugin plugin,Properties properties) {
   this.log = plugin.getLog();  this.stateLocation = plugin.getStateLocation();
   this.hierarchy = new Hierarchy(new RootCategory(Level.DEBUG));
   this.hierarchy.addHierarchyEventListener(new PluginEventListener());
   new PropertyConfigurator().doConfigure(properties,this.hierarchy);	
   LoggingPlugin.getDefault().addLogManager(this); 
}

org.apache.log4j.spi.HierarchyEventListenerを実装する内部クラス(PluginLogManager)があるのに注目してください。これはカスタムのappenderに必要な情報を手渡すソリューションです。リスト3に示されるとおり、appenderがインスタンス化され完全に構成されてアペンドする準備ができたら、addAppenderEvent() メソッドは呼び出されます。

リスト3. PluginEventListener 内部クラス
private class PluginEventListener implements HierarchyEventListener {
		
   public void addAppenderEvent(Category cat, Appender appender) {
      if (appender instanceof PluginLogAppender) {
         ((PluginLogAppender)appender).setLog(log);
      }			
      if (appender instanceof PluginFileAppender) {
         ((PluginFileAppender)appender).setStateLocation(stateLocation);
      }
   }
	
   public void removeAppenderEvent(Category cat, Appender appender) {
   }
}

appenderのライフ・サイクルそしてその判断の一部をより深く理解するには、UML Sequence Diagramが役に立つかも知れません。最もトリッキーと言えるPluginFileAppender インスタンスの構成そして発生へと導くイベントのシーケンスを、図1に示します。

図1. PluginFileAppender 構成のシーケンス図
図1. PluginFileAppender 構成のシーケンス図
図1. PluginFileAppender 構成のシーケンス図

appenderの場合、org.apache.log4j.RollingFileAppender は拡張されています。それはファイル操作を何の代償もなく実行させるだけではなく、一度サイズの最大値に達すれば自動的にログのライティングを別のファイルにカスケードする能力やファイル・サイズの最大値制限などの便利な追加項目をも提供します。

RollingFileAppenderの拡張を選択すれば、その振る舞いにも対処しなくてはなりません。一度Log4j がappenderを作成すれば、「setter」メソッドを呼び出すことにより構成ファイルからそのプロパティーを初期化してから(未解決の初期化をappenderに終了させるために)activateOptions()を呼び出します。これが発生すれば、,RollingFileAppender のインスタンスは(ログ・ファイルを開き書き込みの準備をそれに対してする)setFile()を呼び出します。そうした後のみ、Log4j はPluginEventListener インスタンスに通知します。

当然ですが、state locationを設定する前からファイルが開くことを許可するわけにはいきません。もしもactivateOptions()が呼び出され、(まだ状態情報がないのであれば)それはペンディング中としてフラグを立てられ、(state location が最終的に設定されれば)それはもう一度呼び出されてappenderはビジネスでの実践にて活用できます。

別のappenderであるPluginLogAppenderは同じライフ・サイクルを持ちますが、それは存在するappenderを拡張しませんので、初期化に関する問題について懸念をいだく必要はありません。addAppenderEvent メソッドが呼び出される前には、appenderは機能を開始しません。独自のカスタムappenderを書き込む上で十分な量の論考をLog4jは提供します。そのappendメソッドをリスト4に示します。

リスト4. PluginLogAppender のappend メソッド
public void append(LoggingEvent event) {
		
   if (this.layout == null) {
      this.errorHandler.error("Missing layout for appender " +
             this.name,null,ErrorCode.MISSING_LAYOUT); return;
   }
   String text = this.layout.format(event);
   Throwable thrown = null;
   if (this.layout.ignoresThrowable()) {
      ThrowableInformation info = event.getThrowableInformation();
      if (info != null)
         thrown = info.getThrowable(); }
		
   Level level = event.getLevel();
   int severity = Status.OK;
   if (level.toInt() >= Level.ERROR_INT) severity = Status.ERROR;
   else
   if (level.toInt() >= Level.WARN_INT)
      severity = Status.WARNING;
   else
   if (level.toInt() >= Level.DEBUG_INT) severity = Status.INFO;
	
   this.pluginLog.log(new Status(severity,
             this.pluginLog.getBundle().getSymbolicName(),
             level.toInt(),text,thrown));
}

LoggingPlugin クラスはPluginLogManagersのリストを管理します。リスト5にて示されるとおり、プラグインが停止すれば、全ての階層をシャットダウンしてappenderとロガーが正しく除外されることを可能にしますので、それは必要なのです。

リスト5. ログ・マネジャーへのLoggingPlugin クラスの取り扱い
private ArrayList logManagers = new ArrayList(); 
public void stop(BundleContext context) throws Exception {
   synchronized (this.logManagers) {
      Iterator it = this.logManagers.iterator();
      while (it.hasNext()) {
         PluginLogManager logManager = (PluginLogManager) it.next();
         logManager.internalShutdown(); }
     this.logManagers.clear(); }
   super.stop(context);
}
void addLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
      if (logManager != null)
         this.logManagers.add(logManager); }
}
	
void removeLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
      if (logManager != null)
         this.logManagers.remove(logManager); }
}

PluginLogManager クラスにもう一つ何かが追加されます。(特にワークベンチに依存する)従属するプラグインはときどき例外を投げ掛けます。普通、それらの例外はEclipseによりログされます。従属プラグインをログのフレームワークにフックされるのを許可するのは便利です。例外が発生すれば、Eclipseのロギングならばどれでもログのフレームワークへと集められ別のロガーとその構成を共有します。一ヶ所に全てを終結させアプリケーションの修正を促す事実の履歴を参照できるので、これはかなり使えます。

org.eclipse.core.runtime.ILogListener を実装し、それを従属プラグインのILog インスタンスに追加することにより達成できます。基本的には、Eclipse のロギングにそれをフックしているだけです。この実装は要求を全て選ばれた名前(通常はプラグインID)で作成されたLogger へリダイレクトします。それから同じプロパティー・ファイルを介してその出力を構成できます。Logger 名を指定してそれをフィルターしappenderを追加する等を実行するだけです。クラスはリスト6にて記されています。

リスト6. PluginLogListener クラス
class PluginLogListener implements ILogListener {
   private ILog log;
   private Logger logger;
   PluginLogListener(ILog log,Logger logger) {
      this.log = log;
      this.logger = logger;
      log.addLogListener(this);
   }
   void dispose() {
      if (this.log != null) {
         this.log.removeLogListener(this);
         this.log = null;
         this.logger = null;
      } }
   public void logging(IStatus status, String plugin) {
      if (null == this.logger || null == status) return;
	
      int severity = status.getSeverity();
      Level level = Level.DEBUG;  if (severity == Status.ERROR)
         level = Level.ERROR;
      else
      if (severity == Status.WARNING)
         level = Level.WARN;
      else
      if (severity == Status.INFO)
         level = Level.INFO;
      else
      if (severity == Status.CANCEL)
         level = Level.FATAL;
      plugin = formatText(plugin);
      String statusPlugin = formatText(status.getPlugin());
      String statusMessage = formatText(status.getMessage());
      StringBuffer message = new StringBuffer();
      if (plugin != null) {
         message.append(plugin);
         message.append(" - ");
      }    if (statusPlugin != null && (plugin == null || !statusPlugin.equals(plugin))) {
         message.append(statusPlugin);
         message.append(" - ");
      }	
      message.append(status.getCode());
      if (statusMessage != null) {
         message.append(" - ");
         message.append(statusMessage);
      } 		
      this.logger.log(level,message.toString(),status.getException());	
   }
   static private String formatText(String text) {
      if (text != null) {
         text = text.trim();
         if (text.length() == 0) return null;
      } return text;
   }
}

フレームワーク全体はcom.tools.loggingと呼ばれるプラグイン・プロジェクトに実装されます。それが機能していることを示すために、2つのプラグインを作成しました。

  1. HelloPlugin はプロジェクトのテンプレートから構築され、「Hello, Eclipse world」と書かれたメッセージ・ボックスを表示します。
  2. TestPluginLog はHelloPluginとdependencyを持ち追加されますので、同一のログ階層にフックされます。HelloPluginのログに転送されるEclipse APIを使用してダミー・メッセージを追加するdummyCall() と呼ばれるメソッドを持ちます。

org.eclipse.ui やorg.eclipse.core.runtimeのように、別のプラグインのdependencyも同様にフックできます。

その長所を披露するためにlogger.properties 構成を作成するのには、注意が必要でした。リスト7に示されるとおり、2つのappender が定義されています。A1 と言うappender はPluginFileAppenderであり、ルートのロガーに割り当てられています。他のロガー全てはルートから継承してappenderを使用できます。かくして、(TestPluginLogからのを含む)全てのログが、プラグインのstate locationにあるファイルに行きます。

リスト7. HelloPlugin プロジェクト内のLogger.properties ファイル
log4j.rootCategory=, A1
# A1 is set to be a PluginFileAppender
log4j.appender.A1=com.tools.logging.PluginFileAppender
log4j.appender.A1.File=helloplugin.log
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%p %t %c - %m%n
# A2 is set to be a PluginLogAppender
log4j.appender.A2=com.tools.logging.PluginLogAppender
log4j.appender.A2.layout=org.apache.log4j.PatternLayout
log4j.appender.A2.layout.ConversionPattern=%p %t %c - %m%n
# add appender A2 to helloplugin level only
log4j.logger.helloplugin=, A2

もうひとつのappenderであるA2は、PluginLogAppender であり、「helloplugin」ロガーに追加されるのみなので、TestPluginLog はそれを使用しません。そうでなければ、「TestPluginLog」の「Error View」ウィンドウに2つの入力があることになります。ひとつはEclipse からで、もうひとつはcom.tools.loggingからです。(それで実際に試してみれば、ここで言わんとしていることがわかることでしょう)。単にlog4j.rootCategory にA2 を追加し、log4j.logger.helloplugin の行をまるきり削除するのみです。

「sample menu」アイテムがクリックされてメッセージ・ボックスが表示された後の${workspace}/.metadata/.plugins/HelloPlugin/helloplugin.log のコンテントを、リスト8は示します。最後の行にてEclipse ログであるTestPluginLog がどのように書き込まれているかに注目してください。自身のログと従属プラグインのEclipse ログを一つの出力として結合させることにより、イベントのシーケンスを保持できます。

リスト8. helloplugin.log
INFO main helloplugin.actions.SampleAction - starting constructor.
INFO main helloplugin.actions.SampleAction - ending constructor.
WARN main helloplugin.actions.SampleAction - init
WARN main helloplugin.actions.SampleAction - run method
WARN main TestPluginLog - TestPluginLog - 0 - Logging using the Eclipse API.

まとめ

Eclipse のロギングを向上させる2種類のアプローチを紹介しました。ひとつの方法では、com.tools.logging をプラグインにて使用し、(望むのであれば)Eclipse ログのフレームワークの一部に関わると同時にLog4j の役立つ機能を全て得られます。別のアプローチでは、Log4j うんぬんの知識を持たないプラグインをフックして、それがEclipse ログAPIしか使わなくてもそのログ出力を構成できます。

実際には、com.tools.loggingを所有する必要さえもありません。この時点ではサンプル・コードを抽出して自身のプラグインにそれを(可能性としては個別の分離されたJarファイルとして)追加することも可能です。当然ですが、とにかくLog4j Jar ファイルを忘れてはなりません。

プラグインは新規のOSGIのマニフェストで作成されました。全てのコードはEclipse 3.0 Release Candidate 1、Sun Java 2 SDK 1.4.2、そしてLog4j バージョン 1.2.8を使用して開発そしてテストされました。ダウンロード可能なコードにlog4j-1.2.8.jar ファイルを包括しませんでした。コードをダウンロードするのであれば、このJarファイルをApache Log4j から入手してそれをcom.tools.logging プロジェクトとcom.tools.logging_1.0.0 プラグイン・ディレクトリーに包括させましょう。


ダウンロード可能なリソース


関連トピック

  • eclipse.org のサイトで、プラグイン文書、ソース・コード、そして最新のEclipse ビルドを探しましょう。
  • Sun Java 2 SDK 1.4.2 のサイトにて、java.util.loggingの文書そしてJavaランタイムを入手しましょう。
  • Log4j に関する全てが、ApacheのLog4j Logging Services のサイトにあります。
  • developerWorksのOpen source projectsのページにてより多くのEclipseユーザー用の記事を探してみましょう。それから、alphaWorksにて最新のEclipseテクノロジーに関するダウンロードをご覧になってはいかがでしょうか?
  • Developer BookstoreにあるOpen Source のページにて、オープン・ソースに関する文献を割引価格で購入してみましょう。Eclipse関連の書物をいくつか見付けられます。
  • 最新のIBMツールとミドルウェアを駆使して独自のアプリケーションを開発そしてテストするのでしたら、developerWorks サブスクリプションを活用しない手はありません。DB2、Lotus、Rational、そしてTivoliに加えて、EclipseをベースにしたWebSphere Studio Application Developer for LinuxとWebSphere Studio Application Developer for WindowsのようなIBMソフトウェアを(12ヶ月間有効なソフトウェア使用ライセンスと共に)WebSphereから(全て予想以上に廉価で)入手できます。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Java technology
ArticleID=236853
ArticleTitle=Eclipseプラグインのロギング・フレームワークをプラグイン
publish-date=09272004