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