- 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