Learn NLog Advanced Usage / Change NLog Configuration Per Request

Logging is an essential part of the application, especially when you want to monitor traffic when communicating with an external client that is having some problems connecting to your site.

Let's say a very obscure error is happening with regard to third-party software you are using, and the software provider is asking for debug-level logs in order to help you.

So, the solution is simple: add a logging component like NLog and log into a file by configuring the nlog.config file. But many times, your application may not allow you a simple implementation.

The complications appear when your application is a multi-tenant deployment, or if you can't modify nlog.config or appsettings.json after the deployment, and what if you need to temporarily enable the Debug logging for a specific library only or any temporary logging for that matter?

If we just add an NLog to the multi-tenant application with five clients, then all five clients will start writing logs in the same location, be it a file or a database. And what if the deployed code is not possible to modify (like a read-only zipped version)?

There is a solution for all of the above. NLog libraries provide all the necessary features to resolve the above problems.

The best part is that NLog allows you to use a new instance of NLog per each request and allows you to modify the logger instance just for this scope (per request).

Let's address the issues one by one.

To start, let's use a middleware to set the NLog in a generic manner.

In the middleware class, we will set all the dynamic properties specific to the tenant. In my case, they are

DB connection string; logger filters for specific libraries (like Microsoft, ThirdPartyLib(substitute it with any namespace you want), Nlog.API (current application), and everything(*)).

In the Nlog.config file, we set the logger using the following code (please note mdlc:Nlog_mainDebug). I found that this is the only way to modify the logger dynamically - by using the 'filters' tag.

For loggers

<logger name="Nlog.API*" minlevel="Debug" writeTo="dbTarget" ><!--only works if Nlog_mainDebug is Yes-->
        <filters defaultAction="Log">
            <when condition="'${mdlc:Nlog_mainDebug}' != 'Yes'" action="Ignore" /><!--comparisson is case-sensetive-->
        </filters>
</logger>

For the database target we set a connectionString with a dynamic variable (please note mdlc:NLog_DbCon).

<target xsi:type="Database"
name="dbTarget"
connectionString="${mdlc:NLog_DbCon}"
commandText="INSERT INTO NLogs(CreatedOn,Message,Level,Exception,StackTrace,Logger,Url)
            VALUES (@datetime,@msg,@level,@exception,@trace,@logger,@url)"
            >
        <parameter name="@datetime" layout="${date}" />
        <parameter name="@msg" layout="${message}" />
        <parameter name="@level" layout="${level}" />
        <parameter name="@exception" layout="${exception}" />
        <parameter name="@trace" layout="${stacktrace}" />
        <parameter name="@logger" layout="${logger}" />
        <parameter name="@url" layout="${aspnet-request-url}" />
</target>

In the middleware, the properties are set to specific values (TenantMiddleware.cs).

NLog.ScopeContext.PushProperty("Nlog_Microsoft",Constants_NLogMicrosoft);
NLog.ScopeContext.PushProperty("Nlog_ThirdPartyLib", Constants_NLogThirdPartyLib);
NLog.ScopeContext.PushProperty("Nlog_mainDebug", Constants_NLogMainDebug);
NLog.ScopeContext.PushProperty("Nlog_mainWarning", Constants_NLogMainWarning);
NLog.ScopeContext.PushProperty("Nlog_EverythingElseWarn", Constants_NLogEverythingElseWarn);
NLog.ScopeContext.PushProperty("NLog_DbCon", connectionString);

In this sample, the connection string is static, but it can be retrieved based on the current domain + subdomain dynamically. Depending on the subdomain, the connection string could vary.

The same is true for the logger enabling. The source of the values could be dynamic (like a database).

Testing

Try Cool, Fantastic, and WeatherForecast APIs by making requests to all three controllers.

Swagger

As a result, you should see the logged database rows.

APIs

There are multiple possible scenarios. And here are a few.

In a day to day logging, I would set only one logger, which is Warning for everything like:

<logger name="*" minlevel="Warn" writeTo="dbTarget" >

When you need to debug a specific library, then additionally enable this:

<logger name="ThirdPartyLib*" minlevel="Debug" writeTo="dbTarget">

And, of course, set the DB destination per URL if multi-tenant. If not multi-tenant, for simplicity, set the DB connection in nlog.config instead. I'm demonstrating the most complex case here to see what's possible.

Scenarios

The source code is attached, and more details can be found in the Nlog Multi-tenant Strategy.txt file inside the project. Cheers!