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

て烟熏妆下的殇ゞ 提交于 2021-02-07 18:17:34

问题


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.


回答1:


Maybe you could use a session-cookie to control whether debug-mode is enabled:

<targets>
    <target type="file" name="logfile" filename="applog.txt" />
</targets>
<rules>
    <logger name="*" minlevel="Off" writeTo="logfile" ruleName="debugCookieRule">
      <filters defaultAction="Log">
         <when condition="'${aspnet-session:EnableDebugMode}' == ''" action="Ignore" />
      </filters>
    </logger>
</rules>

Then activate the session-cookie like this:

public void SetMinLogLevel(LogLevel NewLogLevel)
{
    var cookieRule = LogManager.Configuration.FindRuleByName("debugCookieRule");
    if (cookieRule != null)
    {
        cookieRule.MinLevel = NewLogLevel;

        // Schedule disabling of logging-rule again in 60 secs.
        Task.Run(async () => { await Task.Delay(60000).ConfigureAwait(false); cookieRule.MinLevel = LogLevel.Off; LogManager.ReconfigExistingLoggers(); });

        // Activate EnableDebugMode for this session
        HttpContext.Session.SetString("EnableDebugMode", "Doctor has arrived");
    }

    LogManager.ReconfigExistingLoggers();  // Refresh loggers
}

If session-cookies and ${aspnet-session} is not wanted, then NLog.Web.AspNetCore have other options for extracting HttpContext-details. See also: https://nlog-project.org/config/?tab=layout-renderers&search=package:nlog.web.aspnetcore




回答2:


Rather than trying to customize the NLog logging levels (which will affect the entire process), I think you should go for a solution that modifies the log levels of the log statements themselves.

To make this work, you'll need the following:

  1. A mechanism by which to identify requests for which you want debug logging
  2. A wrapper for the logger so that you can dynamically override the log level

The first requirement is simple enough - either set a cookie or a custom HTTP header, and check for the presence of either. You'll need to make this result of this check available to your LogWrapper instance, so that it knows when it should do something special.

The LogWrapper must be instantiated per request, so that the instance isn't shared across requests. The simplest way to do this is to create it on-demand in the constructor of your controller (but you could also wire it up to the DI container for automatic injection).

This would look something like this:

public class HomeController : Controller
{
    private readonly LogWrapper _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        var isDebugRequest = ...;
        _logger = new LogWrapper<HomeController>(logger, isDebugRequest);
    }    

    ...
}

The basics of creating a log wrapper for NLog is explained here, but it looks like you're already using the wrapper created for Microsoft.Extensions.Logging, so you'd need to wrap that interface instead:

public class LogWrapper<T> : Microsoft.Extensions.Logging.ILogger
{
    private readonly ILogger<T> _logger;
    private readonly bool _debug;

    public LogWrapper(ILogger<T> logger, bool isDebug)
    {
        _logger = logger;
        _debug = isDebug;
    }    

    public void Log<TState>(LogLevel logLevel,
                            EventId eventId,
                            TState state,
                            Exception exception,
                            Func<TState, Exception, string> formatter)
    {
        if (_debug) 
        {
            // override log level here
            _logger.Log(LogLevel.Warning, eventId, state, exception, formatter); 
        }
        else 
        {
            _logger.Log(logLevel, eventId, state, exception, formatter);
        }
    }

    // ILogger has two other methods you'll need to implement
}    

The downside to this approach is that log statements won't have their original log level, which may or may not be important for your use case.




回答3:


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.




回答4:


NLog 4.6.7 allows you to use Layouts in loggingrules filters for minLevel / maxLevel

You can have a NLog-Config-Variable with default loglevel, and then create a hidden method on your web-app that modifies the NLog-Config-Variable and calls ReconfigExistingLoggers().

Then setup a Timer that restores that NLog-Config-Variable to its orignal value after 30 secs. And also calls ReconfigExistingLoggers().

See also: https://github.com/NLog/NLog/wiki/Filtering-log-messages#semi-dynamic-routing-rules



来源:https://stackoverflow.com/questions/51754425/dynamically-set-nlog-log-level-per-logger-instance-asp-net-core-2-x

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!