第7章 フィルター
たくさんのアイデアを集めて悪いものを捨てていこう。たくさんのアイデアと、自分なりの基準が無ければ、いいアイデアに巡り会えることはない。
—LINUS PAULING
ここまでに、基本的な選択ルールがlogback-classicモジュールの中核を担っていることを説明してきました。本章では、それに加えてフィルタリングの方法を紹介します。
logbackのフィルターは、三値論理に基づいて合成や連結を駆使して、任意に複雑な条件を実現することができます。主に Linux の iptables から着想を得たものです。
logback-classicモジュール
logback-classic モジュールには二種類のフィルターがあります。通常フィルターとターボフィルターです。
通常フィルター
通常フィルターとは、Filter抽象クラスを継承したものです。本質的にはILoggingEventを引数にとるdecide()メソッドを実装することが目的です。
フィルターは順序付きリストにまとめて扱われます。また、三値論理に基づいて扱われます。それぞれのフィルターのdecide(ILoggingEvent event)メソッドが順番に呼び出されます。このメソッドはFilterReply列挙型の値である、DENY 、 NEUTRALまたはACCEPTを返します。decide()メソッドがDENYを返したら、そのロギングイベントは残りのフィルターに渡されることなく、ただちに破棄されます。NEUTRALを返したら、リスト内の次のフィルターに渡されます。リストの末尾に到達したら、そのロギングイベントは通常通りに処理されることになります。ACCEPTを返したら、残りのフィルターはスキップして、そのロギングイベントはただちに処理されます。
logback-classicモジュールでは、Appenderにフィルターを追加することが出来ます。アペンダーに複数のフィルターを登録すれば、ロギングイベントをさまざまな条件で篩にかけられるようになります。ロギングメッセージの内容やMDCの内容、時刻や日付などロギングイベントのあらゆる内容を判定できるのです。
フィルターを自作する
フィルターを自作するのは簡単です。Filter抽象クラスを継承してdecide()メソッドを実装するだけです。
例としてSampleFilterクラスを見てください。decide()メソッドがACCEPTを返すのは、ロギングイベントのメッセージに "sample" という文字列が含まれる場合だけです。その他の場合はNEUTRALを返すようになっています。
例:基本的な自作フィルター(logback-examples/src/main/java/chapters/filters/SampleFilter.java)
package chapters.filters;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
public class SampleFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
if (event.getMessage().contains("sample")) {
return FilterReply.ACCEPT;
} else {
return FilterReply.NEUTRAL;
}
}
}
次の設定ファイルでは、ConsoleAppenderにSampleFilterを割り当てています。
例:SampleFilterの設定(logback-examples/src/main/java/chapters/filters/SampleFilterConfig.xml)
Groovyとして表示<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="chapters.filters.SampleFilter" />
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT" />
</root>
</configuration>
設定フレームワークの Joran を使えばフィルターのプロパティやサブコンポーネントを指定するのも簡単です。フィルタークラスにフィールドのセッターメソッドを追加すれば、filter要素にネストしてプロパティの値を指定することができます。
たいていの場合フィルタリング条件には2つの直行する条件が含まれています。マッチするかどうかの条件と、何を返すのかの条件です。たとえば、メッセージが"foobar"だったらACCEPTを返し、そうでなければNEUTRALを返すフィルターもありますし、メッセージが"foobar"だったらNEUTRALを返し、そうでなければDENYを返すフィルターもあります。
logback の配布物には、この直交性に焦点を当てたAbstractMatcherFilterが含まれています。このクラスはフィルター条件の判定結果に基づいて何らかの値を返すスケルトンです。判定結果が真の時に返す値をOnMatchプロパティに、偽の時に返す値をOnMismatchプロパティに指定することができます。logbackの配布物に含まれるほとんどの通常フィルターはAbstractMatcherFilterを継承しています。
LevelFilter
LevelFilterは、ロギングイベントのログレベルの正確なマッチングに基づいたフィルタリングをします。ログレベルが設定されたレベルと等しければ、omMatchプロパティあるいはomMismachプロパティに設定された値に応じて、ロギングイベントを受け入れるか拒否するかが決まります。設定ファイルを見てみましょう。
例:LevelFilterの設定例(logback-examples/src/main/java/chapters/filters/levelFilterConfig.xml)
Groovyとして表示<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
ThresholdFilter
ThresholdFilterは、ログレベルが指定されたしきい値より低いロギングイベントをフィルタリングします。ログレベルがしきい値と同じかより高い場合、ThresholdFilterのdecide()はNEUTRALを返します。一方、しきい値より低いログレベルのロギングイベントは拒否します。設定ファイルを見てみましょう。
例:ThresholdFilterの設定例(logback-examples/src/main/java/chapters/filters/thresholdFilterConfig.xml)
Groovyとして表示<configuration>
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<!-- deny all events with a level below INFO, that is TRACE and DEBUG -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
EvaluatorFilter
EvaluatorFilterは内部でEventEvaluatorを使用する汎用的なフィルターです。名前のとおり、ロギングイベントがEventEvaluatorに指定された条件を満たすかどうかを評価します。評価結果がなんであれ、EvaluatorFilterに設定されたonMatchプロパティまたはonMismatchプロパティの値を返します。
EventEvaluatorは抽象クラスです。つまり、EventEvaluatorを継承すれば、独自のイベント評価ロジックを実装することができます。
GEventEvaluator
GEventEvaluatorはEventEvaluatorの派生クラスで、評価条件に結果が真偽値になるGroovy言語で書かれた任意の式を指定することができます。私たちはこのGroovy言語で書かれた式のことを "Groovy評価式" と呼んでいます。Groovy評価式を使うと、ロギングイベントをこれまでにないくらい柔軟にフィルタリングできるようになります。GEventEvaluator を使うにはGroovyのランタイムが必要です。設定ドキュメントの対応するセクションには、クラスパスにGroovyのランタイムを指定する方法が記載されています。
Groovy評価式は設定ファイルを解釈する際にコンパイルされます。どのように実行させるのかか、利用者が考える必要はありません。しかし、Groovy言語として間違いが無いことを保証するのは使用者の責任です。
Groovy評価式は一度に1つのロギングイベントを扱います。logbackは、ロギングイベントをILoggingEvnet型の変数'event'あるいは'e'として用意します。また、ログレベルのTRACE、DBUG、INFO、WARN、ERROR は、Groovy評価式から同じ名前の変数として使用することが出来ます。したがって、"event.level == DEBUG" と "e.level == DEBUG" は同じ意味のGroovy評価式ということになります。ロギングイベントのログレベルがDEBUGの場合、式の値はtrueになります。他の比較演算子を使うときは、ログレベルの変数にtoInt()演算子を適用して、整数値として評価しなければなりません。
具体的な例を見てみましょう。
Groovyとして表示<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator">
<expression>
e.level.toInt() >= WARN.toInt() && <!-- Stands for && in XML -->
!(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
</expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
この設定は、ログレベルがWARN以上で、ユーザーエージェントがクローラー(GooglebotやmsnbotやYahoo)以外のロギングイベントのメッセージをコンソールに出力します。ロギングイベントに関連付けられた MDC の "req.userAgent" の値を正規表現/Googlebot|msnbot|Yahoo/で評価して、ユーザーエージェントを判定しています。MDCがnullになることもあるので、Groovyの安全なデリファレンス演算子(?.)を使っています。同じことをJava言語で実装するともっと長くなってしまうでしょう。
ユーザーエージェント文字列がいつMDCに登録されたのか疑問に思うかもしれません。説明しておくべきでしたが、logbackの配布物に含まれているMDCInsertingServletFilterを使っています。詳しくは後の章で説明します。
JaninoEventEvaluator
logback-classicの配布物には、JaninoEventEvaluatorというEventEvaluatorの別の実装クラスが含まれています。これは、booleanを返す任意のJava言語のブロックを評価するものです。私たちはこのJava言語で書かれた式のことを"Java評価式”と呼んでいます。Java評価式を使うとロギングイベントを柔軟にフィルタリングできるようになります。JaninoEventEvaluatorを使用するにはJaninoライブラリが必要です。設定方法は設定ドキュメントの対応するセクションを参照してください。JaninoEventEvaluatorと比べると、GEventEvaluatorはGroovy言語のおかげで非常に使いやすいです。ですが、JaninoEventEvaluatorのほうが同じ評価式をより高速に実行することができます。
Java評価式は設定ファイルを解釈する間にコンパイルされます。どのように呼び出すのか、利用者は気にすることはありません。ですが、Java言語の式が真偽値を返すものであることを保証するのは利用者の責任です。
Java評価式は一度に1つのロギングイベントを扱います。logback-classicは、ロギングイベントのいろいろなフィールドをJava評価式から参照できる変数として自動的に公開します。公開する変数の名前は大文字小文字を区別するものです。表にまとめました。
| 変数名 | 型 | 説明 |
|---|---|---|
| event | LoggingEvent |
ロギング要求に関連付けられたロギングイベントそのもの。以下の変数はロギングイベントから参照することができます。たとえば、 event.getMessage()はmessage変数と同じ文字列を返します。
|
| message | String |
ロギング要求に指定されたメッセージそのものです。ロガーlについて l.info("Hello {}", name); というロギング式があったとき、"Hello {}" がメッセージとなります。 |
| formattedMessage | String |
ロギング要求の書式化されたメッセージ。ロガーlについて l.info("Hello {}", name); というロギング式があったとき、nameの値が"Alice"なら、"Hello Alice" が書式化されたメッセージになります。 |
| logger | String |
ロガーの名前。 |
| loggerContext | LoggerContextVO |
ロギングイベントが割り当てられたロガーコンテキストの、値オブジェクトとしてのビュー。 |
| level | int |
ログレベルに対応する整数値。ログレベルを含む評価式を簡潔にするため、DEBUG、INFO 、WARN、ERRORが利用できるようになっています。たとえば、 level > INFO は正しい評価式です。 |
| timeStamp | long |
ロギングイベントの作成時のタイムスタンプ。 |
| marker | Marker |
ロギング要求に関連付けられたMarkerオブジェクト。マーカーオブジェクトがnullの場合もあるので、NullPointerExceptionを避けるためにnullチェックをするのは使用者の責任です。
|
| mdc | Map |
ロギングイベントの作成時に関連付けられたMDC。mdc.get("MYKEY")とすると値を参照できます。logback-classic 0.9.30以降では、'mdc'変数は決してnullになりません。
Janino はジェネリクスをサポートしていないので、 |
| throwable | java.lang.Throwable | ロギングイベントに例外オブジェクトが関連付けられていないときは、"throwable"変数はnullになります。"throwable"変数はシリアライズすると失われてしまいます。したがって、リモートサーバ側ではこの値は常にnullになります。ローカルとリモートで同じ評価式を使いたい場合は、次項のthrowableProxy変数を使用してください。
|
| throwableProxy | IThrowableProxy |
ロギングイベント関連付けられた例外オブジェクトのプロキシオブジェクト。例外オブジェクトが関連付けられていないとき、"throwableProxy"変数はnullになります。"throwable"変数と違って、例外オブジェクトがロギングイベントに関連付けられているときは、シリアライズされてリモートサーバに渡された後でも "throwableProxy"変数の値はnullになりません。 |
具体的な例を見てみましょう。
例:評価式の基本的な使い方(logback-examples/src/main/java/chapters/filters/basicEventEvaluator.xml)
Groovyとして表示<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
<expression>return message.contains("billing");</expression>
</evaluator>
<OnMismatch>NEUTRAL</OnMismatch>
<OnMatch>DENY</OnMatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
設定ファイル中の太字部分で、ConsoleAppenderにEvaluatorFilterを追加しています。EvaluationFilterに追加されたのはJaninoEventEvaluatorです。evaluator要素のclass属性を省略すると、JoranはデフォルトのJaninoEventEvaluatorを使用します。これはJoranが暗黙的にコンポーネントの型を推測する珍しいケースの1つです。
expression要素に指定されているのは評価式です。return message.contains("billing");という式の値は真偽値です。message変数は、JaninoEventEvaluatorが自動的に公開した変数です。
OnMismatchプロパティにNEUTRALが、OnMatchプロパティにDENYが指定されているので、このフィルターはメッセージに"billing"という文字列の含まれているロギングイベントをすべて拒否することになります。
FilterEventsアプリケーションでは、0〜9までの連番を付けられた10個のロギング要求を生成します。まずはフィルター無しでFilterEventsを実行してみましょう。
java chapters.filters.FilterEvents src/main/java/chapters/filters/basicConfiguration.xml
次のように、全てのロギング要求が出力されます。
0 [main] INFO chapters.filters.FilterEvents - logging statement 0 0 [main] INFO chapters.filters.FilterEvents - logging statement 1 0 [main] INFO chapters.filters.FilterEvents - logging statement 2 0 [main] DEBUG chapters.filters.FilterEvents - logging statement 3 0 [main] INFO chapters.filters.FilterEvents - logging statement 4 0 [main] INFO chapters.filters.FilterEvents - logging statement 5 0 [main] ERROR chapters.filters.FilterEvents - billing statement 6 0 [main] INFO chapters.filters.FilterEvents - logging statement 7 0 [main] INFO chapters.filters.FilterEvents - logging statement 8 0 [main] INFO chapters.filters.FilterEvents - logging statement 9
この中から"billing statement"を取り除きたいものとします。上記のbasicEventEvaluator.xmlでは、メッセージに"billing"を含むロギングイベントをフィルタリングするので、まさに今欲しいものです。
basicEventEvaluator.xmlを使って実行してみましょう。
java chapters.filters.FilterEvents src/main/java/chapters/filters/basicEventEvaluator.xml
次のような出力になります。
0 [main] INFO chapters.filters.FilterEvents - logging statement 0 0 [main] INFO chapters.filters.FilterEvents - logging statement 1 0 [main] INFO chapters.filters.FilterEvents - logging statement 2 0 [main] DEBUG chapters.filters.FilterEvents - logging statement 3 0 [main] INFO chapters.filters.FilterEvents - logging statement 4 0 [main] INFO chapters.filters.FilterEvents - logging statement 5 0 [main] INFO chapters.filters.FilterEvents - logging statement 7 0 [main] INFO chapters.filters.FilterEvents - logging statement 8 0 [main] INFO chapters.filters.FilterEvents - logging statement 9
Java評価式にはJavaのコードブロックを指定できます。つまり次のようなものでも正しい式なのです。
<evaluator>
<expression>
if(logger.startsWith("org.apache.http"))
return true;
if(mdc == null || mdc.get("entity") == null)
return false;
String payee = (String) mdc.get("entity");
if(logger.equals("org.apache.http.wire") && <!-- & encoded as & -->
payee.contains("someSpecialValue") &&
!message.contains("someSecret")) {
return true;
}
return false;
</expression>
</evaluator>
マッチャー
Stringのmatchs()メソッドを使えば文字列のパターンマッチをすることができます。ですが、毎回Pattern(正規表現)オブジェクトをコンパイルするコストがかかるので、つまりフィルターが呼び出されるたびにコストがかかることになってしまいます。このオーバーヘッドを無くすため、Matcherオブジェクトを事前に複数用意することができます。定義したマッチャーオブジェクトは評価式の中から名前で参照できるようになります。
マッチャーの使用例を見てみましょう。
例:マッチャーの定義(logback-examples/src/main/java/chapters/filters/evaluatorWithMatcher.xml)
Groovyとして表示<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>odd</Name>
<!-- filter out odd numbered statements -->
<regex>statement [13579]</regex>
</matcher>
<expression>odd.matches(formattedMessage)</expression>
</evaluator>
<OnMismatch>NEUTRAL</OnMismatch>
<OnMatch>DENY</OnMatch>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
evaluatorWithMatcher.xmlの設定を使ってみましょう。
java chapters.filters.FilterEvents src/main/java/chapters/filters/evaluatorWithMatcher.xml
コンソールには次のように出力されます。
260 [main] INFO chapters.filters.FilterEvents - logging statement 0 264 [main] INFO chapters.filters.FilterEvents - logging statement 2 264 [main] INFO chapters.filters.FilterEvents - logging statement 4 266 [main] ERROR chapters.filters.FilterEvents - billing statement 6 266 [main] INFO chapters.filters.FilterEvents - logging statement 8
マッチャーを追加したければ、matcher要素を追加すればよいでしょう。
TurboFilters
ターボフィルターとは、TurboFilter抽象クラスを継承したオブジェクトのことです。通常フィルターと同様に、三値論理でロギングイベントを評価します。
全体的に前に説明したフィルターと同じように動作します。ただし、FilterとTurboFilterには大きな違いが2つあります。
TurboFilterはロギングコンテキストに紐付けられています。したがって、アペンダーが使用されたときにだけ呼ばれるのではなく、ロギング要求が発生するたびに呼ばれることになります。つまり、ターボフィルターの有効範囲はアペンダーに割り当てられたフィルターよりも広いのです。
さらに重要なのは、ターボフィルターが呼ばれるのはLoggingEventオブジェクトが作成される前だということです。
TurboFilterオブジェクトは、ロギング要求をフィルタリングするのにロギングイベントを必要としません。つまり、ターボフィルターはロギングイベントの高速なフィルタリングを意図したものなのです。
ターボフィルターを自作する
ターボフィルターを自作するには、TurboFilter抽象クラスを継承するだけです。前述のとおり、フィルターを自作するにはdecide()メソッドを実装するだけでいいのです。少し複雑なフィルターの実装例を見てみましょう。
例:基本的な自作TurboFilter(logback-examples/src/main/java/chapters/filters/SampleTurboFilter.java)
package chapters.filters;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
public class SampleTurboFilter extends TurboFilter {
String marker;
Marker markerToAccept;
@Override
public FilterReply decide(Marker marker, Logger logger, Level level,
String format, Object[] params, Throwable t) {
if (!isStarted()) {
return FilterReply.NEUTRAL;
}
if ((markerToAccept.equals(marker))) {
return FilterReply.ACCEPT;
} else {
return FilterReply.NEUTRAL;
}
}
public String getMarker() {
return marker;
}
public void setMarker(String markerStr) {
this.marker = markerStr;
}
@Override
public void start() {
if (marker != null && marker.trim().length() > 0) {
markerToAccept = MarkerFactory.getMarker(marker);
super.start();
}
}
}
このターボフィルターは、特定のマーカーが含まれているロギングイベントを受け付けます。マーカーが見つからなかったら、チェーン内の次のフィルターに引き継ぎます。
柔軟性を考慮して、チェックするマーカーを設定ファイルで指定できるよう、アクセサメソッドが定義されています。他にも、設定ファイルの解釈中に、指定されたオプションをチェックするため、start()メソッドを実装しています。
自作したターボフィルターを使う設定ファイルは次のとおりです。
例:基本的な自作TurboFilterの設定(logback-examples/src/main/java/chapters/filters/sampleTurboFilterConfig.xml)
<configuration>
<turboFilter class="chapters.filters.SampleTurboFilter">
<Marker>sample</Marker>
</turboFilter>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref="STDOUT" />
</root>
</configuration>
logback-classicの配布物にはいくつかTurboFilterの実装クラスが含まれています。MDCFilterを使うとMDC内の指定された値の存在をチェックすることができますし、DynamicThresholdFilterを使うとMDCのキーまたはレベルをしきい値でフィルタリングすることができます。また、MarkerFilterを使うとロギング要求に関連付けられた特定のマーカーの存在をチェックすることができます。
MDCFilterとMarkerFilterの両方を使う設定を見てみましょう。
例:MDCFilterとMarkerFilterの設定例(logback-examples/src/main/java/chapters/filters/turboFilters.xml)
<configuration>
<turboFilter class="ch.qos.logback.classic.turbo.MDCFilter">
<MDCKey>username</MDCKey>
<Value>sebastien</Value>
<OnMatch>ACCEPT</OnMatch>
</turboFilter>
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>billing</Marker>
<OnMatch>DENY</OnMatch>
</turboFilter>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console" />
</root>
</configuration>
次のコマンドを実行してみましょう。
java chapters.filters.FilterEvents src/main/java/chapters/filters/turboFilters.xml
前に見たように、FilterEventsアプリケーションは0〜9の連番を付けて10個のロギング要求を生成します。3番目と6番目を除く他のロギング要求のログレベルはINFOです。これはルートロガーに割り当てたログレベルと同じです。3番目のロギング要求のログレベルはDEBUGレベルで、これは有効レベルを下回っています。ですが、3番目のロギング要求を生成する直前に、MDCのキー"username"には値"sebastien"が設定され、直後に取り除かれています。そして、MDCFIlterはこのロギング要求だけを受け入れるようになっています。6番目のロギング要求はログレベルERRORで、かつ、"billing" というマーカーが指定されています。このロギング要求はMarkerFilter(二つ目のターボフィルター)によって拒否されます。
結果として、FilterEventsアプリケーションにturboFilters.xmlを指定した場合は次のように出力されます。
2006-12-04 15:17:22,859 [main] INFO chapters.filters.FilterEvents - logging statement 0 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 1 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 2 2006-12-04 15:17:22,875 [main] DEBUG chapters.filters.FilterEvents - logging statement 3 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 4 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 5 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 7 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 8 2006-12-04 15:17:22,875 [main] INFO chapters.filters.FilterEvents - logging statement 9
有効レベルはINFOなのに、3番目のロギング要求、つまり、ログレベルがDEBUGのロギング要求が出力されています。これは最初のTurboFilterが受け入れたからです。
また、6番目のロギング要求はログレベルがERRORなのに出力されていません。二つ目のTurboFilterのOnMatchプロパティにDENYが指定されていたからです。
DuplicateMessageFilter
DuplicateMessageFilterの利点は異なる見え方をします。メッセージの重複を検出し、一定回数以上繰り返す場合は、メッセージを破棄します。
繰り返しの検出は、単純に文字列が一致するかどうかを見ています。数文字違うだけでそれは別のメッセージとして扱われるので、重複メッセージとしては検出しません。たとえばこんな風に書いたとしましょう。
logger.debug("Hello "+name0);
logger.debug("Hello "+name1);
name0とname1が別の値だとしたら、これらのメッセージは別のものであるとみなされます。利用者のニーズによりますが、将来のリリースでは文字列の類似度をチェックすることになりそうです。完全に同一ではないけどよく似ているメッセージの繰り返しを排除するたmです。
ロギングメッセージに引数を指定している場合、書式化される前のメッセージが判定対象になるので注意してください。たとえば次の二つのロギング式のメッセージ部分はどちらも同じ "Hello {}." なので、これは重複メッセージと判定されます。
logger.debug("Hello {}.", name0);
logger.debug("Hello {}.", name1);
繰り返しを許容する回数はAllowedRepetitionsプロパティで指定します。allowedRepetitionsプロパティに1を指定した場合、最初のメッセージは出力されて、2番目のメッセージは破棄されます。同様に、2を指定したら、1番目、2番目のメッセージは出力されて、三番目以降のメッセージは破棄されます。デフォルトは5が設定されています。
繰り返しを検出するには、内部的に古いメッセージへの参照をキャッシュしておかなければなりません。このキャッシュのサイズはCacheSizeプロパティによって決まります。デフォルトは100(個)が設定されています。
例:DuplicateMessageFilterの設定例(logback-examples/src/main/java/chapters/filters/duplicateMessage.xml)
<configuration>
<turboFilter class="ch.qos.logback.classic.turbo.DuplicateMessageFilter"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console" />
</root>
</configuration>
FilterEventsアプリケーションにduplicateMessage.xmlを指定した場合の出力は次のようになります。
2008-12-19 15:04:26,156 [main] INFO chapters.filters.FilterEvents - logging statement 0 2008-12-19 15:04:26,156 [main] INFO chapters.filters.FilterEvents - logging statement 1 2008-12-19 15:04:26,156 [main] INFO chapters.filters.FilterEvents - logging statement 2 2008-12-19 15:04:26,156 [main] INFO chapters.filters.FilterEvents - logging statement 4 2008-12-19 15:04:26,156 [main] INFO chapters.filters.FilterEvents - logging statement 5 2008-12-19 15:04:26,171 [main] ERROR chapters.filters.FilterEvents - billing statement 6
"logging statement 0" は、書式化する前のメッセージ"logging statement {}" によって出力された最初のメッセージです。"logging statement 1"が1回目の繰り返し、"logging statement 2"が2回目の繰り返しとなります。3回目の繰り返しとなる"logging statement 3"はログレベルがDEBUGのはずなので、基本的な選択ルールによって破棄されました。つまり、ターボフィルターは基本的な選択ルールを含む他のフィルターに先駆けて呼び出されるということなのです。したがって、後続の処理チェインの中で破棄されてしまうのですが、DuplicateMessageFilterは"logging statement 3"を繰り返しメッセージだと判断したはずです。したがって "logging statement 4" は4回目の繰り返し、"logging statement 5" は5回目の繰り返しになります。デフォルトで許されている繰り返しは5回なので、"logging statement 5"より後は出てきませんでした。
logback-access モジュール
logback-access モジュールは logback-classic モジュールとほとんど同じ機能を提供します。具体的には、Filterオブジェクトはlogback-classic と同じように利用可能できますし、同じように動作します。一点だけ大きく違うころがあって、それはLoggingEventのインスタンスではなく AccessEventのインスタンスを使うということです。現時点では、logback-access の配布物に含まれているフィルターの数はそれほど多くありません。追加のフィルターを提案したいときは、logback-dev メーリングリスト宛に連絡してください。
CountingFilter
logback-access では、CountingFilterを使ってWebサーバへのアクセス統計情報を集めることができます。CountingFilterは、初期化時に実行プラットフォームの JMX サーバーに自身を MBean として登録します。その後は、MBean に統計情報を問い合わせることができるようになります。分平均、時間平均、日平均、週平均、月平均などです。他にも、集計単位の一つ前の情報と全体の合計を参照することができます。
CountingFilterを使用する設定ファイルを見てみましょう。
<configuration>
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<filter class="ch.qos.logback.access.filter.CountingFilter">
<name>countingFilter</name>
</filter>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%h %l %u %t \"%r\" %s %b</pattern>
</encoder>
</appender>
<appender-ref ref="STDOUT" />
</configuration>
CountingFilterの収集する統計情報は、例えばjconsoleから JMX サーバにアクセスして参照することができます。
EvaluatorFilter
EvaluatorFilterはEventEvaluatorをカプセル化した汎用的なフィルターです。名前が示すように、 EventEvaluatorは指定された条件を評価して、イベントがその条件を満たすかどうかを判定します。条件を満たす場合もそうでない場合も、EvaluatorFilterのonMatchプロパティ、または、onMismatchプロパティに指定された値を返します。EvaluatorFilterについてはlogback-classicモジュールの章で説明してあるので思い出してください(上記参照)。ここの説明は前の章に記載した内容の繰り返しです。
EventEvaluatorは抽象クラスです。
つまり、EventEvaluatorを継承すれば、独自のイベント評価ロジックを実装することができます。
logback-accessの配布物にはJaninoEventEvaluatorという実装クラスが含まれています。これは、booleanを返す任意のJava言語のブロックを評価するものです。私たちはこのJava言語で書かれた式のことを"Java評価式”と呼んでいます。
Java評価式を使うとロギングイベントを柔軟にフィルタリングできるようになります。
JaninoEventEvaluatorを使用するにはJaninoライブラリが必要です。
設定方法は設定ドキュメントの対応するセクションを参照してください。
Java評価式は設定ファイルを解釈する間にコンパイルされます。 どのように呼び出すのか、利用者は気にすることはありません。 ですが、Java言語の式が真偽値を返すものであることを保証するのは利用者の責任です。
Java評価式は一度に1つのイベントを扱います。logback-accessは、AccessEventをeventという名前の変数として公開します。event変数を介して、HTTPリクエストやHTTP応答に関連付けられたさまざまなデータを参照することができます。正確なところはAccessEventクラスのソースコードを読んでください。
次の設定ファイルでは、応答コード404(Not Found)をひっかけます。つまり、応答コードが404になったHTTPリクエストをすべてコンソールに出力するのです。
例:Access Evaluator(logback-examples/src/main/java/chapters/filters/accessEventEvaluator.xml)
<configuration>
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>event.getStatusCode() == 404</expression>
</evaluator>
<onMismatch>DENY</onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b</pattern></encoder>
</appender>
<appender-ref ref="STDOUT" />
</configuration>
次の設定ファイルでは、やはり404エラーをひっかけているのですが、CSSファイルを要求したものだけをひっかけています。
例6.10:Access Evaluator(logback-examples/src/main/java/chapters/filters/accessEventEvaluator2.xml)
<configuration>
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator name="Eval404">
<expression>
(event.getStatusCode() == 404)
&& <!-- ampersand characters need to be escaped -->
!(event.getRequestURI().contains(".css"))
</expression>
</evaluator>
<onMismatch>DENY</onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b</pattern></encoder>
</appender>
<appender-ref ref="STDOUT" />
</configuration>