集成Javascript Logging on MVC or Core

时间:2023-03-08 20:00:15

ASP.NET Core provides us a rich Logging APIs which have a set of logger providers including: ConsoleLoggerPtoviderAzureAppServicesDiagnosticsLoggerProviderEventLogLoggerProvider and much more.

This let C# developers happy than before, because you need to implement them loggers yourself in the past, however a lot of us writing JavaScript code in almost web applications which is little hard, but find the client-side exceptions & errors are even harder.

There are many client-side loggers that you can name it, which are fit our needs, but today I wanna talk about the client-side logging from different angle. Me and you as developers suffer a lot from unexpected javascript error that happen occasionally, sometimes the reason is silly such missing a curly braces or broke a link .. etc.

With that I was thinking last few days that it would be nice to integrate the client-side & server-side logs, so all the logs will be logged from the ASP.NET Core logger providers that we like and love instead of managing two different logger providers for both client-side & server-side.

Logging JavaScript Events

Basically the idea is very simple, I need to inject the client-side logging APIs script into our view, after that I need the JavaScriptLoggingMiddleware to listening to the upcoming script logs and forward them to ASP.NET Core logger providers.

With that we're ready to show some code, but before that I need to mention that I didn't introduce a new logging APIs, but I could, so the javascript console object is enough for logging. In both cases we need to intercept the console logs as the following:

(function () {
var trace = console.trace;
var debug = console.debug;
var info = console.info;
var warn = console.warn;
var error = console.error; console.trace = function (message) {
log(logLevel.Trace, message);
trace.call(this, arguments);
}; console.debug = function (message) {
log(logLevel.Debug, message);
debug.call(this, arguments);
}; console.info = function (message) {
log(logLevel.Information, message);
info.call(this, arguments);
}; console.warn = function (message) {
log(logLevel.Warning, message);
warn.call(this, arguments);
}; console.error = function (message) {
log(logLevel.Error, message);
error.call(this, arguments);
};
})();

The log is a method that I created to post the actual logs to the server, which will handled by the JavaScriptLoggingMiddleware that shown below.

At this point I was wondering whether to store jsLogger script into the disk and render it using a tag helper or not!! after awhile I inspired by Application Insights and decided to store it into a resx file and retrieve them later using JavaScriptLoggingSnippet.

public class JavaScriptLoggingMiddleware
{
private readonly ILogger _logger;
private readonly RequestDelegate _next; public JavaScriptLoggingMiddleware(ILoggerFactory loggerFactory, RequestDelegate next)
{
_logger = loggerFactory.CreateLogger<JavaScriptLoggingMiddleware>();
_next = next;
} public async Task Invoke(HttpContext context)
{
if (context.Request.Path == "/log" && context.Request.Method == "POST" && context.Request.HasFormContentType)
{
var form = await context.Request.ReadFormAsync();
var level = Convert.ToInt32(form["level"].First());
var message = form["message"].First(); switch ((LogLevel)level)
{
case LogLevel.Trace:
_logger.LogTrace(message);
break;
case LogLevel.Debug:
_logger.LogDebug(message);
break;
case LogLevel.Information:
_logger.LogInformation(message);
break;
case LogLevel.Warning:
_logger.LogWarning(message);
break;
case LogLevel.Error:
_logger.LogError(message);
break;
default:
return;
}
}
else
{
await _next(context);
}
}
}

If you look closely to the code above, you will notice that the middleware listening for specific url, when it hit I need to map the client-side log levels with the server-side once, after that I log them normally.

I can finally log JavaScript Exceptions!

Last but not least, sometimes we come up to a situation that we need to catch the global javascript exception that may occur for whatever reason could be. window.onerror is a good place to catch such exception.

I added a JavaScriptLoggingOptions which is shown below to make things configurable in the way that you want.

public class JavaScriptLoggingOptions
{
public bool HandleGlobalExceptions { get; set; }
}

By adding this simple property, the JavaScriptLoggingSnippet is now able to choose the proper script to render in the view.

public class JavaScriptLoggingSnippet
{
private readonly JavaScriptLoggingOptions _loggingOptions; private static readonly HtmlString JavaScriptLoggingScript = new HtmlString(Resources.Script); private static readonly HtmlString JavaScriptLoggingGlobalExceptionHandlingScript = new HtmlString(Resources.GlobalExceptionHandlingScript); public JavaScriptLoggingSnippet(IOptions<JavaScriptLoggingOptions> loggingOptions)
{
_loggingOptions = loggingOptions.Value;
} public HtmlString Script =>
_loggingOptions.HandleGlobalExceptions ? FullScript : JavaScriptLoggingScript; private static HtmlString FullScript => JavaScriptLoggingScript.Concat(HtmlString.NewLine,
JavaScriptLoggingGlobalExceptionHandlingScript);
}

What it takes to make everything happen?

We need few steps to make this happen:

First we need to configure the JavaScriptLogging service in the ConfigureServices method

services.AddJavaScriptLogging(options =>
{
options.HandleGlobalExceptions = true;
});

After that adding the JavaScriptLoggingMiddleware to the Configure method

app.UseJavaScriptLogging();

Finally add the following line into your view or layout page to render the jsLogger script.

@Html.Raw(JavaScriptLoggingSnippet.Script)

You can download the source code for this post from my jsLogger repository on GitHub.