java.util.loggingの使い方

# 基本的な使い方
まずは基本的な使い方です。主な登場実物は以下の3つです。
1. Logger - ログを出力するオブジェクト。
2. Handler - ログの出力先を制御するオブジェクト。
3. Formatter - ログのフォーマットを制御するオブジェクト。

初めにLoggerインスタンスを作成します。

```
Logger logger = Logger.getLogger("loggerの名前")
```

次にHandlerインスタンスを作成し、loggerに登録します。Handlerは複数個登録できます。
今回は`C:\sample\sample.log`を出力先としたHandlerを作成します。ファイルを出力先とする場合は`Handler`インターフェースの実装クラス`FileHandler`を利用します。

```
Handler handler = new FileHandler("C:\\sample\\sample.log");
logger.addHandler(handler);
```

最後にFormatterインスタンスを作成し、Handlerに登録します。
今回は人が読みやすい形にフォーマットするため、`SimpleFormatter`を利用します。`SimpleFormatter`は`Formatter`インターフェースの実装クラスです。

```
Formatter formatter =  new SimpleFormatter();
handler.setFormatter(formatter);
```

これで準備は完了です。loggerにメッセージを渡してログを出力します。
```
logger.log(Level.INFO, "メッセージ");
```

上記にある通り、ログを出力する際はメッセージのログレベルを指定します。
java.util.loggingで用意しているログレベルは以下の7種類です。下にいくほど重大なログとなります。

- FINEST - 非常に詳細なトレースメッセージ
- FINER - かなり詳細なトレースメッセージ
- FINE - 詳細なトレースメッセージ
- CONFIG - 静的な構成メッセージ
- INFO - 情報メッセージ
- WARNING - 警告メッセージ
- SEVERE - 重大なメッセージ

loggerに一定のログレベル以上のメッセージのみ出力するように設定することが出来ます。
例えばINFOレベル以上のメッセージのみ出力したい場合は以下のように設定します。

```
logger.setLevel(Level.INFO);
```

基本的な使い方のサンプルプログラムを以下に記載します。

```java:BasicLoggingSample
package sample;

import java.io.IOException;
import java.util.function.Supplier;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class BasicLoggingSample {

	public static void main(String[] arg) throws SecurityException, IOException {
		// ロガーを取得してログレベルをINFOに設定
		Logger logger = Logger.getLogger(BasicLoggingSample.class.getName());
		logger.setLevel(Level.INFO);

		// ハンドラーを作成してロガーに登録
		Handler handler = new FileHandler("C:\\sample\\sample.log");
		logger.addHandler(handler);
		
		// フォーマッターを作成してハンドラーに登録
		Formatter formatter =  new SimpleFormatter();
		handler.setFormatter(formatter);
		
		// INFOメッセージを出力
		logger.log(Level.INFO, "INFOメッセージ");

		// それぞれのログレベルのメッセージを出力する簡易メソッドが用意されています。
		logger.finest("FINESTメッセージ");
		logger.finer("FINERメッセージ");
		logger.fine("FINEメッセージ");
		logger.config("CONFIGメッセージ");
		logger.info("INFOメッセージ");
		logger.warning("WARNINGメッセージ");
		logger.severe("SEVEREメッセージ");
		
		// メッセージは文字列で渡す方法の他に、Supplier<String>を渡す方法もあります。
		Supplier<String> supplier = new Supplier<String>() {
			@Override
			public String get() {
				return "Supplyメッセージ";
			}
		};
		logger.info(supplier);
		
		// Exceptionが発生した時のログ出力方法は以下の通り。引数に渡されたThrowableのスタックトレースが出力されます。
		logger.log(Level.WARNING, "エラーが発生しました。", new RuntimeException("ランタイムエラー"));
	}
}
```

# ロガーの階層構造

ロガーは名前空間を基にした階層構造を持っています。例えば以下のようなロガーを作成した場合、

```
Logger logger = Logger.getLogger("com.sample.App");
```

階層構造は以下のようになります。`com.sample.App`の親ロガーは`com.sample`となり、`com.sample`の親ロガーは`com`となります。さらに、`com`の親ロガーはルートロガーとなります。

```
Root
└─com
    └─sample
        └─App
```

ロガーがメッセージを出力した場合、その出力は親ロガーにも伝播します。つまり上記の例で言うと`com.sample.App`のロガーへメッセージを出力した場合、`com.sample`、`com`、そしてルートロガーからもそれぞれメッセージが出力されます。

言葉で説明してもピンとこないと思いますのでサンプルプログラムを見てみましょう。

```java:NameSpaceSample
package sample;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class NameSpaceSample {

	public static void main(String[] arg) throws SecurityException, IOException {
		// それぞれのロガーを取得します。ルートロガーを取得するにはロガー名に空文字を指定します。
		Logger root = Logger.getLogger("");
		Logger com = Logger.getLogger("com");
		Logger sample = Logger.getLogger("com.sample");
		Logger app = Logger.getLogger("com.sample.App");
		
		// com.sample.Appの親はcom.sample、com.sampleの親はcom、comの親はルートロガーです。
		System.out.println(app.getParent() == sample);
		System.out.println(app.getParent().getParent() == com);
		System.out.println(app.getParent().getParent().getParent() == root);
		
		// ルートロガーの親はnullとなります。
		System.out.println(root.getParent());
		
		// ロガーそれぞれにハンドラーを設定します。
		Formatter formatter = new SimpleFormatter();
		Handler rootHandler = new FileHandler("C:\\sample\\root.log");
		root.addHandler(rootHandler);
		rootHandler.setFormatter(formatter);
		
		Handler comHandler = new FileHandler("C:\\sample\\com.log");
		com.addHandler(comHandler);
		comHandler.setFormatter(formatter);
		
		Handler sampleHandler = new FileHandler("C:\\sample\\sample.log");
		sample.addHandler(sampleHandler);
		sampleHandler.setFormatter(formatter);
		
		Handler appHandler = new FileHandler("C:\\sample\\App.log");
		app.addHandler(appHandler);
		appHandler.setFormatter(formatter);
		
		// appでメッセージを出力すると親ロガーそれぞれからもメッセージが出力されます。
		app.info("INFOメッセージ");
	}
}
```

サンプルプログラムを実行すると、`C:\sample`ディレクトリ配下の`root.log`、`com.log`、`sample.log`、`App.log`それぞれにログが出力されます。

# フィルターの使い方

ロガーにログレベルを設定して一定のログレベル以上のメッセージのみを出力するように制御する方法を紹介しましたが、より詳細な制御を行いたい場合は`Filter`クラスを利用します。

```
Filter filter = new Filter() {
	@Override
	public boolean isLoggable(LogRecord record) {
		return record.getMessage().contains("処理件数=");
	}
};
logger.setFilter(filter);
```

上記のようにFilterを設定した場合、メッセージに`"処理件数="`を含むログのみが出力されるようになります。

# ハンドラーにログレベルとフィルターを設定する

上の説明でロガーにログレベル・フィルターを設定する方法を紹介しました。
実は、ログレベルとフィルターは`Handler`にも設定することが出来ます。

サンプルプログラムを以下に記載します。

```java:FiltringSample
package sample;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Filter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class FiltringSample {
	
	public static void main(String[] arg) throws SecurityException, IOException {
		// ロガーを作成し、ハンドラーとフォーマッターを設定
		Logger logger = Logger.getLogger("sample1");
		Handler handler = new FileHandler("C:\\sample\\sample.log");
		logger.addHandler(handler);
		handler.setFormatter(new SimpleFormatter());
		
		// ロガーのログレベルをINFOに設定
		logger.setLevel(Level.INFO);
		logger.info("サンプルメッセージ");                          // 出力される
		
		// ロガーにフィルターを設定
		Filter filter1 = new Filter() {
			@Override
			public boolean isLoggable(LogRecord record) {
				return record.getMessage().contains("処理件数=");
			}
		};
		logger.setFilter(filter1);
		logger.info("サンプルメッセージ");                          // 出力されない
		logger.info("サンプルメッセージ, 処理件数=1");               // 出力される
		
		// ハンドラーにフィルターを設定
		Filter filter2 = (record) -> record.getMessage().contains("更新件数=");
		handler.setFilter(filter2);
		logger.info("サンプルメッセージ");                          // 出力されない
		logger.info("サンプルメッセージ, 処理件数=1");               // 出力されない
		logger.info("サンプルメッセージ, 処理件数=1, 更新件数=1");    // 出力される
		
		// ハンドラーにログレベルを設定
		handler.setLevel(Level.WARNING);
		logger.info("サンプルメッセージ");                          // 出力されない
		logger.info("サンプルメッセージ, 処理件数=1");               // 出力されない
		logger.info("サンプルメッセージ, 処理件数=1, 更新件数=1");    // 出力されない
		logger.warning("警告メッセージ, 処理件数=1, 更新件数=1");   // 出力される
	}
}
```

上記を実行すると、ロガー及びハンドラーへ設定したログレベルとフィルターによってログ出力が制限されていることが分かります。

# ロガーの階層構造におけるログ出力制御

ロガーには階層構造があること、そしてロガーへメッセージを出力すると親ロガーへ伝播することを上で紹介しました。
実はこの伝播の仕組みには罠があって、きちんと仕組みを理解していないとドツボに嵌る可能性があります。（私が嵌りました。。）
言葉で説明しても分かりにくいと思いますが一旦言葉で説明すると、親ロガーへの伝播の仕様は以下のようになっています。

- ロガーへ設定したログレベル・フィルターのチェックは、直接呼び出されたロガーでのみ行われる。
- ハンドラーへ設定したログレベル・フィルターのチェックは、親ロガーのハンドラーも含めて全てのハンドラーでチェックされる。

はい、わかりにくいと思います。これについてはロジックを直接見た方が早く理解できると思います。
ログ出力のロジックはざっくり言うと以下の図のようになっています。

![フローチャート.png](https://qiita-image-store.s3.amazonaws.com/0/123980/0658b1aa-90b1-cfc2-a0ad-cc54e470e240.png)

分かりましたでしょうか。
ロガーへ設定したログレベル・フィルターのチェックは呼び出しを受けた最初のロガーでのみチェックされるのに対して、
ハンドラーへ設定したログレベル・フィルターのチェックは全てのハンドラーで行われています。

これを確認するサンプルを2つ、以下に記述します。

```java:NameSpaceSample2
package sample;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class NameSpaceSample2 {

	public static void main(String[] arg) throws SecurityException, IOException {
		// ロガーを3つ取得。ハンドラーの設定、フォーマッターの設定を行う。
		Logger root = Logger.getLogger("");
		Logger sample = Logger.getLogger("sample");
		Logger loggerTreeSample = Logger.getLogger("sample.NameSpaceSample2");
		Handler rootHandler = new FileHandler("C:\\sample\\root.log");
		Handler sampleHandler = new FileHandler("C:\\sample\\sample.log");
		Handler NameSpaceSample2Handler = new FileHandler("C:\\sample\\NameSpaceSample2.log");
		Formatter formatter = new SimpleFormatter();
		rootHandler.setFormatter(formatter);
		sampleHandler.setFormatter(formatter);
		NameSpaceSample2Handler.setFormatter(formatter);
		root.addHandler(rootHandler);
		sample.addHandler(sampleHandler);
		loggerTreeSample.addHandler(NameSpaceSample2Handler);
		
		// ロガーへログレベルの設定を行う。
		root.setLevel(Level.WARNING);
		sample.setLevel(Level.INFO);
		loggerTreeSample.setLevel(Level.FINE);

		// ハンドラーは全てのログレベルを許可するように設定。（Level.ALLを設定すると全てのログレベルのメッセージの出力を許可します。）
		rootHandler.setLevel(Level.ALL);
		sampleHandler.setLevel(Level.ALL);
		NameSpaceSample2Handler.setLevel(Level.ALL);
		
		// ログを出力
		root.fine("rootより発信. FINEメッセージ");
		root.info("rootより発信. INFOメッセージ");
		root.warning("rootより発信. WARNINGメッセージ");
		sample.fine("sampleより発信. FINEメッセージ");
		sample.info("sampleより発信. INFOメッセージ");
		sample.warning("sampleより発信. WARNINGメッセージ");
		loggerTreeSample.fine("NameSpaceSample2Handlerより発信. FINEメッセージ");
		loggerTreeSample.info("NameSpaceSample2Handlerより発信. INFOメッセージ");
		loggerTreeSample.warning("NameSpaceSample2Handlerより発信. WARNINGメッセージ");
	}
}
```

上記を実行すると、`root.setLevel(Level.WARNING);`と設定しているにもかかわらず、`loggerTreeSample.fine(...)`で出力したメッセージが`C:\\sample\\root.log`へ出力されていることが確認できます。

サンプルをもう一つ記載します。上のサンプルとの違いは、ログレベルの設定部分のみです。

```java:NameSpaceSample3
package sample;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class NameSpaceSample3 {

	public static void main(String[] arg) throws SecurityException, IOException {
		// ロガーを3つ取得。ハンドラーの設定、フォーマッターの設定を行う。
		Logger root = Logger.getLogger("");
		Logger sample = Logger.getLogger("sample");
		Logger loggerTreeSample = Logger.getLogger("sample.NameSpaceSample2");
		Handler rootHandler = new FileHandler("C:\\sample\\root.log");
		Handler sampleHandler = new FileHandler("C:\\sample\\sample.log");
		Handler NameSpaceSample3Handler = new FileHandler("C:\\sample\\NameSpaceSample3.log");
		Formatter formatter = new SimpleFormatter();
		rootHandler.setFormatter(formatter);
		sampleHandler.setFormatter(formatter);
		NameSpaceSample3Handler.setFormatter(formatter);
		root.addHandler(rootHandler);
		sample.addHandler(sampleHandler);
		loggerTreeSample.addHandler(NameSpaceSample3Handler);
		
		// ロガーは全てのログレベルを許可するように設定。（Level.ALLを設定すると全てのログレベルのメッセージの出力を許可します。）
		root.setLevel(Level.ALL);
		sample.setLevel(Level.ALL);
		loggerTreeSample.setLevel(Level.ALL);

		// ハンドラーへログレベルの設定を行う。
		rootHandler.setLevel(Level.WARNING);
		sampleHandler.setLevel(Level.INFO);
		NameSpaceSample3Handler.setLevel(Level.FINE);
		
		// ログを出力
		root.fine("rootより発信. FINEメッセージ");
		root.info("rootより発信. INFOメッセージ");
		root.warning("rootより発信. WARNINGメッセージ");
		sample.fine("sampleより発信. FINEメッセージ");
		sample.info("sampleより発信. INFOメッセージ");
		sample.warning("sampleより発信. WARNINGメッセージ");
		loggerTreeSample.fine("NameSpaceSample3Handlerより発信. FINEメッセージ");
		loggerTreeSample.info("NameSpaceSample3Handlerより発信. INFOメッセージ");
		loggerTreeSample.warning("NameSpaceSample3Handlerより発信. WARNINGメッセージ");
	}
}
```

上記を実行すると、`C:\\sample\\root.log`へWARNINGレベルのログのみが出力されていることが確認できます。

# 設定ファイルの読み込み

今までのサンプルプログラムを見て感じていたかもしれませんが、ロガーやハンドラーを設定するために大量のコードを書くのは面倒ですよね。
やはり、設定の部分はプログラムの外に追い出して、設定ファイルに記載したいものです。
そこで次に紹介するのが設定ファイルを外部から読み込む方法です。

設定ファイルをプログラムから読み込む方法は以下の通りです。今回はサンプルプログラムの同じクラスパスに配置している`logging.properties`を読み込むようにしました。
設定ファイルは`java.util.Properties`形式で読み込む必要があります。

```
LogManager.getLogManager().readConfiguration(SettingFileSample.class.getResourceAsStream("logging.properties"));
```

設定ファイルのサンプルは以下の通りです。詳しい説明は`{java.home}\lib\logging.properties`にも記載されていますのでご参照ください。

```properties:logging.properties
# ルートロガーのハンドラー・ログレベルの設定 ConsoleHandlerは標準エラー(System.err)へログを出力します。
handlers=java.util.logging.ConsoleHandler
.level=INFO

# サンプルプログラムで利用するロガー"sample.SettingFileSample"の設定
sample.SettingFileSample.level=FINE
sample.SettingFileSample.handlers=java.util.logging.FileHandler

# ConsoleHandlerの設定
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

# FileHandlerの設定
java.util.logging.FileHandler.pattern=C:/sample/sample.log
java.util.logging.FileHandler.limit=1000
java.util.logging.FileHandler.count=1
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
```

上記の設定ファイルを利用したサンプルプログラムを以下に記載します。

```java:SettingFileSample
package sample;

import java.io.IOException;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class SettingFileSample {

	public static void main(String[] arg) throws SecurityException, IOException {
		LogManager.getLogManager().readConfiguration(SettingFileSample.class.getResourceAsStream("logging.properties"));

		Logger logger = Logger.getLogger("sample.SettingFileSample");
		logger.finer("FINERメッセージ");
		logger.fine("FINEメッセージ");
		logger.info("INFOメッセージ");
	}
}
```

ロガーやハンドラーの設定がなくなってすっきりしましたね。

# ロガーでResourceBundleを利用する方法

java.util.loggingはResourceBundleを利用した国際化対応も行えます。
サンプルプログラムを以下に記載します。サンプルでは、Javaクラスと同じクラスパスにある`resource_ja.properties`と`resource_en.properties`を利用しています。

```java:ResourceBundleSample
package sample;

import java.util.Locale;
import java.util.ResourceBundle;
import java.util.logging.Logger;

public class ResourceBundleSample {

	public static void main(String[] arg) {
		Logger lggr = Logger.getLogger(ResourceBundleSample.class.getName());
		lggr.setResourceBundle(ResourceBundle.getBundle("sample.resource", Locale.JAPANESE));
		lggr.info("ID1");
		
		lggr.setResourceBundle(ResourceBundle.getBundle("sample.resource", Locale.ENGLISH));
		lggr.info("ID1");
	}
}
```

```properties:resource_ja.properties
ID1=日本語メッセージ
```

```properties:resource_en.properties
ID1=english message
```

サンプルプログラムを実行すると、1つ目のログは`日本語のメッセージ`、2つ目のログは`english message`と出力されます。

# ロガーでセキュリティマネージャーを利用する方法

セキュリティマネージャーを有効にすると、ロガーの設定をプログラムから変更できなくなります。
以下のサンプルプログラムをセキュリティマネージャーを有効にして実行すると`AccessControlException`が実行されます。

```java:LoggingPermissionSample
package sample;

import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggingPermissionSample {
	
	public static void main(String[] arg) {
		Logger lg = Logger.getLogger("");
		lg.setLevel(Level.FINE);
		lg.fine("fine message");
		lg.info("info message");
	}
}
```

セキュリティマネージャーを有効にしている時にロガーの変更を許可するためには、policyファイルに以下のように記載します。詳しくは`LoggerPermission`のJavadocに記載されていますのでご参照ください。

```policy:java.policy
grant {
        permission java.util.logging.LoggingPermission "control", "";
}
```

なお、セキュリティマネージャーを有効にするには以下のようにVM引数に`java.security.manager`を指定します。
また、policyファイルを明示的に指定するには以下のようにVM引数に`java.security.policy`を指定します。

```
java -Djava.security.manager -Djava.security.policy=policyファイルのパス sample.LoggingPermissionSample
```