Sunday, March 31, 2019

[JSNLog] Logging on JS and ASP.NET Core 2.X


 Vue.js   ASP.NET Core    NLog    JSNLog 


▌Introduction


The scenario is that we want to log on the front-end and write to the backend.
We will us NLog and JSNLog to fulfill the purpose.




▋Related articles






▌Environment


▋Visual Studio 2017 community
▋NPM: 5.6.0                                     
▋ASP.NET Core 2.2.101
▋Vue CLI: 3.5.1


▌Packages

We will install the following packages into our ASP.NET Core project thru Nuget:

3.  System.Data.SqlClient (Optional, install it when you wanna keep the log in sql server)
4.  JSNLog



▌NLog config


We have to update NLog settings in NLog.config.

▋NLog.config

Copy the following content to your NLog.config, and modify the file pathes, database connection string, targets and rules.

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Off"
      internalLogFile="C:\Logs\internal-nlog.txt">

  <!-- enable asp.net core layout renderers -->
  <extensions>
    <add assembly="NLog.Web.AspNetCore"/>
  </extensions>

  <!--LOG format-->
  <variable name="Layout" value="${longdate} | ${level:uppercase=true} | ${logger} | ${message} ${newline}"/>
  <variable name="LayoutFatal" value="${longdate} | ${level:uppercase=true} | ${logger} | ${message} | ${exception:format=tostring} ${newline}"/>
  <variable name="LayoutEvent" value="${date}: ${message} ${stacktrace}"/>

  <!--LOG path -->
  <variable name="LogTxtLocation" value="${basedir}/App_Data/Logs/${shortdate}/${logger}.log"/>
  <variable name="LogTxtLocationFatal" value="${basedir}/App_Data/Logs/${shortdate}/FatalFile.log"/>
  <variable name="ProjectName" value="MyProject"/>

  <targets>
    <!--Text file-->
    <target name="File" xsi:type="File" fileName="${LogTxtLocation}" layout="${Layout}" />
    <target name="FileFatal" xsi:type="File" fileName="${LogTxtLocationFatal}" layout="${LayoutFatal}"/>
    <!--Event log-->
    <!--<target name="Event" xsi:type="EventLog" source="${ProjectName}" log="Application" layout="${LayoutEvent}" />-->
    <!--Log Viewer(Optional)-->
    <target name="Sentinel" xsi:type="NLogViewer" address="udp://127.0.0.1:3333"/>
    <!--Database-->
    <target name="LogDatabase" xsi:type="Database" >
      <connectionString>
        Data Source=Your-DB-Server-name;Initial Catalog=Your-DB-Name;Integrated Security=True;
      </connectionString>
      <commandText>
         INSERT INTO AspnetCoreLog (
              Application, Logged, Level, Message,
              Logger, CallSite, Exception
              ) values (
              @Application, @Logged, @Level, @Message,
              @Logger, @Callsite, @Exception
              );
      </commandText>
    
      <parameter name="@application" layout="AspNetCoreNlog" />
          <parameter name="@logged" layout="${date}" />
          <parameter name="@level" layout="${level}" />
          <parameter name="@message" layout="${message}" />
          <parameter name="@logger" layout="${logger}" />
          <parameter name="@callSite" layout="${callsite:filename=true}" />
          <parameter name="@exception" layout="${exception:tostring}" />
    </target>
  </targets>

  <rules>
    <logger name="*" levels="Trace, Debug, Info, Warn"  writeTo="File" />
    <logger name="*" levels="Error, Fatal"                           writeTo="FileFatal,LogDatabase" />
    <logger name="*" levels="Trace, Debug, Info, Warn, Error, Fatal" writeTo="Sentinel" />
  </rules>
</nlog>






▌Config ASP.NET core



▋Program.cs

Use NLog for DI logger.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .ConfigureLogging(logging =>
            {
                logging.ClearProviders();
                logging.SetMinimumLevel(LogLevel.Trace);
            })
            .UseNLog();


And we can have other NLog config, like NLog.Development.config, for the development environment.
We can optionally load it depends on the environment variable, ASPNETCORE_ENVIRONMENT.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                var env = hostingContext.HostingEnvironment;
                if (env.IsDevelopment())
                {
                    env.ConfigureNLog($"NLog.{environment}.config");
                }
            })
            .ConfigureLogging(logging =>
            {
                logging.ClearProviders();
                logging.SetMinimumLevel(LogLevel.Trace);
            })
            .UseNLog();





▋Optional: Cross-Origin Resource Sharing (CORS)

If we are going to use the cross-domain logging server, we need to set the allowed domain to JSNLog in backend.
Open Startup.cs and update the Configure function,

using JSNLog;
using Microsoft.Extensions.Logging;

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
 {
            // Configure JSNLog
            var jsnlogConfiguration =
                  new JsnlogConfiguration
                  {
                      corsAllowedOriginsRegex = "^https?://localhost:8080"
                  };
            app.UseJSNLog(new LoggingAdapter(loggerFactory), jsnlogConfiguration);

//…skip
 }



And don’t forget to enable CORS on the ASP.NET Core.

▋Startup.js

public void ConfigureServices(IServiceCollection services)
{
            //Enable CORS
            services.AddCors(options =>
            {
                options.AddPolicy("AllowSpecificOrigin",
                    builder =>
                    builder.WithOrigins("http://localhost:8080")                   
                    // builder.AllowAnyOrigin()
                    .AllowAnyHeader()
                    .AllowCredentials()
                    .AllowAnyMethod()
                   );
            });

           
            #endregion
}

And applying CORS globally as following,

public void Configure(IApplicationBuilder app)
{
   app.UseCors("AllowSpecificOrigin");
   app.UseMvc();
}


I made a mistake to enable CORS ONLY ON Controller’s actions (as following) and the cross-domain requests are denied from front-end logging.

 // Enable CORS for every MVC actions
 services.Configure<MvcOptions>(options =>
 {
    options.Filters.Add(new CorsAuthorizationFilterFactory("AllowSpecificOrigin"));
 });







▌Start logging


▋Backend (MVC controller)

[ApiController]
    public partial class MyController : ControllerBase
    {
        private readonly ILogger<MyController> _logger = null;

        public MyController(
            ILogger<MyController> logger)
        {
            this._logger = logger;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            this._logger.LogDebug("Testing");
            //…skip
        }
    }
}




▋Frontend

First install JSNLog by NPM.

$ npm intall jsnlog

Now you can log like this,

import {JL} from "jsnlog";

JL().trace("Trace...");
 JL().debug("Debug...");
 JL().info("Info...");
 JL().warn("Warn...");
 JL().error("Error...");
 JL().fatal("Fatal...");


If the front-end and back-end are cross-domain, create new AjaxAppender to assign the specified logging server

▋jsnlogger.js

import {JL} from "jsnlog";

const appender = JL.createAjaxAppender("minitisAppender")
appender.setOptions!({
    url: `${process.env.YOUR_HOST_BACKEND_URL}/jsnlog.logger`
});

export default JL("MiniTis-Vue").setOptions({
    "appenders": [appender]
});


Then log by the custom JSNLog object,

import JL from "jsnlogger";

@Component({
  components: {
  },
})
export default class XXXX extends Vue {
    public mounted() {
        JL.trace("Trace...");
        JL.debug("Debug...");
        JL.info("Info...");
        JL.warn("Warn...");
        JL.error("Error...");
        JL.fatal("Fatal...");
    }
}



▋Result




▌Reference