ASP.NET Core - 日志记录系统(二)
- 2.4 日志提供程序
- 2.4.1 内置日志提供程序
- 2.4.2 源码解析
本篇接着上一篇 ASP.NET Core - 日志记录系统(一) 往下讲,所以目录不是从 1 开始的。
2.4 日志提供程序
2.4.1 内置日志提供程序
ASP.NET Core 包括以下日志记录提供程序作为共享框架的一部分:
- Console
- Debug
- EventSource
- EventLog
除此之外,还有一些微软官方提供的,但是没有和 .NET Core 框架集成的提供程序,如 ApplicationInsights 、AzureAppServicesFile 和 AzureAppServicesBlob ,这些在日常开发中使用的比较少,大家有兴趣的话可以自行了解一下。
-
控制台
Console 提供程序将输出记录到控制台。
通用主机启动时就包含了控制台日志提供程序,使用它之后,我们记录的日志在调试过程中,可以在 VS 的 “调试输出” 窗口和 “ASP.NET Core Web 服务器” 窗口(非 IIS Press 启动) 看到。使用 dotnet run 运行应用时,可以在控制台窗口中看到。以 “Microsoft” 类别开头的日志来自 ASP.NET Core 框架代码。 ASP.NET Core 和应用程序代码使用相同的日志记录 API 和提供程序。
控制台提供程序提供了多个 API,允许根据需要对输出格式、文字颜色等进行调整。
-
调试
Debug 提供程序使用 System.Diagnostics.Debug 类写入日志输出。 对 System.Diagnostics.Debug.WriteLine 的调用写入到 Debug 提供程序。
在 Linux 上,Debug 提供程序日志位置取决于分发,并且可以是以下位置之一:
- /var/log/message
- /var/log/syslog
-
事件来源
EventSource 提供程序写入名称为 Microsoft-Extensions-Logging 的跨平台事件源。 在 Windows 上,提供程序使用的是 ETW。
EventSource 日志提供程序记录的日志可以使用跨平台的 dotnet 追踪工具 dotnet-trace 来收集和跟踪。dotnet-trace 的安装和使用请参阅 dotnet-trace 诊断工具 - .NET CLI | Microsoft Learn 。
-
事件日志
仅在 Windows 系统下生效,可通过“事件查看器”进行日志查看。
EventLog 提供程序将日志输出发送到 Windows 事件日志。 与其他提供程序不同,EventLog 提供程序不继承默认的非提供程序设置。 如果未指定 EventLog 日志设置,则它们默认为 LogLevel.Warning。若要记录低于 LogLevel.Warning 的事件,请显式设置日志级别。
默认情况下,记录下来的事件日志一些基本参数如下:
- LogName:“Application”
- SourceName:“.NET Runtime”
- MachineName:使用本地计算机名称。
我们可以通过
AddEventLog
重载可以传入EventLogSettings
来修改:var builder = WebApplication.CreateBuilder(); builder.Logging.AddEventLog(eventLogSettings => { eventLogSettings.SourceName = "MyLogs"; });
上面已经讲到,在通过通用主机启动一个 .NET Core 应用时,会默认添加了 Console、Debug、EventSource 日志提供程序,如果运行平台是 Windows,还会添加 EventLog 日志提供程序。通过 ILoggingBuilder
我们可以重置并自定义日志提供程序的类型,这使得我们可以根据需要使用任何符合标准的日志提供程序。
如果是没有使用通用主机的非托管控制台应用,可以通过以下方式添加控制台日志提供程序:
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
ILogger logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Example log message");
不止控制台日志提供程序,其他各种日志提供程序都是按照日志记录系统框架进行开发和集成。显然,仅这些内置日志提供程序并不能满足我们生产开发中的使用,例如缺少最基础且常用的文件日志提供程序,还有在分布式应用已经非常普遍的业界现状下,时常需要将日志写分布式日志系统中进行统一的管理和分析,这些是微软没有提供的,但是第三方都有成熟的按照 .NET Core 日志记录系统体系架构开发的实现,这些将在下面细讲。
2.4.2 源码解析
.NET Core 日志记录系统的使用非常简单方便,其扩展性非常强,上面的章节已经讲解了基本的配置和使用,最核心的实现在于 LogeerFactory
和 ILogger
, LogeerFactory
用于日志系统的配置,ILogger
用于日志记录。下面从框架源码的角度,解析一下日志记录系统的实现。
阅读一个框架源码,我们可以从其开放出来的 API 作为入口,这样能较容易地梳理出其设计思路和实现脉络。首先是日志配置这一块,我们在应用集成的时候,对日志系统的配置都是基于 ILoggingBuilder
的,当然通过上面章节的内容,大家都已经知道在我们通过 ILoggingBuilder
进行配置之前,通用主机已经进行了一些配置。
ILoggingBuilder
的实现类其实就只是保存了容器,其他各种可用的方法都是扩展方法,都是往容器之中添加配置。
在添加日志系统默认配置的时候,可以看到显示调用了 AddLogging() 方法,之后就是像我们自己在实际应用中对日志系统进行配置一样帮我们添加了一些默认配置。
AddLogging()
方法是扩展方法,在 LoggingServiceCollectionExtensions
类中,在这个方法中往容器注入了三个日志记录系统最关键的东西,分别是 LoggerFactory
、Logger<>
和日志配置。
当我们从使用日志记录器的时候,要么就是从容器中解析,要么就是通过 LoggerFactory.CreateLogger()
方法创建,查看 Logger<>
类的实现,其内部其实也是通过 LoggerFactory
创建了 ILogger
实例,注意这里的 ILogger
是没有泛型的,最终我们使用的其实都是这一个没有泛型的。
LoggerFactory
在其初始化的时候,会从容器中解析出我们添加的日志记录提供程序以及和日记记录系统相关的配置信息,并将日志记录提供程序保存到集合中。
当调用 CreateLogger
方法时,会创建 Logger
实例,为其配置并应用过滤规则,并将其保存起来。
这里就引出了三个重要的内部实现 Logger
、LoggerInformation
和 MessageLogger
。Logger
是上面讲到的我们最终实际使用的 ILogger
的实现类,它的构造函数中需要传入 LoggerInformation
数组,LoggerInformation
数组与日志提供程序的数量对应。LoggerInformation
是一个结构体,是针对特定日志记录提供程序和日志类别的封装,在内部创建了特定于具体日志提供程序的日志记录器。
MessageLogger
是最终的日志信息书写的地方,它也是一个结构体,包含了规则过滤等内容,可以看到它的构造函数中传入了 LoggerInformation
的 Logger
属性,也就是说最终也是使用特定于日志提供程序的日志记录器的。
最终返回的 MessageLogger
数组赋值给了 Logger
,MessageLogger
数组不一定与日志记录提供程序的数量一样,因为有些日志记录提供程序在规则配置检查中可能跳过了。
Logger
类应用了装饰器模式,对多种日志记录提供程序的记录器进行了包装,提供了统一的日志记录 API,使得我们在使用时可以通过统一的入口将日志同时书写到不同的地方。当我们调用 Logger
实例的 Log
方法时,实际上是遍历了 MessageLogger
数组,通过具体的日志提供程序对应的日志记录器对当前日志级别进行检查,并进行最终的日志记录。
以控制台提供程序为例,这里中间有很多代码,其实只是为了实现更好的扩展性和性能,可以先忽略不看,最终也只是返回了特定 ConsoleLogger
。
而 ConsoleLogger
中的 Log
方法最终是将日志信息放到队列中,再由队列处理器写到控制台中。
了解完 .NET Core 日志记录系统的整体实现逻辑之后,我们想实现一个自己的日志提供程序其实还是比较简单的,当然如果要像微软内置的日志记录提供程序,或者第三方成熟的日志框架那样,各种细节处理得很好,就稍微有些难度了。以下是一个简单的示例,模仿默认日志记录提供程序的的实现方式,将日志记录到文件中:
(1) 创建一个类库项目,并引入以下依赖包
Install-Package Microsoft.Extensions.Logging.Abstractions
Install-Package Microsoft.Extensions.Logging
(2) 首先是实现 ILogger
接口,提供我们的日志记录器
internal sealed class WeWantFileLogger : ILogger
{
private readonly object _sync = new object();
/// <summary>
/// 创建日志记录域
/// </summary>
/// <typeparam name="TState"></typeparam>
/// <param name="state"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
// 由于不准备支持日志记录域功能,这里返回一个空实现
return NullScope.Instance;
}
/// <summary>
/// 判断是否记录日志
/// </summary>
/// <param name="logLevel"></param>
/// <returns></returns>
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
/// <summary>
/// 记录日志,这里简单的演示例子
/// 一个可用于正式环境的文件记录器还需考虑很多可扩展性、性能等因素
/// </summary>
/// <typeparam name="TState"></typeparam>
/// <param name="logLevel"></param>
/// <param name="eventId"></param>
/// <param name="state"></param>
/// <param name="exception"></param>
/// <param name="formatter"></param>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
ThrowHelper.ThrowIfNull(formatter);
string message = formatter(state, exception);
if(string.IsNullOrEmpty(message))
{
return;
}
message = $"{logLevel}: {message} { Environment.NewLine }";
if(exception != null)
{
message += Environment.NewLine + Environment.NewLine + exception;
}
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "log.txt");
lock (_sync)
{
System.IO.File.AppendAllText(filePath, message);
}
}
}
其他相关的类如下:
internal sealed class NullScope : IDisposable
{
public static NullScope Instance = new NullScope();
private NullScope() { }
public void Dispose()
{
}
}
internal static partial class ThrowHelper
{
/// <summary>Throws an <see cref="ArgumentNullException"/> if <paramref name="argument"/> is null.</summary>
/// <param name="argument">The reference type argument to validate as non-null.</param>
/// <param name="paramName">The name of the parameter with which <paramref name="argument"/> corresponds.</param>
internal static void ThrowIfNull(
#if NETCOREAPP3_0_OR_GREATER
[NotNull]
#endif
object? argument,
[CallerArgumentExpression("argument")] string? paramName = null)
{
if (argument is null)
{
Throw(paramName);
}
}
#if NETCOREAPP3_0_OR_GREATER
[DoesNotReturn]
#endif
private static void Throw(string? paramName) => throw new ArgumentNullException(paramName);
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}
public string ParameterName { get; }
}
(3) 然后是实现 ILoggerProvider
接口,提供日志记录提供程序
[ProviderAlias("WeWantFile")]
public class WeWantFileLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new WeWantFileLogger();
}
public void Dispose()
{
}
}
(4) 提供相应的扩展方法
public static class WeWantFileLoggerFactoryExtensions
{
public static ILoggingBuilder AddWeWantFile(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, WeWantFileLoggerProvider>());
return builder;
}
}
(5) 在之前的项目中引用,并进行以下配置
// 清除默认的日志记录提供程序,添加自定义的日志记录提供程序
builder.Logging.ClearProviders();
builder.Logging.AddWeWantFile();
(6) 测试日志记录
调用之前测试用的 Web API 接口,代码如下:
// 各种日志API对应各种日志级别
// 断点
_logger.LogTrace("这是一个断点日志");
//调试
_logger.LogDebug("this is a debug.");
//信息
_logger.LogInformation("this is an info.");
//警告
_logger.LogWarning("this is a warning.");
//错误
_logger.LogError("this is an error.");
//当机
_logger.LogCritical("this is Critical");
可以看到项目文件夹中多了 log.txt文件,内容如下:
.NET Core 下的日志记录系统大概就介绍到这里,后面再继续介绍一下一些第三方日志框架,怎么将其集成到 .NET Core 框架中进行正式生产环境下的应用。
参考文章:
.NET Core 和 ASP.NET Core 中的日志记录 | Microsoft Learn
理解ASP.NET Core - 日志(Logging) - xiaoxiaotank - 博客园 (cnblogs.com)