环境:
参照:《.NET Core 和 ASP.NET Core 中的日志记录》
微软为了统一日志输出代码就做了一个日志输出框架,它由以下两个包组成:
它有以下几个特点:
日志框架
、.neter代码
以及第三方日志记录提供程序
,它们三者之间的协作关系如下图:
日志的类别是用一个字符串来区分的,我们可以在创建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
),我们可以根据日志的重要程度,将输出的日志内容标记为其中一个级别。
日志级别定义如下:
我们可以根据日志记录提供程序的全名(或别名)
、日志类别
、日志级别
这三个因素去设置过滤规则,只有符合规则的日志才能输出到下游日志记录提供程序。
虽然在我们可以设置多条过滤规则,但日志框架只会选取其中一个最优的规则来使用,其他的都会忽略掉(这个过程是在创建ILogger
时完成的)。而日志框架选取最优过滤规则时是根据我们给这条规则指定的日志记录提供程序的全名(或别名)
和日志类别
进行匹配选取的。
我们获取到的日志记录器(ILogger
)如下图所示:
下面大概说下匹配最优规则:
首先我们知道,在匹配前:过滤规则可以有指定的日志记录提供程序全名(或别名),也可以没有;同样,过滤规则可以有指定的日志类别,也可以没有。而日志记录器是有确定的日志类别的和日志记录提供程序全名(或别名)(多个日志记录提供程序,但匹配的时候是每个都单独匹配的)的。那么,接下来看匹配的规则:
*
或默认前缀匹配,这样匹配成功的会再比较过滤规则的日志类别字符串的长度,较长的具有更高的权重。选取最优过滤规则的源码如下:
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();
}
}
上面的代码是使用编码的方式去配置的,但我们最常用的还是使用json
文件去配置,虽然写法不同,但它的应用规则是一样的(上面总结的过滤规则),示例的配置文件如下:
注意: 虽然日志框架是在依赖容器之上,但是配置模块是独立于依赖容器的,可参考配置模块的文章《.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();
}
}
}
可以看到,我们配置的过滤级别是
Debug
,那么Trace
级别的日志将不会输出。
如果我们没有设置任何过滤规则,那么日志框架会有一个过滤规则,即:日志输出的最小的日志级别(默认的是LogLevel.Information
)。
上面说的一些都属于日志框架上的内容,然而日志框架不负责最终的输出,它只负责将过滤好的日志消息分发给下游日志记录提供程序。
不过,微软已经提供了几个日志记录提供程序,如:
除了微软提供的,其他常用的第三方日志记录提供程序如下:
Microsoft.Extensions.Logging.Log4Net.AspNetCore
Serilog.AspNetCore
NLog.Web.AspNetCore
这些日志记录提供程序所处的位置如下图所示:
《.netcore入门20:aspnetcore集成log4net》
《.netcore入门21:aspnetcore集成Serilog》
《.netcore入门32:asp.net core集成NLog》