An issue that can crop up with Flex applications is the lack of any log information to match up against users' bug reports. It is possible to configure the debug version of Flash Player to record trace() output, but most users do not have this setup. Additionally, it is much more desirable to maintain this kind of information server-side, where the development team have control, rather than relying on users' whilst also clogging up their machines. A general approach to this problem is relatively obvious - send the information back to the server - but there are all sorts of ways this could be implemented. Of course, sending lots of logging information to the server creates an overhead in the application, so this should very much be considered on a case by case basis.

A solution that we use takes advantage of Flex's native logging API, allowing it to be transparently added or removed from an application while providing us with the benefits of a decent logging framework, for example logging levels. Flex provides the ILogger class, instances of which are obtained using Log's getLogger() method, to log messages. In turn, these messages are sent to one or more ILoggingTargets, the most common of which, TraceTarget outputs the logging information using the trace() method. The logging API documentation provides details and examples for how to get started with this basic logging setup.

On the Flex side, our solution consists of little more than a custom ILoggingTarget implementation, RemoteTarget, that attempts to send the logging information to a specified URL and the appropriate code to initialise the target. Note that RemoteTarget uses URLLoaders rather than HTTPServices because HTTPService itself uses the logging API, thereby causing an infinite loop. Another feature worth highlighting is that our implementation stops logging if it has encounters any errors when sending the information to the server, so that, for example, an application does not repeatedly hit a server that is unavailable. All source code and examples associated with this article can be found here.

package com.scottlogic.logging
{
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.net.URLVariables;

    import mx.logging.AbstractTarget;
    import mx.logging.ILogger;
    import mx.logging.LogEvent;
    import mx.utils.StringUtil;

    /**
     * ILoggingTarget implementation that sends logging details
     * to a remote service.  The following details are sent as
     * request parameters:
     *  - level: the log level as an integer (see LogEventLevel)
     *  - category: the log category
     *  - timestamp: as milliseconds from 1st Jan 1970
     *  - message: the log message
     */
    public class RemoteTarget extends AbstractTarget
    {
        /**
         * Flag to indicate whether remote logging is enabled
         * or not.  The RemoteTarget will automatically disable
         * itself if it encounters any errors.
         */
        private var enabled:Boolean = true;

        /**
         * Constructor
         */
        public function RemoteTarget()
        {
            super();
        }

        private var _url:String;
        /**
         * The URL of the service to which to log.
         */
        public function get url():String
        {
            return _url;
        }
        /**
         * @private
         */
        public function set url(value:String):void
        {
            _url = value;
        }

        /**
         * @private
         */
        override public function logEvent(event:LogEvent):void
        {
            // if there has been any issue with remote logging
            if (!enabled)
                return;
            // if no url is set then do nothing
            if (!_url || StringUtil.trim(_url).length == 0)
                return;

            // attempt to log to the server
            try
            {
                var variables:URLVariables = new URLVariables();
                variables.level = event.level;
                variables.category = ILogger(event.target).category;
                variables.timestamp = new Date().time;
                variables.message = event.message;
                var request:URLRequest = new URLRequest(_url);
                request.method = "POST";
                request.data = variables;
                var loader:URLLoader = createLoader();
                loader.load(request);
            }
            catch (e:Error)
            {
                enabled = false;
                trace(e.message);
                trace(e.getStackTrace());
            }
        }

        /**
         * Creates a URLLoader with all the appropriate event listeners.
         */
        private function createLoader():URLLoader
        {
            var loader:URLLoader = new URLLoader();
            loader.addEventListener(IOErrorEvent.IO_ERROR,
                                    ioErrorHandler,
                                    false, 0, true);
            loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,
                                    securityErrorHandler,
                                    false, 0, true);
            return loader;
        }

        /**
         * Disables the remote logging and traces debug information
         * when there is an IO error during logging to the remote server.
         */
        private function ioErrorHandler(event:IOErrorEvent):void
        {
            enabled = false;
            trace("ERROR - IO error in RemoteTarget");
            trace(event.text);
        }

        /**
         * Disables the remote logging and traces debug information
         * when there is a security error during logging to the remote
         * server.
         */
        private function securityErrorHandler(event:SecurityErrorEvent):void
        {
            enabled = false;
            trace("ERROR - Security error in RemoteTarget");
            trace(event.text);
        }
    }
}

Here is a small example showing how RemoteTarget can be used:

<mx:Application
    xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="horizontal"
    applicationComplete="initialise()"
>
    <mx:Script>
        <![CDATA[
            import com.scottlogic.logging.RemoteTarget;
            import mx.logging.Log;
            import mx.logging.LogEventLevel;
            import mx.logging.ILogger;

            private static const LOG:ILogger = Log.getLogger("RemoteLogging");

            private function initialise():void
            {
                var target:RemoteTarget = new RemoteTarget();
                target.url = ""; // set logging service URL here
                target.level = LogEventLevel.INFO;
                Log.addTarget(target);
            }
        ]]>
    </mx:Script>
    <mx:Label text="Text:" />
    <mx:TextInput id="input" />
    <mx:Button label="Log Text" click="LOG.info(input.text);" />
</mx:Application>

As the information is being sent back as a standard example server-side code I have included is in Java and consists of a relatively simple HttpServlet that parses the information and mirrors it onto its own logging framework, in this case log4j.