Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically Set Nlog Log Level per Logger Instance ASP.Net Core 2.x

Objective: To dynamically select which HTTP request I want verbose logging (different log level).

Overview: I have a ASP.Net core 2.1 web server running and once in production, if I need to debug an issue I want to be able to change the log level. I have found how to globally change the log level; however, changing the log level is persistent... aka, does not reset after each call to my controller.

    [HttpGet]
    public async Task<IEnumerable<string>> Get()
    {
        this.Logger.LogTrace("This should NOT get logged");
        SetMinLogLevel(LogLevel.Trace);
        this.Logger.LogTrace("This should be logged");

        return new string[] { "value1", "value2" };
    }

   public static void SetMinLogLevel(LogLevel NewLogLevel)
    {
        foreach (var rule in LogManager.Configuration.LoggingRules)
        {
            rule.EnableLoggingForLevel(NewLogLevel);
        }

        //Call to update existing Loggers created with GetLogger() or 
        //GetCurrentClassLogger()
        LogManager.ReconfigExistingLoggers();
    }

I want the requester to be able set a flag in their HTTP request (header or cookie) to enable a more verbose level of logging per request. That way I do not flood my logs with detailed logs from their requester.

Question: How to I dynamically set the Log Level per logger instance? (I believe that is the correct wording)

I am currently using NLog package 4.5.

like image 640
MATTHEW Avatar asked Nov 18 '22 05:11

MATTHEW


1 Answers

We know NLog 4.6.7 added support for using NLog Layout like ${gdc:globalLevel} to dynamically change level attributes at runtime. And the Better solution is to upgrade your NLog if it is possible.

Update: New solution I tried this code on version 4.5 and it works fine. It seems you don't need to upgrade your NLog version. In this case, all the configurations set programmatically. You can send your desired level in the header as loglevel. If you send loglevel in the header, it will be used. Otherwise, logLevel will be Error. See here, please.

Notice: Just use using NLog;. You don't need using Microsoft.Extensions.Logging;

[Route("api/[controller]/[action]")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        private readonly Logger _log = LogManager.GetCurrentClassLogger();

        [HttpGet]
        public async Task<IEnumerable<string>> Get()
        {
            var requestLogLevel = Request.Headers.SingleOrDefault(x => x.Key == "loglevel");
            LogLevel logLevel = LogLevel.Error;
            switch (requestLogLevel.Value.ToString().ToLower())
            {
                case "trace":
                    logLevel = LogLevel.Trace;
                    break;
                case "debug":
                    logLevel = LogLevel.Debug;
                    break;
                case "info":
                    logLevel = LogLevel.Info;
                    break;
                case "warn":
                case "warning":
                    logLevel = LogLevel.Warn;
                    break;
                case "error":
                    logLevel = LogLevel.Error;
                    break;
                case "fatal":
                    logLevel = LogLevel.Fatal;
                    break;
            }

            var config = new NLog.Config.LoggingConfiguration();
            var defaultMode = new NLog.Targets.FileTarget("defaultlog") { FileName = "log.txt" };
            config.AddRule(logLevel, LogLevel.Fatal, defaultMode);
            NLog.LogManager.Configuration = config;

            _log.Trace("Some logs");

            return new string[] { "value1", "value2" };
        }
    }

Solution 1) Upgrade NLog to 4.6.7 or later:

var config = new NLog.Config.LoggingConfiguration();

// Targets where to log to: File and Console
var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "file.txt" };
var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
            
// Rules for mapping loggers to targets            
config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
            
// Apply config           
NLog.LogManager.Configuration = config;

Solution 2) Change the configuration file programmatically: Because your version of NLog doesn't support change configuration automatically, we are going to change it programmatically:

[Route("api/[controller]/[action]")]
[ApiController]
public class HomeController : ControllerBase
{
    private readonly Logger _log = LogManager.GetCurrentClassLogger();

    // Special Authorization needed
    public bool ChangeToDebugMode()
    {
        try
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(AppDomain.CurrentDomain.BaseDirectory +  "nlog.config");
            XmlNode root = doc.DocumentElement;
            XmlNode myNode = root["include"].Attributes["file"];
            myNode.Value = "debugmode.config";
            doc.Save(AppDomain.CurrentDomain.BaseDirectory + "nlog.config");
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    // Special Authorization needed
    public bool RestToDefault()
    {
        try
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(AppDomain.CurrentDomain.BaseDirectory + "nlog.config");
            XmlNode root = doc.DocumentElement;
            XmlNode myNode = root["include"].Attributes["file"];
            myNode.Value = "defaultmode.config";
            doc.Save(AppDomain.CurrentDomain.BaseDirectory + "nlog.config");
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    [HttpGet]
    public async Task<IEnumerable<string>> Get()
    {
        _log.Trace("Some logs");

        return new string[] { "value1", "value2" };
    }
}

In this case, you need some change in your config file. You need to add autoReload=true to configuration. Now, when any configuration change, NLog automatically reload the configuration and you don't need to restart the application. You need to take a look at autoReload and include here

nlog.config

<?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">
  <include file="defaultmode.config" />
</nlog>

defaultmode.config

<?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">

  <targets>
    <target name="logfile" xsi:type="File" fileName="file.txt" />
  </targets>

  <rules>
    <logger name="*" minlevel="Debug" writeTo="logfile" />
  </rules>
  <!-- ... -->
</nlog>

debugmode.config

<?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">

  <targets>
    <target name="logfile" xsi:type="File" fileName="file.txt" />
  </targets>

  <rules>
    <logger name="*" minlevel="Trace" writeTo="logfile" />
  </rules>
  <!-- ... -->
</nlog>

I made two other config files. debugmode.config and defaultmode.config. By default in nlog.config file, deafultmode.config is included. When ChangeToDebugMode is called, it changes to debugmode.config and when RestToDefault is called, it changes to defaultmode.config. I used include and separate configuration into two files just for simlicity.

Solution 3) Based on your question: In this case, I used the code that you provided in your question. If you send the log level in your request header, it will be considered. If you don't send, it will use the default value that you set in the configuration. Thus, you don't need to change your application on the client-side. It works fine. Just send your desired log level when you are debugging.

[Route("api/[controller]/[action]")]
[ApiController]
public class HomeController : ControllerBase
{
    private readonly Logger _log = LogManager.GetCurrentClassLogger();

    [HttpGet]
    public async Task<IEnumerable<string>> Get()
    {
        var requestLogLevel = Request.Headers.SingleOrDefault(x => x.Key == "loglevel");
        LogLevel logLevel = LogLevel.Error;
        switch (requestLogLevel.Value.ToString().ToLower())
        {
            case "trace":
                logLevel = LogLevel.Trace;
                break;
            case "debug":
                logLevel = LogLevel.Debug;
                break;
            case "info":
                logLevel = LogLevel.Info;
                break;
            case "warn":
            case "warning":
                logLevel = LogLevel.Warn;
                break;
            case "error":
                logLevel = LogLevel.Error;
                break;
            case "fatal":
                logLevel = LogLevel.Fatal;
                break;
        }
        SetMinLogLevel(logLevel);               

        _log.Trace("Some logs.");

        return new string[] { "value1", "value2" };
    }

    public static void SetMinLogLevel(LogLevel NewLogLevel)
    {
        foreach (var rule in LogManager.Configuration.LoggingRules)
        {
            rule.EnableLoggingForLevel(NewLogLevel);
        }

        //Call to update existing Loggers created with GetLogger() or 
        //GetCurrentClassLogger()
        LogManager.ReconfigExistingLoggers();
    }
}

The problem is, this situation needs to send the log level every time. In these screenshots, you see how to send log level in debugging mode. The Postman result

output text file

like image 160
Saeid Amini Avatar answered Dec 26 '22 02:12

Saeid Amini