ASP.NET Core - 日志记录系统(二)

本篇接着上一篇 [ASP.NET Core - 日志记录系统(一)] 往下讲,所以目录不是从 1 开始的。

2.4 日志提供程序

2.4.1 内置日志提供程序

ASP.NET Core 包括以下日志记录提供程序作为共享框架的一部分:

  • Console
  • Debug
  • EventSource
  • EventLog

除此之外,还有一些微软官方提供的,但是没有和 .NET Core 框架集成的提供程序,如 ApplicationInsightsAzureAppServicesFile 和 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 来修改:

    csharp 复制代码
    var builder = WebApplication.CreateBuilder();
    builder.Logging.AddEventLog(eventLogSettings =>
    {
    	eventLogSettings.SourceName = "MyLogs";
    });

上面已经讲到,在通过通用主机启动一个 .NET Core 应用时,会默认添加了 Console、Debug、EventSource 日志提供程序,如果运行平台是 Windows,还会添加 EventLog 日志提供程序。通过 ILoggingBuilder 我们可以重置并自定义日志提供程序的类型,这使得我们可以根据需要使用任何符合标准的日志提供程序。

如果是没有使用通用主机的非托管控制台应用,可以通过以下方式添加控制台日志提供程序:

csharp 复制代码
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 日志记录系统的使用非常简单方便,其扩展性非常强,上面的章节已经讲解了基本的配置和使用,最核心的实现在于 LogeerFactoryILogger, LogeerFactory 用于日志系统的配置,ILogger 用于日志记录。下面从框架源码的角度,解析一下日志记录系统的实现。

阅读一个框架源码,我们可以从其开放出来的 API 作为入口,这样能较容易地梳理出其设计思路和实现脉络。首先是日志配置这一块,我们在应用集成的时候,对日志系统的配置都是基于 ILoggingBuilder 的,当然通过上面章节的内容,大家都已经知道在我们 ILoggingBuilder 进行配置之前,通用主机已经进行了一些配置。

ILoggingBuilder 的实现类其实就只是保存了容器,其他各种可用的方法都是扩展方法,都是往容器之中添加配置。

在添加日志系统默认配置的时候,可以看到显示调用了 AddLogging() 方法,之后就是像我们自己在实际应用中对日志系统进行配置一样帮我们添加了一些默认配置。

AddLogging() 方法是扩展方法,在 LoggingServiceCollectionExtensions 类中,在这个方法中往容器注入了三个日志记录系统最关键的东西,分别是 LoggerFactoryLogger<> 和日志配置。

当我们从使用日志记录器的时候,要么就是从容器中解析,要么就是通过 LoggerFactory.CreateLogger() 方法创建,查看 Logger<> 类的实现,其内部其实也是通过 LoggerFactory 创建了 ILogger 实例,注意这里的 ILogger 是没有泛型的,最终我们使用的其实都是这一个没有泛型的。

LoggerFactory 在其初始化的时候,会从容器中解析出我们添加的日志记录提供程序以及和日记记录系统相关的配置信息,并将日志记录提供程序保存到集合中。

当调用 CreateLogger 方法时,会创建 Logger 实例,为其配置并应用过滤规则,并将其保存起来。

这里就引出了三个重要的内部实现 LoggerLoggerInformationMessageLoggerLogger 是上面讲到的我们最终实际使用的 ILogger 的实现类,它的构造函数中需要传入 LoggerInformation 数组,LoggerInformation 数组与与日志提供程序的数量对应。LoggerInformation 是一个结构体,是针对特定日志记录提供程序和日志类别的封装,在内部创建了特定于具体日志提供程序的日志记录器。

MessageLogger 是最终的日志信息书写的地方,它也是一个结构体,包含了规则过滤等内容,可以看到它的构造函数中传入了 LoggerInformationLogger 属性,也就是说最终也是使用特定于日志提供程序的日志记录器的。

最终返回的 MessageLogger 数组赋值给了 LoggerMessageLogger 数组不一定与日志记录提供程序的数量一样,应该有些日志记录提供程序在规则配置检查中可能跳过了。

Logger 类应用了装饰器模式,对多种日志记录提供程序的记录器进行了包装,提供了统一的日志记录 API,使得我们在使用时可以通过统一的入口将日志同时书写到不同的地方。当我们调用 Logger 实例的 Log 方法时,实际上是遍历了 MessageLogger 数组,通过具体的日志提供程序对应的日志记录器对当前日志级别进行检查,并进行最终的日志记录。

最终返回的 MessageLogger 数组赋值给了 LoggerMessageLogger 数组不一定与日志记录提供程序的数量一样,应该有些日志记录提供程序在规则配置检查中可能跳过了。

Logger 类应用了装饰器模式,对多种日志记录提供程序的记录器进行了包装,提供了统一的日志记录 API,使得我们在使用时可以通过统一的入口将日志同时书写到不同的地方。当我们调用 Logger 实例的 Log 方法时,实际上是遍历了 MessageLogger 数组,通过具体的日志提供程序对应的日志记录器对当前日志级别进行检查,并进行最终的日志记录。

以控制台提供程序为例,这里中间有很多代码,其实只是为了实现更好的扩展性和性能,可以先忽略不看,最终也只是返回了特定 ConsoleLogger

ConsoleLogger 中的 Log 方法最终是将日志信息放到队列中,再由队列处理器写到控制台中。

2.4.3 自定义日志提供程序

了解完 .NET Core 日志记录系统的整体实现逻辑之后,我们想实现一个自己的日志提供程序其实还是比较简单的,当然如果要像微软内置的日志记录提供程序,或者第三方成熟的日志框架那样,各种细节处理得很好,就稍微有些难度了。以下是一个简单的示例,模仿默认日志记录提供程序的的实现方式,将日志记录到文件中:

(1) 创建一个类库项目,并引入以下依赖包

shell 复制代码
Install-Package Microsoft.Extensions.Logging.Abstractions
Install-Package Microsoft.Extensions.Logging

(2) 首先是实现 ILogger 接口,提供我们的日志记录器

csharp 复制代码
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);
		}
		
	}
}

其他相关的类如下:

csharp 复制代码
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 接口,提供日志记录提供程序

csharp 复制代码
[ProviderAlias("WeWantFile")]
public class WeWantFileLoggerProvider : ILoggerProvider
{
	public ILogger CreateLogger(string categoryName)
	{
		return new WeWantFileLogger();
	}

	public void Dispose()
	{
	}
}

(4) 提供相应的扩展方法

csharp 复制代码
public static class WeWantFileLoggerFactoryExtensions
{
	public static ILoggingBuilder AddWeWantFile(this ILoggingBuilder builder)
	{
		builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, WeWantFileLoggerProvider>());
		return builder;
	}
}

(5) 在之前的项目中引用,并进行以下配置

csharp 复制代码
// 清除默认的日志记录提供程序,添加自定义的日志记录提供程序
builder.Logging.ClearProviders();
builder.Logging.AddWeWantFile();

(6) 测试日志记录

调用之前测试用的 Web API 接口,代码如下:

csharp 复制代码
// 各种日志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)

ASP.NET Core 系列:

目录:ASP.NET Core 系列总结

上一篇:ASP.NET Core - 日志记录系统(一)

相关推荐
VAllen7 天前
分析基于ASP.NET Core Kernel的gRPC服务在不同.NET版本的不同部署方式的不同线程池下的性能表现
.net·性能测试·asp.net core·grpc·dotnet
棉晗榜25 天前
.net core在linux导出excel,System.Drawing.Common is not supported on this platform
linux·excel·asp.net core·miniexcel
棉晗榜25 天前
asp.net core发布配置端口号,支持linux
asp.net core
coredx1 个月前
如何优雅地让 ASP.NET Core 支持异步模型验证
asp.net core
小乖兽技术1 个月前
ASP.NET Core Web 项目的部署:选择 IIS 还是 Kestrel?
后端·kestrel·iis·asp.net·asp.net core
界面开发小八哥1 个月前
DevExtreme JS & ASP.NET Core v24.2新功能预览 - 全新的聊天组件
javascript·ui·asp.net core·界面控件·ui开发·devextreme·.net 9
gc_22991 个月前
学习ASP.NET Core的身份认证(基于Session的身份认证3)
asp.net core·session·身份认证
gc_22991 个月前
ASP.NET Core项目中使用SqlSugar连接多个数据库的方式
asp.net core·sqlsugar·多数据库
gc_22991 个月前
学习ASP.NET Core的身份认证(基于Session的身份认证1)
asp.net core·session·身份认证