3
\$\begingroup\$

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?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.