Flexにもログを #3

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回はブラウザのコンソールにログを出力してみました。今回は、ログの設定の管理について考えてみたいと思います。

ソースコード内でのログの設定

あるアプリケーションにおいて、ログ出力のルールが下記のようになっていると仮定します。

  • 標準出力とブラウザコンソールにログを出力
  • 出力するのはアプリケーションとmx.messagingパッケージコンポーネントのログ
  • アプリケーションのログはDEBUG以上を出力
  • mx.messagingパッケージのコンポーネントのログはINFO以上を出力

この場合、ロガーを設定するコードは下記のようになります。アプリケーションログは"LoggerTest"というカテゴリで出力されるものとします。

    var traceTarget:TraceTarget = new TraceTarget();
    traceTarget.level = LogEventLevel.DEBUG;
    traceTarget.filters = ["LoggerTest"];
    traceTarget.includeDate = true;
    traceTarget.includeTime = true;
    traceTarget.includeLevel = true;
    traceTarget.includeCategory = true;
    Log.addTarget(traceTarget);
    var consoleTarget:ConsoleTarget = new ConsoleTarget();
    consoleTarget.level = LogEventLevel.DEBUG;
    consoleTarget.filters = ["LoggerTest"];
    consoleTarget.includeDate = true;
    consoleTarget.includeTime = true;
    consoleTarget.includeLevel = true;
    consoleTarget.includeCategory = true;
    Log.addTarget(consoleTarget);
    
    traceTarget = new TraceTarget();
    traceTarget.level = LogEventLevel.INFO;
    traceTarget.filters = ["mx.messaging.*"];
    traceTarget.includeDate = true;
    traceTarget.includeTime = true;
    traceTarget.includeLevel = true;
    traceTarget.includeCategory = true;
    Log.addTarget(traceTarget);
    consoleTarget = new ConsoleTarget();
    consoleTarget.level = LogEventLevel.INFO;
    consoleTarget.filters = ["mx.messaging.*"];
    consoleTarget.includeDate = true;
    consoleTarget.includeTime = true;
    consoleTarget.includeLevel = true;
    consoleTarget.includeCategory = true;
    Log.addTarget(consoleTarget);

設定のコードが長くなってしまいました。このようにログの設定がコードに記述してあると、変更が大変ですし、設定の間違えを起こす可能性が高くなってしまいそうですね。さらに、「リリース時は標準出力に出力しない・アプリケーションログはINFO以上を出力する」というような条件が加わってきたとすると、設定をするコードの一部がコメントアウトされてコードが荒れることが予想されます。

そのような状態を改善する方法として、設定を外部ファイルに記述することが挙げられます。今回はログの設定をXMLファイルに記述し、実行時にそれを読み込んでログターゲットを生成してみたいと思います。

ログの設定をXMLファイルに記述

まずは、ログの設定を記述したXMLファイルです。今回は、log-config.xmlという名前でプロジェクトのルート直下に配置します。

<?xml version="1.0"?> 
<log-config>
    <targets>
        <target type="trace" level="INFO" filters="mx.messaging.*"
            includeDate="true" includeTime="true" includeLevel="true" includeCategory="true">
        </target>
        <target type="trace" level="ALL" filters="LoggerTest"
            includeDate="true" includeTime="true" includeLevel="true" includeCategory="true">
        </target>
        <target type="console" level="INFO" filters="mx.messaging.*" 
            includeDate="true" includeTime="true" includeLevel="true" includeCategory="true">
        </target>
        <target type="console" level="ALL" filters="LoggerTest" 
            includeDate="true" includeTime="true" includeLevel="true" includeCategory="true">
        </target>
    </targets>
</log-config>

生成したいログターゲット一つにつき、targetタグのエレメントを一つ定義しています。さらに、ログターゲットに設定する情報を各属性に記述しています。XMLのツリー構成やタグ名は、解析さえできれば好きなように決めて大丈夫です。

次は、実行時に上記のXMLを読み込んでログの設定をするクラスを作成します。

package jp.classmethod.sample.helper
{
    import jp.classmethod.sample.logtarget.ConsoleTarget;
    
    import mx.logging.ILoggingTarget;
    import mx.logging.Log;
    import mx.logging.LogEventLevel;
    import mx.logging.targets.LineFormattedTarget;
    import mx.logging.targets.TraceTarget;

    public class LogHelper
    {
        [Embed(source="log-config.xml", mimeType="application/octet-stream")]
        private static const logConfig:Class;
        
        public static function initialize():void
        {
            var logConfigXML:XML = new XML(new logConfig());
            var targets:XMLList = logConfigXML..target;
            for each (var target:XML in targets)
            {
                var logTarget:ILoggingTarget;
                var type:String = target.@type;
                switch (type)
                {
                    case "trace":
                        logTarget = new TraceTarget();
                        break;
                    case "console":
                        logTarget = new ConsoleTarget();
                        break;
                    default:
                        break;
                }
                
                if (!logTarget)
                {
                    continue;
                }
                
                var level:String = target.@level;
                var logLevel:int;
                if (level)
                {
                    level = level.toUpperCase();
                    switch (level)
                    {
                        case "FATAL":
                            logLevel = LogEventLevel.FATAL;
                            break;
                        case "ERROR":
                            logLevel = LogEventLevel.ERROR;
                            break;
                        case "WARN":
                            logLevel = LogEventLevel.WARN;
                            break;
                        case "INFO":
                            logLevel = LogEventLevel.INFO;
                            break;
                        case "DEBUG":
                            logLevel = LogEventLevel.DEBUG;
                            break;
                        case "ALL":
                            logLevel = LogEventLevel.ALL;
                            break;
                        default:
                            logLevel = LogEventLevel.INFO;
                            break;
                    }
                }
                logTarget.level = logLevel;
                
                var filters:String = target.@filters;
                if (filters)
                {
                    logTarget.filters = filters.split(",");
                }
                
                if (logTarget is LineFormattedTarget)
                {
                    var lineFormattedTarget:LineFormattedTarget = LineFormattedTarget(logTarget);
                    lineFormattedTarget.includeDate = toBoolean(target.@includeDate);
                    lineFormattedTarget.includeTime = toBoolean(target.@includeTime);
                    lineFormattedTarget.includeLevel = toBoolean(target.@includeLevel);
                    lineFormattedTarget.includeCategory = toBoolean(target.@includeCategory);
                }
                
                Log.addTarget(logTarget);
            }
        }
        
        private static function toBoolean(value:String):Boolean
        {
            return value && value == "true";
        }
    }
}

それでは、処理内容を順番に見ていきましょう。

XMLの埋め込みと取得

XMLの埋め込み部のソースコードです。

[Embed(source="log-config.xml", mimeType="application/octet-stream")]
private static const logConfig:Class;

先ほど作成したXMLファイルをEmbedメタタグを利用して埋め込んでいます。EmbedメタタグはXML形式に対応していませんので、MIMEタイプをapplication/octet-streamにしてバイナリ形式で埋め込んでいます。これにより、定数logConfigは埋め込んだXMLのバイトデータを表すByteArrayAsset拡張クラスのClassクラスとなります。

XMLを取得するソースコードです。

var logConfigXML:XML = new XML(new logConfig());

logConfigをインスタンス化すると、埋め込んだXMLのバイトデータのインスタンスが生成されます。これをXMLクラスのコンストラクタの引数に渡すと、埋め込んだXML情報を保持するXMLクラスのインスタンスが取得できます。

XMLの解析とログターゲットの生成

まずはXMLからログターゲット単位でエレメントを取り出します。

var targets:XMLList = logConfigXML..target;
for each (var target:XML in targets)
{
    ...
}

targetタグのエレメントのリストを取得し、for eachの中で各エレメントについて処理を行っています。

次に、ログターゲットの生成部分です。

var logTarget:ILoggingTarget;
var type:String = target.@type;
switch (type)
{
    case "trace":
        logTarget = new TraceTarget();
        break;
    case "console":
        logTarget = new ConsoleTarget();
        break;
    default:
        break;
}

type属性の文字列を取り出し、それを使って生成するログターゲットを判断しています。

続いて、生成したログターゲットに設定を行います。

var level:String = target.@level;
...
logTarget.level = logLevel;

var filters:String = target.@filters;
if (filters)
{
    logTarget.filters = filters.split(",");
}

if (logTarget is LineFormattedTarget)
{
    var lineFormattedTarget:LineFormattedTarget = LineFormattedTarget(logTarget);
    lineFormattedTarget.includeDate = toBoolean(target.@includeDate);
    lineFormattedTarget.includeTime = toBoolean(target.@includeTime);
    lineFormattedTarget.includeLevel = toBoolean(target.@includeLevel);
    lineFormattedTarget.includeCategory = toBoolean(target.@includeCategory);
}

こちらも、対応する各属性から取り出した値をログターゲットにセットしています。

最後に、ログターゲットを登録すればログの設定が完了します。

Log.addTarget(logTarget);

サンプルアプリケーション

では、作成したXMLとその処理クラスを利用してログを出力してみましょう。以下はボタンをクリックするとRPC通信を行うサンプルアプリケーションです。RPC通信にはBlazeDSを利用しています。(FlexSDK 4.5.1.21328にて動作確認)

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx"
               preinitialize="application1_preinitializeHandler(event)"
               applicationComplete="application1_applicationCompleteHandler(event)">
    <fx:Script>
        <![CDATA[
            import jp.classmethod.sample.helper.LogHelper;
            import jp.classmethod.sample.service.TestService;
            
            import mx.events.FlexEvent;
            import mx.logging.ILogger;
            import mx.logging.Log;
            import mx.rpc.Responder;
            import mx.rpc.events.FaultEvent;
            import mx.rpc.events.ResultEvent;
            
            private var logger:ILogger = Log.getLogger("LoggerTest");
            private var testService:TestService;
            
            protected function application1_preinitializeHandler(event:FlexEvent):void
            {
                LogHelper.initialize();
            }

            protected function application1_applicationCompleteHandler(event:FlexEvent):void
            {
                testService = new TestService();
            }

            protected function button_clickHandler(event:MouseEvent):void
            {
                testService.test(new mx.rpc.Responder(
                    function (event:ResultEvent):void
                    {
                        logger.debug(event.result as String);
                    },
                    function (event:FaultEvent):void
                    {
                        logger.debug(event.fault.faultString);
                    }
                ));
            }

        ]]>
    </fx:Script>
    <s:Button id="button" click="button_clickHandler(event)"/>
</s:Application>

package jp.classmethod.sample.service
{
    import mx.rpc.AsyncToken;
    import mx.rpc.IResponder;
    import mx.rpc.remoting.RemoteObject;

    public class TestService
    {
        private var remoteObject:RemoteObject;
        
        public function TestService()
        {
            remoteObject = new RemoteObject("testService");
        }
        
        public function test(responder:IResponder):void
        {
            var token:AsyncToken = remoteObject.test();
            token.addResponder(responder);
        }
    }
}

Javaで実装されたサーバー側のサービスコンポーネントです。

package jp.classmethod.sample.service;

public class TestService {
    
    public String test() {
        return "サービスから取得したデータ";
    }
    
}

実行結果です。

XMLファイルで設定した内容通り、アプリケーションログはDEBUG以上、mx.messagingパッケージのコンポーネントのログはINFO以上が出力されていることが確認できると思います。また、標準出力・ブラウザのコンソールともにログが出力されています。設定をいろいろ変えて試してみてください。

なお、今回は設定を記述したXMLファイルをswfに埋め込んでいますので、設定を変更した場合は再度ビルドする必要があります。XMLファイルを実行時にサーバーから取得するようにすれば必ずしも埋め込む必要はないのですが、その場合はXMLファイルのロードが終わるまでロガーに出力設定がされません。結果、Flexの初期化や使用しているフレームワークの初期化の際のログが出力される保証がなくなってしまう点に注意してください。

まとめ

三回にわたってFlexのログAPIとその利用法について紹介してきました。とても簡単に利用することができる上に、ある程度の拡張であれば容易にできることがお分かりいただけたかと思います。今まで利用してこられなかった方も、是非これを機に利用してみてください。ほんの少しの手間をかけて入れたログが、後になって大きな助けとなってくれるはずです。