Enhancing AngularJS Logging using Decorators

11 September 2015By Rich @Black Sand Solutions
  • AngularJS

I recently decided to improve my approach to logging in AngularJS. Shortly before this decision I was simply...

Enhancing AngularJS Logging using Decorators

Shortly before this decision I was simply including calls to console.log whenever I needed to gain some extra insight into what was happening in my code - I know!

Most of the time I remembered to remove them - but not always, eeek.

In short I was looking for a solution that:

  • did not break older browsers (< IE8) if accidentally left in the code
  • did not break older browsers (< IE8) if intentionally left in the code
  • could return some more useful (and customisable) information
  • could easily be turned ON and OFF.

As I always I started with a spot of Googling which led me to this excellent post (and blog) of one Thomas Burleson.

His code did pretty much everything I was looking to do with one exception:

  • disabling / enabling all log messages

Also, his approach uses a mixture of AngularJS and RequireJS - whilst this looks interesting it was not something I had time to investigate right now and I also needed a solution that would work with plain vanilla AngularJS.

Aside from not using RequireJS it differs from Thomas's implementation in a few small ways.

$interpolate

Thomas uses Douglas Crockford's supplant function; which provides features to build complex strings with tokenized parameters.

However the AngularJS $interpolate service can be used for the same purpose- although it is a little bit more verbose...

  • First you need to define the expression

var exp = $interpolate("{{time}} - {{className}}{{data}}");

  • Then provide the arguments in a context

var context = { time: now, data: args[0], className: className };

  • An then invoke the expression to get the interpolated result

var result = exp(context);

Disable All Messages

Unless I missed something, with Thomas's solution it was not possible to disable ALL messages.

The $log.debug() method could be disabled by calling

$log.debugEnabled(false)

but all other messages would continue to display.

Solution

To 'fix' this I've updated the interface a little.
The following method is used to both enabled AND enhance the $log service.
The enhance method does the same as in Thomas's implementation (but using $interpolate).
The notable difference is the setting of the _enable flag.
This is used later on to enable / disable the messages, as shown in the second code block.

function debugEnabled($log, enable)
       {
            ///
            /// Enable / Disable ALL logging mesages
            /// Named debugEnabled as synonymous with original $log.debugEnabled method
            ///
            _enabled = enable;

            return enhance($log);
       }

Disable messages if _enable is not true.

        function prepareLogFn(logFn, className )
        {
            var enhancedLogFn = function () {
                // if logging is not enabled then return an empty function
                // this will replace the the existing angular log method, thus disabling it.
                if (!_enabled) {
                    return function () { };
                }
         ...
      }

The complete solution is below...

(function () {

    "use strict";

    angular.module("BlackSand.Logging")
    .provider("bsLogger", function() {

        this.$get = [
            '$interpolate', function ( $interpolate)
            {
                //return the factory as a provider, that is available during the configuration phase
                return new bsLoggerService( $interpolate);
            }
        ];
    });

    function bsLoggerService($interpolate) {
        ///
        /// A service that decorates the angular $log service messages with additional information
        /// Provides a way for disabling ALL log messages (not just debug - as per      $logProvider.debugEnabled(false)
        ///

        var _$log = undefined;
        var _enabled = false;

        var service = {
            addClassName: addClassName,
            debugEnabled: debugEnabled
        };

        return service;

        ///API METHODS /////////////////////

        function debugEnabled($log, enable)
        {
            ///
            /// Enable / Disable ALL logging mesages
            /// Called debugEnabled as synonymous with original $log.debugEnabled method
            ///
            _enabled = enable;

            return enhance($log);
        }

        function addClassName(className) {
            ///
            /// Call this method in any object for which you wish the object name to inserted into the log message
            ///
            /// E.g. in HomeController.js, these method calls...
            /// $log = $log.getInstance("HomeController");
            /// $log.log('test');
            ///
            /// will result in console log like
            /// 9/11/2015 1:06:59 PM - HomeController::test

            className = (className !== undefined) ? className + "::" : "";

            return {
                log: prepareLogFn(_$log.log, className),
                info: prepareLogFn(_$log.info, className),
                warn: prepareLogFn(_$log.warn, className),
                debug: prepareLogFn(_$log.debug, className),
                error: prepareLogFn(_$log.error, className)
            };
        }

        ///PRIVATE METHODS /////////////////////

        function capture$Log($log) {
            _$log = (function ($log) {
                return {
                    log: $log.log,
                    info: $log.info,
                    warn: $log.warn,
                    debug: $log.debug,
                    error: $log.error
                };
            })($log);
        }

        function enhance($log) {
            capture$Log($log);

            ///
            /// Enhance log messages with timestamp
            ///
            $log.log = prepareLogFn($log.log);
            $log.info = prepareLogFn($log.info);
            $log.warn = prepareLogFn($log.warn);
            $log.debug = prepareLogFn($log.debug);
            $log.error = prepareLogFn($log.error);

            // Add special method to AngularJS $log
            $log.addClassName = addClassName;

            return $log;
        }

        function prepareLogFn(logFn, className )
        {
            var enhancedLogFn = function () {
                // if logging is not enabled then return an empty function
                // this will replace the the existing angular log method, thus disabling it.
                if (!_enabled) {
                    return function () { };
                }

                var args = Array.prototype.slice.call(arguments),
                    now = timeStamp();

                // Prepend timestamp
                var exp = $interpolate("{{time}} - {{className}}{{data}}");
                var context = { time: now, data: args[0], className: className };
                var result = []; //apply requires an array
                result.push(exp(context));

                logFn.apply(null, result);
            };

            // Special... only needed to support angular-mocks expectations
            enhancedLogFn.logs = [];

            return enhancedLogFn;
        }

        function timeStamp()
        {
            ///
            /// Create nicely formatted timestamp for use in log messages.
            ///
            var now = new Date();

            // create arrays with current month, day, year and time
            var date = [now.getMonth() + 1, now.getDate(), now.getFullYear()];
            var time = [now.getHours(), now.getMinutes(), now.getSeconds()];
            var suffix = (time[0] &lt; 12) ? "AM" : "PM";

            // Convert hour from military time
            time[0] = (time[0] &lt; 12) ? time[0] : time[0] - 12;
            time[0] = time[0] || 12;

            // If seconds and minutes are less than 10, add a zero
            for (var i = 1; i &lt; 3; i++) {
                if (time[i] &lt; 10) {
                    time[i] = "0" + time[i];
                }
            }

            return date.join("/") + " " + time.join(":") + " " + suffix;
        }
        
    }
    })();

And module

    angular.module("BlackSand.Logging")
        .config(["$provide", function ($provide) {

            $provide.decorator('$log', ["$delegate", "$injector", function ($delegate, $injector)
            {
                var logger= $injector.get("bsLogger");

                //turn on logging here - it will be false by default
                return logger.debugEnabled($delegate, false);

            }]);
        }]);

Next Steps

Integrate server side logging
http://jsnlog.com/Documentation/GetStartedLogging/AngularJsErrorHandling

All Posts