每个请求的日志级别

目录

问题描述

每个应用程序的日志级

每个请求的日志级别

缺点

结论


在这里,我将描述如何为Web应用程序的每个请求设置单独的日志级别。

问题描述

那么我们在谈论什么呢?假设您有一个Web服务。有一刻,它在生产环境中开始失败。但它只在一些请求上失败。例如,它仅对一个用户失败。或仅适用于特定终端。当然,我们必须找到原因。在这种情况下,日志应该有所帮助。

我们可以在代码中插入足够的日志指令来查明失败的不同原因。日志指令通常是相关联的一些日志级别(DebugInfoWarning...)的消息。此外,记录器具有自己的日志级别。因此,所有级别高于记录器级别的消息都将写入日志接收器(文件,数据库,...)。如果消息的级别低于记录器级别,则将丢弃该消息。

当应用程序运行良好时,我们希望尽可能少的日志消息,以保持日志接收器的大小。同时,如果应用程序失败,我们希望尽可能多的日志消息能够找到问题的原因。这里的困难在于,我们通常会为应用程序中的所有记录器设置一个级别。如果一切正常,我们会保持这个水平(例如Warning)。如果我们需要调查失败,我们将此级别设置为低(例如Debug)。

每个应用程序的日志级

当我们将应用程序日志级别设置为低时,突然会在日志接收器中显示大量消息。这些消息将来自许多请求,并且将被洗牌,因为可以同时处理许多请求。它会导致一些潜在的困难:

  • 如何从有问题的请求中分离消息,从没有问题的请求中分离消息?
  • 没有问题的请求仍然会花时间将消息写入日志接收器,尽管这些消息将不会被使用。
  • 没有问题的请求消息仍然占用了日志接收器中的空间,尽管这些消息将不会被使用。

那么,所有这些困难都不是很严重。要将请求的消息与请求的消息分开,我们可以使用相关ID。所有现代日志记录系统都支持按字段过滤消息。

性能通常也不是一个大问题。记录系统支持异步写入,因此大量日志记录的影响不应太严重。

现在存储空间相对便宜,所以这也不是一个大问题。特别是如果我们可以删除旧记录。

不过,我们可以做得更好吗?我们可以根据某些条件为每个请求设置单独的日志级别吗?我想在这里调查这个问题。

每个请求的日志级别

请允许我为我将实施的解决方案制定要求。应该有一种方法可以为每个请求单独设置日志级别。必须有一种灵活的方法来定义为请求选择日志级别的条件。并且应该可以在运行时更改这些条件而无需重新启动应用程序。

背景已经介绍完了。让我们开始吧。

我将使用.NET Core编写一个简单的Web API应用程序。它将拥有唯一的控制器: 

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    ...

    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        Logger.Info("Executing Get all");

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

    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        Logger.Info($"Executing Get {id}");

        return "value";
    }
}

我们稍后会讨论Logger属性的实现。对于此实现,我将使用log4net库进行日志记录。这个库有一个有趣的功能。我在谈论级别继承。简单地说,如果在日志的配置中,我说名称为X的记录器应该有日志级别Info,这意味着所有名称以X(如X.YX.ZX.A.B)开头的记录器将继承相同的日志级别。

所以这是最初的想法。对于每个请求,我都会以某种方式计算所需的日志级别。然后我将在log4net配置中创建一个新命名的记录器。此记录器将具有所需的日志级别。之后,在此请求期间创建的所有记录器对象都必须具有前缀为该记录器名称的名称。这里唯一的事情是log4net永远不会删除记录器。一旦创建它们,它们将在应用程序运行时存在。这就是我为每个日志级别预先创建具有特定名称的记录器的原因:

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
  <appender name="Console" type="log4net.Appender.ConsoleAppender">
    <layout type="log4net.Layout.PatternLayout">
      <!-- Pattern to output the caller's file name and line number -->
      <conversionPattern value="%5level [%thread] (%file:%line) - %message%newline" />
    </layout>
  </appender>

  <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
    <file value="RequestLoggingLog.log" />
    <appendToFile value="true" />
    <maximumFileSize value="100KB" />
    <maxSizeRollBackups value="2" />

    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%level %thread %logger - %message%newline" />
    </layout>
  </appender>

  <root>
    <level value="WARN" />
    <appender-ref ref="Console" />
    <appender-ref ref="RollingFile" />
  </root>

  <logger name="EdlinSoftware.Log.Error">
    <level value="ERROR" />
  </logger>

  <logger name="EdlinSoftware.Log.Warning">
    <level value="WARN" />
  </logger>

  <logger name="EdlinSoftware.Log.Info">
    <level value="INFO" />
  </logger>

  <logger name="EdlinSoftware.Log.Debug">
    <level value="DEBUG" />
  </logger>
</log4net>

现在我有几个带有名称EdlinSoftware.Log.XXXX的预定义记录器。这些名称将是实际级别名称的前缀。为了避免请求之间的冲突,我将在AsyncLocal对象中保存当前请求的前缀。我将在新的OWIN中间件中设置此对象的值:

app.Use(async (context, next) =>
{
    try
    {
        LogSupport.LogNamePrefix.Value = await LogSupport.GetLogNamePrefix(context);

        await next();
    }
    finally
    {
        LogSupport.LogNamePrefix.Value = null;
    }
});

设置此值后,可以轻松创建具有所需前缀名称的记录器:

public static class LogSupport
{
    public static readonly AsyncLocal<string> LogNamePrefix = new AsyncLocal<string>();

    public static ILog GetLogger(string name)
    {
        return GetLoggerWithPrefixedName(name);
    }

    public static ILog GetLogger(Type type)
    {
        return GetLoggerWithPrefixedName(type.FullName);
    }

    private static ILog GetLoggerWithPrefixedName(string name)
    {
        if (!string.IsNullOrWhiteSpace(LogNamePrefix.Value))
        { name = $"{LogNamePrefix.Value}.{name}"; }

        return LogManager.GetLogger(typeof(LogSupport).Assembly, name);
    }

    ....

}

现在如何在我们的控制器中获取一个logger实例已经很清楚了:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private ILog _logger;

    private ILog Logger
    {
        get => _logger ?? (_logger = LogSupport.GetLogger(typeof(ValuesController)));
    }

    ....
}

唯一剩下要做的就是如何设置规则来定义应该为请求分配哪个日志级别。这种机制应该足够灵活。这里的主要思想是使用C#脚本。我将创建一个文件LogLevelRules.json,在其中我将定义一组成对规则(字典)日志级别:

 

[
  {
    "logLevel": "Debug",
    "ruleCode": "context.Request.Path.Value == \"/api/values/1\""
  },
  {
    "logLevel": "Debug",
    "ruleCode": "context.Request.Path.Value == \"/api/values/3\""
  }
]

这里,logLevel是所需的日志级别, ruleCode——C#代码返回给定请求的布尔值。应用程序将逐个运行这些规则的代码。第一个规则返回true,将设置相应的日志级别。如果所有规则都返回false,将使用默认日志级别。

要从string规则表示中创建委托,可以使用以下CSharpScript类:

public class Globals
{
    public HttpContext context;
}

internal class LogLevelRulesCompiler
{
    public IReadOnlyList<LogLevelRule> 
           Compile(IReadOnlyList<LogLevelRuleDescription> levelRuleDescriptions)
    {
        var result = new List<LogLevelRule>();

        foreach (var levelRuleDescription in levelRuleDescriptions ?? new LogLevelRuleDescription[0])
        {
            var script = CSharpScript.Create<bool>
                         (levelRuleDescription.RuleCode, globalsType: typeof(Globals));
            ScriptRunner<bool> runner = script.CreateDelegate();

            result.Add(new LogLevelRule(levelRuleDescription.LogLevel, runner));
        }

        return result;
    }
}

internal sealed class LogLevelRule
{
    public string LogLevel { get; }

    public ScriptRunner<bool> Rule { get; }

    public LogLevelRule(string logLevel, ScriptRunner<bool> rule)
    {
        LogLevel = logLevel ?? throw new ArgumentNullException(nameof(logLevel));
        Rule = rule ?? throw new ArgumentNullException(nameof(rule));
    }
}

这里,Compile方法获取从LogLevelRules.json文件中读取的对象列表。它为每个规则创建runner委托并存储它以供以后使用。

委托列表可以被存储:

LogSupport.LogLevelSetters = new LogLevelRulesCompiler().Compile(
    new LogLevelRulesFileReader().ReadFile("LogLevelRules.json")
);

并在以后使用:

public static class LogSupport
{
    internal static IReadOnlyList<LogLevelRule> LogLevelSetters = new LogLevelRule[0];

    ...

    public static async Task<string> GetLogNamePrefix(HttpContext context)
    {
        var globals = new Globals
        {
            context = context
        };

        string result = null;

        foreach (var logLevelSetter in LogLevelSetters)
        {
            if (await logLevelSetter.Rule(globals))
            {
                result = $"EdlinSoftware.Log.{logLevelSetter.LogLevel}";
                break;
            }
        }

        return result;
    }
}

因此,在应用程序开始时,我们读取LogLevelRules.json文件,使用CSharpScript类将其内容转换为委托列表并将其存储到LogSupport.LogLevelSetters字段中。在每个请求中,我们从此列表中运行委托以获取请求的日志级别。

剩下要做的唯一事情就是观察LogLevelRules.json文件的修改。当我们想要为某种请求设置日志级别时,我们在此文件中添加另一个检查器。要使应用程序在不重新启动的情况下应用这些更改,我们必须查看该文件:

var watcher = new FileSystemWatcher
{
    Path = Directory.GetCurrentDirectory(),
    Filter = "*.json",
    NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += (sender, eventArgs) =>
{
    // Wait for the application that modifies the file to release it..
    Thread.Sleep(1000);

    LogSupport.LogLevelSetters = new LogLevelRulesCompiler().Compile(
        new LogLevelRulesFileReader().ReadFile("LogLevelRules.json")
    );
};
watcher.EnableRaisingEvents = true;

为简洁起见,我在使用LogSupport.LogLevelSetters字段时没有使用线程同步代码。但在实际应用中,你真的应该应用它。

您可以在GitHub找到该应用程序的完整代码。

缺点

此代码解决了为每个请求定义日志级别的问题。但它也有一些缺点。我们来讨论一下。

  1. 此方法更改记录器的名称。因此,在日志文件而不是MyClassLogger中,您会看到类似Edlinsoft.Log.Debug.MyClassLogger的内容。人们可以忍受它,但它不是很方便。可能的,这个问题可以通过使用日志布局来解决。
  2. 现在不可能使记录器实例成为静态,因为它们应该为每个请求单独创建。对我来说,最严重的问题是所有团队成员都应该记住这样做。人们可能会意外地使用记录器定义静态字段并获得奇怪的结果。为了克服这种情况,我们可以为记录器创建一个包装类,并使用它而不是直接使用log4net类。这样的包装类总是可以为每个操作创建log4net记录器的新实例。在这种情况下,使用包装类的静态实例将没有问题。
  3. 所描述的方法创建了许多记录器实例。它会污染内存并占用CPU周期。根据应用,它可能是也可能不是问题。
  4. 然后我们用规则修改JSON文件,规则代码可能包含错误。它很容易设置trycatch阻止这些错误不会破坏我们的主程序。但我们仍然需要意识到出了问题。可能有两种类型的错误:
    • 编写规则代码到委托时编译时错误。
    • 执行这些代理期间的运行时错误。

不管如何,我们必须意识到这些错误,否则,我们将不会得到日志消息,甚至不会知道它。

结论

总的来说,我应该说我觉得这不是一个很好的解决方案,应该取代现代的日志方法。即使使用每个应用程序的日志级别,一个可以过滤日志记录的好工具也可以提供帮助。无论如何,我希望对问题的分析能给你一些思考。

 

原文地址:https://www.codeproject.com/Articles/1273941/Log-Level-Per-Request

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/87924457
今日推荐