Being a newbie into AngularJS, I have overlooked the importance of client side logging until some non-reproducible client-side errors occurred and I realized that they were not logged.
My web application is developed using MVC 5 and AngularJS and my intention is to have a good client side logging that sends errors and debug information to a database table.
AngularJS part is based on the great code provided by Nadeem Shabir here. Basically, all Angular's unhandled exceptions are caught and logged, but a logging service also allows to arbitrary send debug information:
/**
factory that provides a wrapper for stacktrace's print method
*/
coreModule.factory(
"traceService",
function () {
return ({
print: printStackTrace
});
}
);
/**
* Override Angular's built in exception handler, and tell it to
* use our new exceptionLoggingService which is defined below
*/
coreModule.provider(
"$exceptionHandler", {
$get: function (exceptionLoggingService) {
return (exceptionLoggingService);
}
}
);
/**
* Exception Logging Service, currently only used by the $exceptionHandler
* it preserves the default behaviour ( logging to the console) but
* also posts the error server side after generating a stacktrace.
*/
coreModule.factory(
"exceptionLoggingService", function ($log, $window, $rootUrlService, traceService) {
function error(exception, cause) {
// preserve the default behaviour which will log the error
// to the console, and allow the application to continue running.
$log.error.apply($log, arguments);
// now try to log the error to the server side.
try {
var errorMessage = exception.toString();
// use our traceService to generate a stack trace
var stackTrace = traceService.print({ e: exception });
// use AJAX (in this example jQuery) and NOT
// an angular service such as $http
$.ajax({
type: "POST",
url: $rootUrlService.rootUrl + "Logging/LogException",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace,
cause: (cause || "")
})
});
} catch (loggingError) {
$log.warn("Error server-side logging failed");
$log.log(loggingError);
}
}
return (error);
}
);
/**
* Application Logging Service to give us a way of logging
* error / debug statements from the client to the server.
*/
coreModule.factory(
"applicationLoggingService", function ($log, $window, $rootUrlService) {
return ({
error: function (message) {
// preserve default behaviour
$log.error.apply($log, arguments);
// send server side
$.ajax({
type: "POST",
url: $rootUrlService.rootUrl + "Logging/Log",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: message,
type: "error"
})
});
},
debug: function (message) {
$log.log.apply($log, arguments);
$.ajax({
type: "POST",
url: $rootUrlService.rootUrl + "Logging/Log",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: message,
type: "debug"
})
});
}
});
}
);
MVC controller code (server-side)
public class LoggingController : BaseController
{
[HttpPost]
public void LogException(String url, String message, String type, List<String> stackTrace, String cause)
{
String stackTraceStr = String.Join(Environment.NewLine, stackTrace);
Logger.Log(LogLevel.Error, "Angular/Client error {0} at URL = {1}, client stack trace = {2}, cause = {3}", message, url, stackTraceStr, cause);
}
[HttpPost]
public void Log(String url, String message, String type)
{
var level = type.Contains("error") ? LogLevel.Error : LogLevel.Info;
Logger.Log(level, "Angular/Client custom info {0} at URL = {1}, type = {2}", message, url, type);
}
}
public class BaseController : Controller
{
[Inject]
public ILoggingService Logger { get; set; }
public BaseController()
{
// trigger the dependency injection on properties
NinjectWebCommon.PerformInjectionOn(this);
}
}
Logging service
public class LoggingService : ILoggingService
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public void Log(LogLevel level, String message)
{
logger.Log(level, message);
}
public void Log(LogLevel level, String format, params object[] parameters)
{
logger.Log(level, format, parameters);
}
public void Log(LogLevel level, IList<String> list)
{
String output = String.Join("; ", list);
Log(level, output);
}
public void Log(LogLevel level, String message, Exception exc)
{
try
{
GlobalDiagnosticsContext.Set("FullExceptionInfo", exc.ToString());
logger.Log(level, message, exc);
}
finally
{
GlobalDiagnosticsContext.Remove("FullExceptionInfo");
}
}
public void Log(LogLevel level, String format, Exception exc, params object[] parameters)
{
try
{
GlobalDiagnosticsContext.Set("FullExceptionInfo", exc.ToString());
logger.Log(level, format, parameters);
}
finally
{
GlobalDiagnosticsContext.Remove("FullExceptionInfo");
}
}
}
In order to reduce logging overhead, async logging is enabled for all database writes.
NLog configuration
<targets async="true">
<target name="database" type="Database">
<connectionString>
<!-- connection string comes here -->
</connectionString>
<commandText>
insert into dbo.nlog
(log_date, log_level_id, log_level, logger, log_message, machine_name, log_user_name, call_site, thread, exception, stack_trace, full_exception_info)
values(@timestamp, dbo.func_get_nlog_level_id(@level), @level, NULL /*@logger*/, @message, @machinename, @username, NULL /*@call_site */, @threadid, @log_exception, @stacktrace, @FullExceptionInfo);
</commandText>
<parameter name="@timestamp" layout="${longdate}"/>
<parameter name="@level" layout="${level}"/>
<parameter name="@logger" layout="${logger}"/>
<parameter name="@message" layout="${message}"/>
<parameter name="@machinename" layout="${machinename}"/>
<parameter name="@username" layout="${windows-identity:domain=true}"/>
<parameter name="@call_site" layout="${callsite:filename=true}"/>
<parameter name="@threadid" layout="${threadid}"/>
<parameter name="@log_exception" layout="${exception}"/>
<parameter name="@stacktrace" layout="${stacktrace}"/>
<parameter name="@FullExceptionInfo" layout="${gdc:FullExceptionInfo}"/>
</target>
</targets>
Can this logging mechanism be improved? some pitfalls I might fall into?