Logging in ActionScript 3
22nd February, 2010 – 9:27 pmLogging is a fundamental part of any mid to large scale application, your application needs to be able to talk to other developers and Q&A engineers to help diagnose problems and gain insight into what’s going on under the hood. Jesse Warden recently wrote an excellent article on logging, however, my own implementation of logging AS3 apps differs so much that I thought it warranted a blog post.
The Problems
One of the most important things in OOP is to ensure that you avoid tight coupling; Jesse’s approach of using a static logger leaves your code scattered with a dependency on it.
Another point that Jesse makes is how important it is to include the name of the enclosing class when writing a log message,
… all log messages start with “ClassName::methodName” where ClassName is the name of the class you are in, and methodName is the current method the log message is in. It seems a major pain at first, but I guarantee you when you start removing them 2 months later, you know EXACTLY where to find it vs. that one trace that stays there for weeks because no one found where the bastard is.
I couldn’t agree more with this, having come from working on a massive application with thousands of log messages, not knowing where a message was originating from would have been a nightmare. However, Jesse’s approach is very manual and relies on your entire team being disciplined; not to mention the fact that as soon as you start refactoring that “ClassName::methodName” identifier is quickly going to get out of sync.
A Commons Solution
I feel that there is a simple solution to both the above problems in the form of the AS3 Commons Logging Framework. The framework provides an abstraction of logging which means your code is not dependant on any one implementation, plus it has the added benefit of automatically adding the name of the Class when writing a log message; winner!
Usage is pretty straight forward, but requires a little more setup than Jesse’s static helper approach; first of all your request a static ILogger instance from the LoggerFactory at the top of your class and then you are free to use it throughout the Class to write log messages, for example:
{
public class MyClass
{
// Request an ILogger instance for this Class.
public static const LOGGER : ILogger = LogFactory.getClassLogger(MyClass);
public function MyClass()
{
// Write a log message using the Logger instance.
LOGGER.info("Created instance of MyClass");
}
}
}
Using the TraceLogger implementation which the AS3 Commons Logger will default to, you would receive the following in your Flash TraceLog:
Sun Feb 21 10:33:29 GMT+0000 2010 [INFO] uk.co.jonnyreeves.MyClass - Created instance of MyClass
The beauty of this approach is that the actual implementation of the ILogger interface is left to the LoggerFactory which means it can be switched at runtime. Another neat benfit is that the name of the Logger is taken from the encolosing Class which created it (via LoggerFactory.getClassLogger()). This mean that is you rename the Class as part of a refactoring effort, all the log messages contained in said class will automatically update.
Getting More Specific
So I’ve mentioned that your can decide the ILogger implementation at runtime, let’s see an example of that in action. My current Logging implementation of choice is the excellent DeMonsters Monster Debugger which reminds me a lot of the AS2 classic, LumnicBox in that it allows you to fully inspect the Object Graph at run time.
The first thing we need is to create an implementation of LogFactory which will return our ILogger instances upon request:
{
import nl.demonsters.debugger.MonsterDebugger;
import org.as3commons.logging.ILogger;
import org.as3commons.logging.ILoggerFactory;
/**
* Provides a bridge between the AS3Commons Logging Framework and MonsterDebugger.
*
* @author John Reeves
*/
public class MonsterDebuggerLoggerFactory implements ILoggerFactory
{
/**
* Initialises MonsterDebugger
*
* @param target The Base Object you wisht to start graphing from (usually you will supply the
* Display Root DisplayObject).
*
* @param clearConsole Clears MonsterDebugger’s Trace Console when the initial connection is made.
*/
public function MonsterDebuggerLoggerFactory(target : Object, clearConsole : Boolean = true)
{
new MonsterDebugger(target);
if (clearConsole)
{
MonsterDebugger.clearTraces();
}
}
/**
* Factory Method, used to return an instance of ILogger to the AS3Commons Logging Framework.
*/
public function getLogger(name : String) : ILogger
{
return new MonsterDebuggerLogger(name);
}
}
}
Now all that’s left to do is to create the MonsterDebuggerLogger implementation of the ILogger interface; this will re-write all the LOGGER.info(), etc calls through to MonsterDebugger.
{
import org.as3commons.logging.util.MessageUtil;
import nl.demonsters.debugger.MonsterDebugger;
import org.as3commons.logging.LogLevel;
import org.as3commons.logging.impl.AbstractLogger;
/**
* Provides a bridge between the AS3 Commons Logging implementation and Monster Debugger; it’s not perfect but it
* works for the most part.
*
* The Target of the log message will always be the name of the logger.
*
* The Object of the log message will vary depending on the parameters passed. If the message string is empty
* then the params Array will be logged. If the message string is not empty, the params Array will be used to
* tokenise the message string.
*
* Usage:
* LOGGER.debug("Hello World"); // Outputs message: "(String) Hello World"
* LOGGER.debug("Hello {0} {1}, "Jonny", "Reeves"); // Outputs message: "(String) Hello Jonny Reeves"
* LOGGER.debug("", [ 1, 2, 3 ]); // Outputs message: "(Array)…" (which can then be inspected)
*
* Incorrect usage:
* LOGGER.debug("Some Array", [1, 2, 3]); // Outputs message: "(String) Some Array" (which can’t be inspected)
*
* @author John Reeves
*/
public class MonsterDebuggerLogger extends AbstractLogger
{
public var level : int = LogLevel.DEBUG;
public function MonsterDebuggerLogger(name : String)
{
super(name);
}
override protected function log(level : uint, message : String, params : Array) : void
{
if (level >= this.level)
{
var target : Object = name;
var object : Object = getLogObject(message, params);
var colour : uint = getLogMessageColour(level);
MonsterDebugger.trace(target, object, colour);
}
}
private function getLogObject(message : String, params : Array) : Object
{
if (message == "")
{
// If the user only supplied a single param to log, we remove the params array which is enclosing it.
return (params.length == 1) ? params.pop() : params;
}
else
{
return MessageUtil.toString(message, params);
}
}
private function getLogMessageColour(level : int) : uint
{
var colour : int;
switch (level)
{
case LogLevel.FATAL:
case LogLevel.ERROR:
colour = MonsterDebugger.COLOR_ERROR;
break;
case LogLevel.WARN:
colour = MonsterDebugger.COLOR_WARNING;
break;
default:
colour = MonsterDebugger.COLOR_NORMAL;
break;
}
return colour;
}
override public function get debugEnabled() : Boolean
{
return LogLevel.DEBUG >= level;
}
override public function get errorEnabled() : Boolean
{
return LogLevel.ERROR >= level;
}
override public function get infoEnabled() : Boolean
{
return LogLevel.INFO >= level;
}
override public function get warnEnabled() : Boolean
{
return LogLevel.WARN >= level;
}
override public function get fatalEnabled() : Boolean
{
return LogLevel.FATAL >= level;
}
}
}
The code here is fairly self documenting with the protected log() method proxying all the logging requests through to MonsterDebugger. Because the creators of the AS3Commons Logging Project specified the message param to have a datatype of String, we are forced to do a little bit of a workaround when we want to dump Objects out to the MonsterDebugger console. There is an open ticket on the AS3Commons project page should you wish to request a change.
The last piece of the puzzle is to swap the AS3Commons LoggingFactory implementation, this is very straight forward and requires a single line of code during your application’s startup routine (preferably before any logging takes place).
{
import uk.co.jonnyreeves.logging.util.MonsterDebuggerLoggerFactory
import flash.display.Sprite;
/**
* Application Entry Point.
* @author John Reeves
*/
public class MyApp extends Sprite
{
public function MyApp()
{
LoggerFactory.loggerFactory = new MonsterDebuggerLoggerFactory(this);
}
}
}
Once this is done you are free to start using the LoggingFactory as per the example further up the page. Your log messages should now show up in the MonsterDebugger logging console.
2 Responses to “Logging in ActionScript 3”
Hi Jonny,
Thanks for the post! :-)
One question … its not clear to me how you would get the methodName into every log message using this approach – without relying on a manual approach and a diciplined team like in Jesse Warden’s example.
I suppose one could define a logger for every single method but there must surely be a better solution.
By Pete on Apr 10, 2010
Hi Pete,
You’re right, I should have shown this; luckily the Error Class’ getStackTrace() method contains everything we need to extract both the calling method so it can be presented to the Logging function (after a bit of String Manipulation.
A previous post of mine touches on this.
By Jonny on Apr 10, 2010