8 Javaロギングの概要

JavaロギングAPIはjava.util.loggingパッケージに含まれており、エンド・ユーザー、システム管理者、フィールド・サービス・エンジニアおよびソフトウェア開発チームが分析するためのログ・レポートを作成することで、顧客サイトでのソフトウェアのサービスや保守を簡単にできるようにします。Logging APIでは、アプリケーションまたはプラットフォームで発生したセキュリティの失敗、構成エラー、パフォーマンス・ボトルネック、バグなどの情報を取り込みます。

コア・パッケージには、プレーン・テキストまたはXMLフォーマット・ログ・レコードをメモリー、出力ストリーム、コンソール、ファイル、およびソケットに配信するためのサポートが含まれます。ロギングAPIには、ホスト・オペレーティング・システム上の既存のログ・サービスと対話する機能もあります。

制御フローの概要

アプリケーションはLoggerオブジェクトでロギング呼出しを行います。Loggerオブジェクトは階層構造を持つ名前空間に編成され、子Loggerオブジェクトは名前空間における親からロギング・プロパティの一部を継承できます。

Loggerオブジェクトでは、パブリッシュ用にHandlerオブジェクトに渡されるLogRecordオブジェクトを割り当てます。LoggerオブジェクトもHandlerオブジェクトも、ロギングLevelオブジェクトと、場合によってはFilterオブジェクトを使用して、特定のLogRecordオブジェクトと関係があるかどうかを判断します。LogRecordオブジェクトを外部に公開する必要がある場合、HandlerオブジェクトはI/Oストリームに公開する前に、Formatterオブジェクトを使用してメッセージをローカライズしたりフォーマットできます。

図8-1 Javaロギング制御フロー

この図は、前述の段落を示しています。

Loggerオブジェクトが、一連の出力Handlerオブジェクトを追跡します。デフォルトでは、すべてのLoggerオブジェクトが自分の親Loggerに出力を送信します。より高い階層にあるHandlerオブジェクトは無視するようにLoggerオブジェクトを設定することもできます。

一部のHandlerオブジェクトの出力先が他のHandlerオブジェクトになる場合があります。たとえば、MemoryHandlerLogRecordオブジェクトの内部リング・バッファを保持し、トリガー・イベントでターゲットHandlerを通してLogRecordオブジェクトを公開します。そのような場合、チェーン内の最後のHandlerで、なんらかのフォーマットが実行されます。

図8-2 MemoryHandlerによるJavaロギング制御フロー

この図は、前述の段落を示しています。

APIは、ロギングが無効になっている場合、Logger APIの呼出しコストを抑えるように構成されています。あるログ・レベルでロギングが無効になっている場合、Loggerは低コストの比較テストを行なって結果を返すことができます。あるログ・レベルでロギングが有効になっている場合も、Loggerはコストを抑えるように注意しながらHandlerLogRecordを渡します。特にローカライズとフォーマットは比較的コストが高いため、Handlerからの要求があるまでは実行されません。たとえば、MemoryHandlerはフォーマット・コストを払わずにLogRecordオブジェクトの循環バッファを維持できます。

ログ・レベル

各ログ・メッセージには、ログLevelオブジェクトが関連付けられています。Levelによって、ログ・メッセージの重要度と緊急度がほぼわかります。ログLevelオブジェクトは整数値をカプセル化し、この値が高いほど、高い優先度を示します。

Levelクラスでは、FINEST (最低の優先度、値は最小)からSEVERE (最高の優先度、値は最大)まで7つの標準ログ・レベルが定義されています。

ロガー

前述のように、クライアント・コードはLoggerオブジェクトにログ要求を送信します。各ロガーは関係のあるログ・レベルを追跡し、このレベル以下のログ要求を破棄します。

通常、Loggerオブジェクトは名前の付いたエンティティであり、java.awtのようにドット区切りの名前を使用します。名前空間には階層があり、LogManagerによって管理されています。多くの場合、名前空間はJavaパッケージ名前空間に対応していますが、正確に対応する必要はありません。たとえば、java.awtというLoggerはjava.awtパッケージ内のクラスのロギング要求を処理しますが、java.awtパッケージで定義された、クライアントから見える抽象化をサポートするsun.awtのクラスのロギングを処理することもあります。

名前付きLoggerオブジェクト以外に、共有名前空間に含められない匿名Loggerオブジェクトを作成することもできます。セキュリティの項を参照してください。

各Loggerは、ロギングの名前空間における親ロガーを追跡します。ロガーの親とは、ロギングの名前空間に現存するもっとも近い祖先のことです。ルート・ロガーの名前は""で、ルート・ロガーに親はありません。すべての匿名ロガーにとって、ルート・ロガーが親となります。Loggerは、ロガーの名前空間における親からさまざまな属性を継承できます。特に、次のような属性を継承できます。

  • ロギング・レベル: ロガーのレベルがnullに設定されている場合、このロガーはツリーをさかのぼって最初に見つかったnull以外のLevelを使用する有効なLevelを使用します。

  • ハンドラ: デフォルトでは、Loggerは自分の親のハンドラにすべての出力メッセージを送信します。こうして、次々にツリーをさかのぼって送信が行われます。

  • リソース・バンドル名: ロガーのリソース・バンドル名がnullである場合、このロガーは親のリソース・バンドル名を継承します。こうして、次々にツリーをさかのぼって継承が行われます。

ロギング・メソッド

Loggerクラスは、ログ・メッセージを生成するための簡易メソッドを数多く提供します。ロギング・レベルごとにメソッドがあり、便宜上、ロギングのレベル名に対応しています。このため、開発者はlogger.log(Level.WARNING, ...)ではなく、簡易メソッドlogger.warning(...)を呼び出すことができます。

ロギング・メソッドには2種類のスタイルがあり、さまざまなユーザーの要求に対応しています。

1つ目は、明示的なソース・クラス名とソース・メソッド名を取るメソッドです。このメソッドは、特定のロギング・メッセージのソースをすばやくつきとめる必要がある開発者向けです。次にこのスタイルの例を示します。

void warning(String sourceClass, String sourceMethod, String msg);

2つ目は、明示的なソース・クラス名とソース・メソッド名を取らないメソッドです。これは、簡単なロギングを使用するだけで、詳細なソース情報を必要としない開発者向けです。

void warning(String msg);

2つ目のメソッドでは、ロギング・フレームワークは呼出し元のクラスとメソッドの識別をベスト・エフォートで行い、この情報をLogRecordに追加します。ただし、自動的に推測されたこの情報は概略に過ぎないことを理解しておく必要があります。仮想マシンでは、just-in-timeコンパイルの際に大規模な最適化を行ってスタック・フレームをすべて削除するため、呼出し元のクラスとメソッドを確実に検出することは不可能となっています。

ハンドラ

Java SEには、次のようなHandlerクラスがあります。

  • StreamHandler: フォーマット済レコードをOutputStreamに書き込む単純なハンドラ。

  • ConsoleHandler: フォーマット済レコードをSystem.errに書き込む単純なハンドラ

  • FileHandler: フォーマット済みログ・レコードを1つのファイルまたはログ・ファイルのローテーション・セットに書き込むハンドラ。

  • SocketHandler: フォーマット済みログ・レコードをリモートのTCPポートに書き込むハンドラ。

  • MemoryHandler: ログ・レコードをメモリーにバッファリングするハンドラ。

新しいHandlerクラスを開発するのは比較的簡単です。特殊な機能が必要な開発者は、ハンドラをゼロから開発することも、提供されたハンドラの1つをサブクラス化することもできます。

フォーマッタ

Java SEには、次の2つの標準Formatterクラスがあります。

  • SimpleFormatter: ログ・レコードのサマリーを「人間が読めるように」記述します。

  • XMLFormatter: 詳細なXML構成の情報を記述します。

ハンドラと同様に、新しいフォーマッタの開発は比較的簡単です。

LogManager

グローバル・ロギング情報を追跡するグローバルなLogManagerオブジェクトがあります。次のものが含まれます。

  • 名前付きロガーの階層名前空間。

  • 構成ファイルから読み取ったロギング制御プロパティ。構成ファイルの項を参照してください。

static LogManager.getLogManagerメソッドを使用して取得できる単一LogManagerオブジェクトがあります。これは、LogManagerの初期化中にシステム・プロパティに基づいて作成されます。このプロパティを使用すると、コンテナ・アプリケーション(EJBコンテナなど)はLogManagerの独自のサブクラスをデフォルト・クラスと置き換えることができます。

構成ファイル

ロギング構成は、起動時に読み取られるロギング構成ファイルで初期化できます。このロギング構成ファイルは、標準のjava.util.Properties形式です。

また、初期化プロパティの読取りに使用するクラスを指定してロギング構成を初期化することもできます。このメカニズムを使用すると、LDAPやJDBCなどの任意のソースから構成データを読み取ることができます。

グローバル構成情報の量はわずかです。これはLogManagerクラスの記述に明記されており、起動時にインストールするルート・レベルのハンドラのリストが含まれています。

初期構成で名前付きロガーのレベルを指定することもできます。これらのレベルは、その名前付きロガーおよび名前階層でその下にあるすべてのロガーに適用されます。レベルは、構成ファイルで定義した順に適用されます。

初期構成には、ハンドラやロギングを行うサブシステムが使用する任意のプロパティが含まれます。便宜上、これらのプロパティには、ハンドラ・クラスの名前やサブシステムのメインLogger名で始まる名前を使用します。

たとえば、MemoryHandlerjava.util.logging.MemoryHandler.sizeプロパティを使用してリング・バッファのデフォルト・サイズを決定します。

デフォルトの構成

JDKの出荷時に設定されているデフォルトのロギング構成は単なるデフォルトであるため、ISV、システム管理者およびエンド・ユーザーがオーバーライドできます。このファイルは、java-home/conf/logging.propertiesに格納されています。

デフォルトの構成は、かぎられたディスク容量だけを利用します。処理できないほど情報量が多くなることはありませんが、重要な不具合情報は必ず取り込みます。

デフォルトの構成では、ルート・ロガーのハンドラの1つがコンソールへの出力用として設定されます。

構成の動的更新

プログラマは、さまざまな方法で実行時にロギング構成を更新できます。

  • FileHandlerMemoryHandlerおよびConsoleHandlerオブジェクトは、どれも様々な属性を使用して作成できます。

  • 新しいHandlerオブジェクトを追加したり、古いハンドラを削除できます。

  • 新しいLoggerオブジェクトを作成し、特定のHandlerを供給できます。

  • LevelオブジェクトをターゲットHandlerオブジェクトに設定できます。

ネイティブ・メソッド

ロギングにはネイティブAPIがありません。

ネイティブ・コードでJavaロギング・メカニズムを使う場合は、通常のJNI呼出しでJavaロギングAPIを呼び出す必要があります。

XML DTD

XMLFormatterで使用されるXML DTDの詳細は、付録A: XMLFormatter出力のDTDを参照してください。

このDTDでは、トップ・レベルのドキュメントとして<log>要素が設計されます。次にそれぞれのログ・レコードが<record>要素として記述されます。

JVMがクラッシュすると、</log>XMLFormatterストリームを正しく閉じてきちんと終了できないことがあります。そのため、ログ・レコードを分析するツールを用意して、終了していないストリームに対処する必要があります。

一意のメッセージID

JavaロギングAPIは、一意のメッセージIDを直接にはサポートしません。一意のメッセージIDを必要とするアプリケーションやサブシステムは、独自の規則を定義してメッセージ文字列に適切な一意のIDを付与する必要があります。

セキュリティ

セキュリティの第一要件は、信頼されていないコードがロギング構成を変更できないようにすることです。特に、特定のカテゴリの情報を特定のHandlerに記録するようにロギング構成を設定した場合は、信頼されていないコードがロギングを妨害したり中断したりできないようにする必要があります。

セキュリティ権限LoggingPermissionは、ロギング構成の更新を制御します。

信頼されているアプリケーションには適切なLoggingPermissionが与えられ、任意のロギング構成APIを呼び出すことができます。ただし、これは信頼されていないアプレットには当てはまりません。信頼されていないアプレットは、名前付きロガーを通常の方法で作成して使用できますが、ロギング制御設定を変更してハンドラの追加や削除を行ったり、ログ・レベルを変更することはできません。ただし、信頼されていないアプレットは、Logger.getAnonymousLoggerを使用して独自の匿名ロガーを作成して使用できます。このような匿名ロガーはグローバル名前空間には登録されません。また匿名ロガーのメソッドのアクセスはチェックされないので、信頼されていないコードであってもそうしたメソッドのロギング制御設定を変更できます。

ロギング・フレームワークは不正行為を防止しようとはしません。ロギング呼出しのソースは確実に識別できるとはかぎらないので、特定のソース・クラスとソース・メソッドからのものであるというLogRecordが公開されても、偽物であることがあります。同様に、XMLFormatterなどのフォーマッタも、メッセージ文字列内の入れ子のログ・メッセージに対して自身を保護しようとしません。このように、偽のLogRecordはメッセージ文字列内に偽のXMLを含んでいることがあり、出力時に別のXMLレコードがあるように見えることがあります。

また、ロギング・フレームワークはサービス妨害攻撃に対して自身を保護しようとはしません。任意のロギング・クライアントがロギング・フレームワークを意味のないメッセージであふれさせ、重要なログ・メッセージを隠すことができます。

構成管理

APIは、構成情報の初期セットを構成ファイルからプロパティとして読み取るように構成されています。次に構成情報はさまざまなロギング・クラスやオブジェクトを呼び出して、プログラムによって変更できます。

さらに、LogManagerには構成ファイルを再読取りできるメソッドがあります。再読込みを行うと、構成ファイルの値がプログラムが行なった変更をオーバーライドします。

パッケージ化

すべてのロギング・クラスは、java.util.loggingパッケージにある名前空間のjava.*の部分にあります。

ローカライズ

ログ・メッセージをローカライズする必要のある場合があります。

各ロガーには、ResourceBundle名が関連付けられている場合があります。対応するResourceBundleを使用して、原文のメッセージ文字列とローカライズするメッセージ文字列をマッピングできます。

通常、フォーマッタはローカライズを実行します。便宜上、Formatterクラスは基本的なローカリゼーションとフォーマットをサポートするformatMessageメソッドを提供します。

リモート・アクセスと直列化

ほとんどのJavaプラットフォームAPIでは、ロギングAPIは単一アドレス空間で使用する設計になっています。呼出しはすべてローカルとなります。ただし、出力を他のシステムに転送しようとするハンドラがあることも考えられます。次のようなさまざまな方法でこれを行うことができます。

SocketHandlerなど、XMLFormatterを使用して他のシステムにデータを書き込むハンドラがあります。これにより、さまざまなシステムで構文解析と処理が可能な簡単で標準的な交換可能なフォーマットが提供されます。

RMIでLogRecordオブジェクトを渡すハンドラもあります。したがって、LogRecordクラスはシリアライズ可能です。ただし、LogRecordパラメータをどのように扱うかという問題があります。直列化できないパラメータがある一方で、ロギングに必要とされる以上の状態に直列化するパラメータもあるからです。この問題を回避するため、LogRecordクラスには、Object.toString()でパラメータを文字列に変換してから書き出すカスタムのwriteObjectメソッドが用意されています。

ほとんどのロギング・クラスは、直列化可能にはなっていません。ロガーもハンドラも、特定の仮想マシンに結び付けられたステートフル・クラスです。この点では、どちらもjava.ioクラスと似ています。このクラスもシリアライズできません。

Javaロギングの例

簡単な用法

デフォルト設定を使ってロギングを実行する小さなプログラムを次に示します。

このプログラムは、構成ファイルに基づいてLogManagerが確立したルート・ハンドラに依存します。これは、独自のLoggerオブジェクトを作成し、このLoggerオブジェクトを呼び出して様々なイベントをレポートします。

package com.wombat;
import java.util.logging.*;

public class Nose {
    // Obtain a suitable logger.
    private static Logger logger = Logger.getLogger("com.wombat.nose");
    public static void main(String argv[]) {
        // Log a FINE tracing message
        logger.fine("doing stuff");
        try {
            Wombat.sneeze();
        } catch (Exception ex) {
            // Log the exception
            logger.log(Level.WARNING, "trouble sneezing", ex);
        }
        logger.fine("done");
    }
}

構成の変更

ロギング構成を動的に調整して特定のファイルに出力を送信し、wombatに関する多くの情報を取得する小さなプログラムを次に示します。パターン%tは、システムの一時ディレクトリを表します。

public static void main(String[] args) {
    Handler fh = new FileHandler("%t/wombat.log");
    Logger.getLogger("").addHandler(fh);
    Logger.getLogger("com.wombat").setLevel(Level.FINEST);
    ...
}

グローバル設定を無視した簡単な用法

独自のロギングHandlerを設定し、グローバル設定を無視する小さいプログラムを次に示します。

package com.wombat;

import java.util.logging.*;

public class Nose {
    private static Logger logger = Logger.getLogger("com.wombat.nose");
    private static FileHandler fh = new FileHandler("mylog.txt");
    public static void main(String argv[]) {
        // Send logger output to our FileHandler.
        logger.addHandler(fh);
        // Request that every detail gets logged.
        logger.setLevel(Level.ALL);
        // Log a simple INFO message.
        logger.info("doing stuff");
        try {
            Wombat.sneeze();
        } catch (Exception ex) {
            logger.log(Level.WARNING, "trouble sneezing", ex);
        }
        logger.fine("done");
    }
}

XMLの出力例

次に、XMLFormatter XML出力の例を示します。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
  <record>
    <date>2015-02-27T09:35:44.885562Z</date>
    <millis>1425029744885</millis>
    <nanos>562000</nanos>
    <sequence>1256</sequence>
    <logger>kgh.test.fred</logger>
    <level>INFO</level>
    <class>kgh.test.XMLTest</class>
    <method>writeLog</method>
    <thread>10</thread>
    <message>Hello world!</message>
  </record>
</log>

付録A: XMLFormatter出力用のDTD

<!-- DTD used by the java.util.logging.XMLFormatter -->
<!-- This provides an XML formatted log message. -->

<!-- The document type is "log" which consists of a sequence
of record elements -->
<!ELEMENT log (record*)>

<!-- Each logging call is described by a record element. -->
<!ELEMENT record (date, millis, nanos?, sequence, logger?, level,
class?, method?, thread?, message, key?, catalog?, param*, exception?)>

<!-- Date and time when LogRecord was created in ISO 8601 format -->
<!ELEMENT date (#PCDATA)>

<!-- Time when LogRecord was created in milliseconds since
midnight January 1st, 1970, UTC. -->
<!ELEMENT millis (#PCDATA)>

<!-- Nano second adjustement to add to the time in milliseconds. 
This is an optional element, added since JDK 9, which adds further
precision to the time when LogRecord was created.
 -->
<!ELEMENT nanos (#PCDATA)>

<!-- Unique sequence number within source VM. -->
<!ELEMENT sequence (#PCDATA)>

<!-- Name of source Logger object. -->
<!ELEMENT logger (#PCDATA)>

<!-- Logging level, may be either one of the constant
names from java.util.logging.Level (such as "SEVERE"
or "WARNING") or an integer value such as "20". -->
<!ELEMENT level (#PCDATA)>

<!-- Fully qualified name of class that issued
logging call, e.g. "javax.marsupial.Wombat". -->
<!ELEMENT class (#PCDATA)>

<!-- Name of method that issued logging call.
It may be either an unqualified method name such as
"fred" or it may include argument type information
in parenthesis, for example "fred(int,String)". -->
<!ELEMENT method (#PCDATA)>

<!-- Integer thread ID. -->
<!ELEMENT thread (#PCDATA)>

<!-- The message element contains the text string of a log message. -->
<!ELEMENT message (#PCDATA)>

<!-- If the message string was localized, the key element provides
the original localization message key. -->
<!ELEMENT key (#PCDATA)>

<!-- If the message string was localized, the catalog element provides
the logger's localization resource bundle name. -->
<!ELEMENT catalog (#PCDATA)>

<!-- If the message string was localized, each of the param elements
provides the String value (obtained using Object.toString())
of the corresponding LogRecord parameter. -->
<!ELEMENT param (#PCDATA)>

<!-- An exception consists of an optional message string followed
by a series of StackFrames. Exception elements are used
for Java exceptions and other java Throwables. -->
<!ELEMENT exception (message?, frame+)>

<!-- A frame describes one line in a Throwable backtrace. -->
<!ELEMENT frame (class, method, line?)>

<!-- an integer line number within a class's source file. -->
<!ELEMENT line (#PCDATA)>