Stefan B.log

for whatever reason

Flex: Using the Flex3 Logging API

with 6 comments

The short story

Use the Flex3 Logging API in your Flex3 code, nothing else! No trace() and flashlog.txt mess and no proprietary third parties logging framework API’s.

Define a logger:

var log:ILogger = Log.getLogger("de.sbistram.logtest.Foo");

and use it:

if (Log.isDebug()) {
    log.debug("Foo debugging");
}

That’s it - mmh, almost…

The long story

Coming from a Java log4j and Ruby log4r background I was first looking for an Actionscript3 log4AS3, but it seems to be not an option. Anyway, there is a nice Flex3 Logging API just in place (the Java Logging API was introduced in JDK 1.4, too late).

To see the logging output you need a logging target which is normally a console. It can be the console of the Flex Builder IDE, a Firebug console or some kind of a standalone console. But the most important fact is that you can inject the logging output targets via the mx.logging.Log#addTarget method. What this means? Hang on…

Candidates

I’ve created a small sample project to explore and play around with a mixture of four different logging frameworks and targets:

It is just my choice, you may want to check others like Alcon, but the design principles should be the same.

Sample project

Before you start with your own experiments take a look at the articles of the blogs mentioned above and download and install the frameworks. It may look like a complete mess to use four different logging targets in one project, but actually its fun. You need only two additional libraries and the SOSLoggingTarget.as file:

The Logging.mxml class initialize the logging via Config.initLogging() and create a test button. Each click on it will create a Foo class, which generates some logging output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
    creationComplete="init()">
 
    <mx:Script><![CDATA[
 
    import de.sbistram.config.Config;
    import de.sbistram.logtest.Foo;
 
    private function init():void {
        Config.initLogging();
    }
 
    private function testLogging():void {
        new Foo(); // show some logging       
    }
 
    ]]></mx:Script>
 
    <mx:Button label="Test Logging" click="testLogging()"/>
 
</mx:Application>

The hole logger configuration is done in the Config class and all dependencies on the logging frameworks and logging targets are only used in this class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package de.sbistram.config {
 
import mx.logging.ILogger;
import mx.logging.Log;
import mx.logging.LogEventLevel;
import mx.logging.targets.TraceTarget;
 
import com.adobe.air.logging.FileTarget;
import com.soenkerohde.logging.SOSLoggingTarget;
import org.osflash.thunderbolt.Logger;
import org.osflash.thunderbolt.ThunderBoltTarget;
 
import de.sbistram.logtest.Foo;
import de.sbistram.utils.LogUtils;
 
public class Config {
 
    // Log file URI, see source of com.adobe.air.logging.FileTarget
    private static const DEFAULT_LOG_PATH:String = "app-storage:/application.log";    
 
    // the log file path for air application
    public static var logFilePath:String = DEFAULT_LOG_PATH;
 
    // switch on/off different targets
    private static var SOS_TARGET:Boolean             = true;
    private static var THUNDERBOLT_TARGET:Boolean     = false;
    private static var FILE_TARGET:Boolean            = true;    // only AIR
    private static var TRACE_TARGET:Boolean           = false;
 
    /**
     * Configure logging
     */
    public static function initLogging():void {
 
        var logLevel:int = LogEventLevel.ALL;
 
        // filter for packages or classes
        var filters:Array = LogUtils.getFilter([
            Config,               // add this class
            [Foo],                // instead of: "de.sbistram.logtest.*"
            Array,                // any class is allowed
            logLevel,             // you can also use an instance
            "de.sbistram.foo.*",  // you can still use strings
            "Foo.*" 	          // to log all internal classes of Foo
            ]);
 
        // so the filter array for the above filters array would be:
        // [de.sbistram.config.Config,de.sbistram.logtest.*,Array,int,de.sbistram.foo.*]
 
        //
        // ThunderBoltTarget (ThunderBoltAS3_v1.0.zip)
        //
        if (THUNDERBOLT_TARGET) {
            var thunderBoltTarget:ThunderBoltTarget = new ThunderBoltTarget();
 
            // Bug?: Windows XP - automatic detection of FireFox 3.0.4 with Flash Player 10 was not working.
            // @see http://www.websector.de/blog/2008/06/15/10-tips-and-tricks-using-thunderbolt-as3/
            Logger.console = false;    // presume we're running Firefox / Firebug
            try {
                // next line will throw an exception without ExternalInterface, e.g. in standalone Flash Player:
                // Error: Error #2067: The ExternalInterface is not available in this container...
                Logger.error("");
            } catch (error:Error) {
                Logger.console = true;    // use the trace output
            }
 
            // Flex3.2: Setting the level is already adding the target to the logging.
            // see source code: mx.logging.AbstractTarget#level
            thunderBoltTarget.level = logLevel;            
 
            thunderBoltTarget.filters = filters;
            Log.addTarget(thunderBoltTarget);
        }
 
        //
        // SOS_TARGETLoggingTarget
        //        
        if (SOS_TARGET) {
            var sosTarget:SOSLoggingTarget = new SOSLoggingTarget();
            sosTarget.level = logLevel;    
            sosTarget.fieldSeparator = ":";
            sosTarget.includeCategory = true;
            sosTarget.includeLevel = true;
            //sosTarget.filters = LogUtils.getFilter([Array]);    // only Array logging for SOS_TARGET
            sosTarget.filters = filters;
 
            Log.addTarget(sosTarget);
        }
 
        //
        // FileTarget (only for AIR)
        // Part of http://code.google.com/p/as3corelib
        //
 
        // use conditional compilation for AIR code
        // @see http://livedocs.adobe.com/flex/3/html/help.html?content=compilers_21.html
        // Otherwise (un)comment this AIR part
        //CONFIG::air {    // conditional compilation for AIR
        /*
            if (FILE_TARGET) {
                import flash.filesystem.File;
                var file:File = new File(DEFAULT_LOG_PATH);
                var fileTarget:FileTarget = new FileTarget(file);
                fileTarget.fieldSeparator = ":";
                fileTarget.filters = filters;
                fileTarget.includeCategory = true;
                fileTarget.includeLevel = true;
                fileTarget.includeTime = true;
                fileTarget.includeDate = true;
                Log.addTarget(fileTarget);
            }
        */
        //}    // end CONFIG::air
 
        //
        // TraceTarget (redirect logging output to trace())
        // Enable this target in Adobe Flex Builder to see logging output in the console
        // The application must run in debug mode!
        //
        if (TRACE_TARGET) {
            var traceTarget:TraceTarget = new TraceTarget();
            traceTarget.fieldSeparator = ":";
            traceTarget.filters = filters;
            traceTarget.includeCategory = true;
            traceTarget.includeLevel = true;
            traceTarget.includeTime = true;
            traceTarget.level = logLevel;
            Log.addTarget(traceTarget);
        }
 
        // start logging
        if (Log.isDebug()) {
            var log:ILogger = LogUtils.getLogger(Config);
            if (THUNDERBOLT_TARGET == true) {
                log.debug("Logging - ThunderBoltTarget initialized, filters: [{0}]", thunderBoltTarget.filters);
            }
            if (SOS_TARGET) {
                log.debug("Logging - SOS_TARGETLoggingTarget initialized, filters: [{0}]", sosTarget.filters);
            }
            //CONFIG::air {
            /*
                if (FILE_TARGET) {
                    log.debug("Logging - AIR FileTarget initialized, filters: [{0}]", fileTarget.filters);
                    // Note: Under Windows XP - the dir is hidden (use 'dir /A:H')! 
                    log.debug("Logging - AIR file path: " + file.nativePath);
                }
            */
            //} // end CONFIG:air
            if (TRACE_TARGET) {
                log.debug("Logging - TraceTarget initialized, filters: [{0}]", traceTarget.filters);
            }        
        }
    }        
}
}

Play around with the settings (switch targets on/off, set filters, …) and see where the logging output goes. I like to use the SOS console for WEB- and Adobe AIR development. SOS is using a simple socket connection to the SOS console and is really lightweight. Thunderbolt is great for working with Firefox and the Firebug console and it has also a Thunderbolt AIR console. The FileTarget writes the logging statements to a file and therefore can only be used for AIR. It is missing some features (like log4j’s DailyRollingFileAppender) and not ready for the enterprise, but a good starting point. The TraceTarget can be used in Flex Builder 3 to see some logging output in the console, but because it is using the trace() method you have to run your application in debug mode. You can also do any kind of combination of these logging targets - just try it out and see what is working for you and what you like. :-)

The actual usage of the Logging API looks like e.g.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package de.sbistram.logtest {
 
import mx.logging.ILogger;
import mx.logging.Log;
import de.sbistram.utils.LogUtils;
 
public class Foo {
 
    private var log:ILogger = LogUtils.getLogger(this);
 
    public function Foo() {
        testLogging();
    }
 
    private function testLogging():void {
 
        log.debug("Logging test for DEBUG");
        log.info("Logging test for INFO");
        log.warn("Logging test for WARN");
        log.error("Logging test for ERROR");
        log.fatal("Logging test for FATAL");
 
        var seasons:Array = ["spring", "summer", "autumn", "winter"];
 
        if (Log.isDebug()) {
            // Note: Using the 'seasons' category, not Foo
            LogUtils.getLogger(seasons).debug("seasons = {0}", seasons);
        }
 
        new InternalFoo();    // check also the logger output of an internal class
    }
 
}
}
 
//
// Test the logging of an internal class. Output will look like:
// 21:26:58.505:[DEBUG]:Foo.as.0.InternalFoo:InternalFoo logging
//
import mx.logging.ILogger;
import de.sbistram.utils.LogUtils;
 
class InternalFoo {
 
    private var log:ILogger = LogUtils.getLogger(this);
 
    public function InternalFoo() {
        log.debug("InternalFoo logging");
    }
}

The important fact here is that the code has no dependency on any third party logging stuff! It only uses the Flex3 Logging API and a helper method of the LogUtils class.

Best practice

It is a simple case of the DI design principle - Dependency Injection: The Flex3 Logging API allows you to create your own instance of a ILoggingTarget and inject it via the method mx.logging.Log.addTarget(target:ILoggingTarget).

So don’t use example code like this, e.g. Thunderbolt:

import org.osflash.thunderbolt.Logger;
 
var myNumber: int = 5;
var myString: String = "Lorem ipsum";
Logger.error ("Logging two objects: A number typed as int and a string", myNumber, myString);

and don’t use additional features of a Logging API, e.g. Thunderbolt:

Logger.info(Logger.memorySnapshot());
 
// stop logging
Logger.hide = true;
// resume logging
Logger.hide = false;
 
Logger.about();

Again your code depends on one special Logging Framework - bad design!
But as long as you stick to the Flex Logging API your code could be used across multiple projects without any changes. And the second issue - a logger should care about logging, not memory snapshots, that’s another important design principle: SoC - Separation of Concerns.

Ok, and the last design principle for this article: DRY - Don’t Repeat Yourself:

package de.sbistram.logtest {
 
import mx.logging.ILogger;
import mx.logging.Log;
 
public class Foo {
 
    private var log:ILogger = Log.getLogger("de.sbistram.logtest.Foo");

What’s wrong with the code? It’s a common naming convention to use the full qualified class name for the logger category. Ok, but the information (package + class) is already there - remember: Don’t Repeat Yourself (actually I did it right now :-) ).
The Log.getLogger("de.sbistram.logtest.Foo") should be replaced by LogUtils.getLogger(this) and this is just a shortcut for Log.getLogger(getQualifiedClassName(this).replace("::", ".")). Another advantage of not using a String is code refactoring. If the package name or the class name is changing you don’t have to update any hardcoded String references. Btw, the same principle is used in defining a filter for a logger target in the LogUtils.getFilter method.

Here’s the code of the LogUtils class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package de.sbistram.utils {
 
import flash.utils.*;
 
import mx.logging.ILogger;
import mx.logging.Log;
 
public class LogUtils {
 
    //--------------------------------------------
    // Flex API logging support
    //--------------------------------------------
 
    /**
     * Get an instance of an <code>ILogger</code>
     * Use the full qualified class name as the category
     * 
     * @param    value can be a class or an instance
     * @return    mx.logging.ILogger
     */
    public static function getLogger(value:*):ILogger {
        return Log.getLogger(getCategory(value));
    }
 
    /**
     * Using the full qualified class name as the category
     * name for the logger.
     * 
     * <p>
     * Unfortunately the method getQualifiedClassName is 
     * returning e.g. de.sbistram.Utils::LogUtils for this class, 
     * but Log.getLogger doesn't allow ':' chars in the category
     * name, so "::" will be replaced by "."
     *
     * Internal classes don't include the package, e.g.:
     * Foo.as$0::InternalFoo
     * </p>
     * 
     * @param    value of an instance object or class
     * @return    the full qualified class name without "::"
     * @see http://livedocs.adobe.com/flex/3/langref/mx/logging/Log.html#getLogger()
     */
    public static function getCategory(value:*):String {
        var qcn:String = getQualifiedClassName(value);
        qcn = qcn.replace("$", ".");    // maybe an internal class using '$'
        return qcn.replace("::", ".");  // public class in a package using '::'
    }
 
    /**
     * Convert an array of classes or instances to an array of valid 
     * category names. 
     * 
     * - [...] means to include the complete package of the class
     *  - String filter items left untouched
     *  - For internal classes of a public class Foo -> "Foo.*"
     * 
     * @param    values
     * @return    array of String categories
 
     */
    public static function getFilter(values:Array):Array {
        var categories:Array = [];
        for (var value:* in values) {
            value = values[value];
            // [] indicate to include all classes of a package
            if (value is Array) {
                var qcn:String = getQualifiedClassName(value[0]);
                value = qcn.slice(0, qcn.indexOf("::")).concat(".*");
            } 
            categories.push((value is String) ? value : getCategory(value));
        }
        return categories;
    }
}
}

Conclusion

So no matter what kind of application framework you’re using (Cairngorm, PureMVC, Swiz, Mate or whatever or none at all) and no matter whether you’re just hacking a few lines of code or building the next great enterprise RIA or AIR application - use the Flex Logging API.

Written by Stefan Bistram

December 12th, 2008 at 10:41 am

Posted in AS3, Flex

Tagged with ,

6 Responses to 'Flex: Using the Flex3 Logging API'

Subscribe to comments with RSS or TrackBack to 'Flex: Using the Flex3 Logging API'.

  1. Hi Stefan,
    This is a perfect post, I used some of your codes in my project, I think it’s needed to comment here. And for the filters of log target, we use a method that simply add some prefix to the category name, e.g. “EXOWEB_”, then the filters can be ["EXOWEB_*"], it’s simple.

    Regards,
    YuChao

    Yuchao

    24 Feb 09 at 4:28 am

  2. I reviewed the material, and adopted it to my needs.
    Very much appreciated.

    In Config.as, I received compiler errors for the statements with ‘console’ (i.e Logger.console)

    Amnon Janiv

    2 Apr 09 at 8:17 pm

  3. im not cleared about ur topic please explain breaf so that v can understand :)

    vinod

    12 May 09 at 7:43 am

  4. [...] cool, right? You can read more about TraceTarget and other logging frameworks here. But if you just want a quick and dirty way to see some more debug info on your Remoting calls then [...]

  5. In Config.as example code, you should set filters before level.
    example:
    thunderBoltTarget.filters = filters; //before level
    thunderBoltTarget.level = logLevel;

    see point 2 in http://blog.adamcreeger.com/2008/08/logging-in-flexair-two-strange-things.html

    Danilo Ascione

    1 Oct 09 at 1:54 pm

  6. [...] cool, right? You can read more about TraceTarget and other logging frameworks here. But if you just want a quick and dirty way to see some more debug info on your Remoting calls then [...]

Leave a Reply