- AngularJS
Black Sand Solutions
Enhancing AngularJS Logging using Decorators
- 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] < 12) ? "AM" : "PM"; // Convert hour from military time time[0] = (time[0] < 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 < 3; i++) { if (time[i] < 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