.netcore入门31:.net core中的日志框架

环境:

  • window10x64
  • .net core3.1
  • vs2019

参照:《.NET Core 和 ASP.NET Core 中的日志记录》

一、日志框架的概念

微软为了统一日志输出代码就做了一个日志输出框架,它由以下两个包组成:

  • Microsoft.Extensions.Logging.Abstractions(抽象定义)
  • Microsoft.Extensions.Logging(具体实现)

它有以下几个特点:

  • 日志框架构建在依赖注入框架之上;
  • 日志框架中给日志定义了两大特征:类别和级别。用户可以根据这两大特征配置日志框架是否向下游(第三方日志记录程序)输出日志;
  • 日志框架提供了范围的概念,它可以将多条日志打上一个标签,这样我们就可以记录一个http请求的链条了;
  • 我们只需要适应日志框架中的概念写日志代码即可,至于输出可交给第三方日志记录提供程序(如: NLog、Log4net、Serialog等);

日志框架.neter代码以及第三方日志记录提供程序,它们三者之间的协作关系如下图:
.netcore入门31:.net core中的日志框架_第1张图片

二、日志框架中的类别和级别以及过滤规则

2.1 类别和级别

日志的类别是用一个字符串来区分的,我们可以在创建ILogger的时候手动指定,但一般倾向于使用当前类的全名作为类别字符串,下面分别演示了两个方式:

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddLogging(builder =>
        {
            builder.ClearProviders();
            builder.AddConsole();
        });
        var provider = services.BuildServiceProvider();
        //手动指定日志的类别字符串
        var logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger("Logging_Trial.Program");
        //使用当前类全名作为类别字符串
        var logger2 = provider.GetRequiredService<ILogger<Program>>();
        //...
        Console.WriteLine("Hello World!");
    }
}

日志框架中定义好了六大日志级别(忽略:LogLevel.None),我们可以根据日志的重要程度,将输出的日志内容标记为其中一个级别。
日志级别定义如下:
.netcore入门31:.net core中的日志框架_第2张图片

2.2 过滤规则

我们可以根据日志记录提供程序的全名(或别名)日志类别日志级别这三个因素去设置过滤规则,只有符合规则的日志才能输出到下游日志记录提供程序。

虽然在我们可以设置多条过滤规则,但日志框架只会选取其中一个最优的规则来使用,其他的都会忽略掉(这个过程是在创建ILogger时完成的)。而日志框架选取最优过滤规则时是根据我们给这条规则指定的日志记录提供程序的全名(或别名)日志类别进行匹配选取的。
我们获取到的日志记录器(ILogger)如下图所示:
.netcore入门31:.net core中的日志框架_第3张图片

下面大概说下匹配最优规则:

首先我们知道,在匹配前:过滤规则可以有指定的日志记录提供程序全名(或别名),也可以没有;同样,过滤规则可以有指定的日志类别,也可以没有。而日志记录器是有确定的日志类别的和日志记录提供程序全名(或别名)(多个日志记录提供程序,但匹配的时候是每个都单独匹配的)的。那么,接下来看匹配的规则:

  1. 首先看过滤规则的日志记录提供程序的名称(或别名)是否与日志记录器的匹配。
    当过滤规则没有指定日志记录提供程序全名(或别名)或指定的日志记录提供程序全名(或别名)与日志记录器上的相同时,都属于匹配成功,不过后者权重更高。
  2. 然后再看过滤规则的日志类别是否与日志记录器的匹配。
    当过滤规则没有指定日志类别或指定的日志类别匹配日志记录器上的时候,这个过滤规则就称为匹配成功,同样,后者权重更大,因为它更严格。另外日志类别匹配的时候会使用通配符*或默认前缀匹配,这样匹配成功的会再比较过滤规则的日志类别字符串的长度,较长的具有更高的权重。
  3. 最后,当上面的都比较完成后,还是找到了两个或两个以上的规则时,就按照后来者居上的原则,选取最后一个作为最优规则。

选取最优过滤规则的源码如下:

internal class LoggerRuleSelector
{
	// Token: 0x06000050 RID: 80 RVA: 0x00002F10 File Offset: 0x00001110
	public void Select(LoggerFilterOptions options, Type providerType, string category, out LogLevel? minLevel, out Func<string, string, LogLevel, bool> filter)
	{
		filter = null;
		minLevel = new LogLevel?(options.MinLevel);
		string alias = ProviderAliasUtilities.GetAlias(providerType);
		LoggerFilterRule loggerFilterRule = null;
		foreach (LoggerFilterRule loggerFilterRule2 in options.Rules)
		{
			if (LoggerRuleSelector.IsBetter(loggerFilterRule2, loggerFilterRule, providerType.FullName, category) || (!string.IsNullOrEmpty(alias) && LoggerRuleSelector.IsBetter(loggerFilterRule2, loggerFilterRule, alias, category)))
			{
				loggerFilterRule = loggerFilterRule2;
			}
		}
		if (loggerFilterRule != null)
		{
			filter = loggerFilterRule.Filter;
			minLevel = loggerFilterRule.LogLevel;
		}
	}

	// Token: 0x06000051 RID: 81 RVA: 0x00002FB4 File Offset: 0x000011B4
	private static bool IsBetter(LoggerFilterRule rule, LoggerFilterRule current, string logger, string category)
	{
		if (rule.ProviderName != null && rule.ProviderName != logger)
		{
			return false;
		}
		if (rule.CategoryName != null)
		{
			string[] array = rule.CategoryName.Split(LoggerRuleSelector.WildcardChar);
			if (array.Length > 2)
			{
				throw new InvalidOperationException("Only one wildcard character is allowed in category name.");
			}
			string value = array[0];
			string value2 = (array.Length > 1) ? array[1] : string.Empty;
			if (!category.StartsWith(value, StringComparison.OrdinalIgnoreCase) || !category.EndsWith(value2, StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
		}
		if (current != null && current.ProviderName != null)
		{
			if (rule.ProviderName == null)
			{
				return false;
			}
		}
		else if (rule.ProviderName != null)
		{
			return true;
		}
		if (current != null && current.CategoryName != null)
		{
			if (rule.CategoryName == null)
			{
				return false;
			}
			if (current.CategoryName.Length > rule.CategoryName.Length)
			{
				return false;
			}
		}
		return true;
	}

	// Token: 0x0400001E RID: 30
	private static readonly char[] WildcardChar = new char[]
	{
		'*'
	};
}

根据上面的描述,我们应该能看懂下面的代码,哪种情况下哪些过滤规则会被选中:

//namespace Logging_Trial
class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddLogging(builder =>
        {
            builder.ClearProviders();
            builder.AddConsole();
            //应用的日志记录提供程序: 所有   应用的日志类别: 所有
            builder.AddFilter(level => level >= LogLevel.Warning);
            //应用的日志记录提供程序: 所有   应用的日志类别: 所有
            builder.AddFilter((category, level) => category == typeof(Program).FullName && level >= LogLevel.Debug);
            //应用的日志记录提供程序: 所有   应用的日志类别: 所有
            builder.AddFilter((alias, category, level) => level > LogLevel.Debug);
            //应用的日志记录提供程序: 所有   应用的日志类别: Logging_Trial.Program
            builder.AddFilter(typeof(Program).FullName, LogLevel.Debug);
            //应用的日志记录提供程序: 所有   应用的日志类别: Logging_Trial.Program
            builder.AddFilter(typeof(Program).FullName, level => level >= LogLevel.Debug);

            //应用的日志记录提供程序: ConsoleLoggerProvider   应用的日志类别: 所有
            builder.AddFilter<ConsoleLoggerProvider>(level => level >= LogLevel.Debug);
            //应用的日志记录提供程序: ConsoleLoggerProvider   应用的日志类别: Logging_Trial.Program
            builder.AddFilter<ConsoleLoggerProvider>(typeof(Program).FullName, level => level >= LogLevel.Debug);
            //应用的日志记录提供程序: ConsoleLoggerProvider   应用的日志类别: 所有
            builder.AddFilter<ConsoleLoggerProvider>((category, level) => level >= LogLevel.Debug);
            //应用的日志记录提供程序: ConsoleLoggerProvider   应用的日志类别: 所有
            builder.AddFilter<ConsoleLoggerProvider>(typeof(Program).FullName, LogLevel.Debug);

        });
        var provider = services.BuildServiceProvider();
        var logger2 = provider.GetRequiredService<ILogger<Program>>();
        //...
        Console.WriteLine("ok!");
        Console.ReadLine();
    }
}

2.3 过滤规则配置的形式

2.3.1 概述

上面的代码是使用编码的方式去配置的,但我们最常用的还是使用json文件去配置,虽然写法不同,但它的应用规则是一样的(上面总结的过滤规则),示例的配置文件如下:
.netcore入门31:.net core中的日志框架_第4张图片

2.3.2 使用配置文件配置过滤规则实例

注意: 虽然日志框架是在依赖容器之上,但是配置模块是独立于依赖容器的,可参考配置模块的文章《.netcore入门25:.net core源码分析之配置模块(IConfiguration)》

  • 新建一个控制台工程ConsoleApp3
  • 先引入配置模块的包:
    <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.9" />
        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
    ItemGroup>
    
  • 再引入依赖容器的包:
    <ItemGroup>
     	<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
    ItemGroup>
    
  • 再引入日志包
    <ItemGroup>
        
        <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.9" />
        
        <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.9" />
        
        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.9" />
    ItemGroup>
    
  • 新建配置文件demo.json,并将其设置为"复制到输出目录",文件内容如下:
    {
     "Logging": {
       "LogLevel": {
         "Default": "Information",
         "Microsoft": "Warning",
         "Microsoft.Hosting.Lifetime": "Information",
         "ConsoleApp3.*": "Debug"
       }
     }
    }
    
  • 编写测试代码:Program.cs
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Linq;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging.Configuration;
    using Microsoft.Extensions.Logging;
    
    namespace ConsoleApp3
    {
        class Program
        {
            static void Main(string[] args)
            {
                ConfigurationBuilder builder = new ConfigurationBuilder();
                var config = builder.AddJsonFile("demo.json", optional: false).Build();
                var services = new ServiceCollection();
                services.AddLogging(builder =>
                {
                    builder.ClearProviders();
                    builder.AddConsole();
                    //使用配置文件配置日志框架
                    builder.AddConfiguration(config.GetSection("Logging"));
                });
                var provider = services.BuildServiceProvider();
                var logger = provider.GetRequiredService<ILogger<Program>>();
                //输出日志
                logger.LogTrace("Trace...");
                logger.LogDebug("Debug...");
                logger.LogInformation("Info...");
                logger.LogWarning("Waring...");
                logger.LogError("Error...");
                logger.LogCritical("Critical...");
                Console.ReadLine();
            }
        }
    }	
    
  • 运行看效果:
    .netcore入门31:.net core中的日志框架_第5张图片

    可以看到,我们配置的过滤级别是Debug,那么Trace级别的日志将不会输出。

2.4 最小级别

如果我们没有设置任何过滤规则,那么日志框架会有一个过滤规则,即:日志输出的最小的日志级别(默认的是LogLevel.Information)。

2.5 关于过滤规则,官方的描述

.netcore入门31:.net core中的日志框架_第6张图片

三、第三方日志记录提供程序

上面说的一些都属于日志框架上的内容,然而日志框架不负责最终的输出,它只负责将过滤好的日志消息分发给下游日志记录提供程序。
不过,微软已经提供了几个日志记录提供程序,如:

  • Microsoft.Extensions.Logging.Console :输出日志到控制台
  • Microsoft.Extensions.Logging.Debug : 输出日志到调试器的调试窗口
  • Microsoft.Extensions.Logging.EventSource:输出日志到Windows操作系统的事件跟踪系统ETW(Event Tracing for Window)
  • Microsoft.Extensions.Logging.TraceSource: 输出日志到以TraceSource为核心的跟踪日志系统

除了微软提供的,其他常用的第三方日志记录提供程序如下:

  • log4net,需引用 Microsoft.Extensions.Logging.Log4Net.AspNetCore
  • Serilog,需引用 Serilog.AspNetCore
  • NLog, 需引用 NLog.Web.AspNetCore

这些日志记录提供程序所处的位置如下图所示:

.netcore入门31:.net core中的日志框架_第7张图片
关于如何集成第三方日志,可参考:

《.netcore入门20:aspnetcore集成log4net》
《.netcore入门21:aspnetcore集成Serilog》
《.netcore入门32:asp.net core集成NLog》

你可能感兴趣的:(c#,.net)