《asp.netcore6框架揭秘》实例演示[13]:日志的基本编程模式

dotNET跨平台 dotNET跨平台     2022-12-01     748

关键词:

ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式》介绍了四种常用的诊断日志框架。其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net、NLog和Serilog 等。虽然这些框架大都采用类似的设计,但是它们采用的编程模式具有很大的差异。为了对这些日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架。[本文节选《ASP.NET Core 6框架揭秘》第8章]

[S801]将日志输出到控制台和调试窗口(源代码)
[S802]利用ILoggerFactory工厂创建Ilogger<T>对象(源代码)
[S803]注入Ilogger<T>对象(源代码)
[S804]TraceSource和EventSource的日志输出(源代码)
[S805]针对等级的日志过滤(源代码)
[S806]针对等级和类别的日志过滤(源代码)
[S807]针对等级、类别和ILoggerProvider类型的日志过滤(源代码)

[S801]将日志输出到控制台和调试窗口

我们通过一个简单的实例来演示如何将具有不同等级的日志消息输出到当前控制台和Visual Studio的调试窗口。如下所示的两个NuGet包提供了针对这两种日志输出渠道的支持,所以演示程序需要添加针对它们的引用。

  • Microsoft.Extensions.Logging.Console

  • Microsoft.Extensions.Logging.Debug

应用程序一般使用ILoggerFacotry工厂创建的ILogger对象来记录日志,下面的演示实例利用依赖注入容器来提供ILoggerFactory对象。如代码片段所示,我们创建了一个ServiceCollection对象,并调用AddLogging扩展方法注册了与日志相关的核心服务,作为依赖注入容器的IServiceProvider对象被构建出来后,我们从中提取出ILoggerFactory对象。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var logger = new ServiceCollection()
    .AddLogging(builder => builder
        .AddConsole()
        .AddDebug())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>()
    .CreateLogger("Program");

var levels = (LogLevel[])Enum
    .GetValues(typeof(LogLevel));
levels = levels.Where(it 
    => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, 
    level => logger.Log(level, eventId++, 
    "This is a/an 0 log message.", level));
Console.Read();

在调用AddLogging扩展方法时,我们利用提供的Action<ILoggingBuilder>委托完成了针对ConsoleLoggerProvider和DebugLoggerProvider的注册。具体来说,前者由ILoggingBuilder接口的AddConsole扩展方法注册,后者则由AddDebug扩展方法进行注册。我们通过指定日志类别(“Program”)调用ILoggerFactory接口的CreateLogger方法将对应的ILogger对象创建出来。每个ILogger对象都对应一个确定的类别,我们倾向于将当前写入日志的组件、服务或者类型名称作为日志类别,所以需要指定的是当前类型的名称“Program”。

我们通过调用ILogger的Log方法针对每个有效的日志等级分发了六个日志事件,事件的ID分别被设置成1~6的整数。我们在调用Log方法时通过指定一个包含占位符(0)的消息模板和对应参数的方式来格式化最终输出的消息内容。程序启动后,相应的日志会以图1所示的形式同时输出到控制台和Visual Studio的调试窗口。


图1 针对控制台和Debugger的日志输出

[S802]利用ILoggerFactory工厂创建Ilogger<T>对象

在前面演示的实例中,我们将字符串形式表示的日志类别“Program”作为参数调用ILoggerFactory工厂的CreateLogger方法来创建对应的ILogger对象,实际上我们还可以调用泛型的CreateLogger<T>方法创建一个ILogger<T>对象来完成相同的工作。如果调用这个方法,我们就不需要额外提供日志类别,因为日志类别会根据泛型参数类型T自动解析出来。在如下的代码片段中,我们调用了ILoggerFactory工厂的CreateLogger<Program>方法将对应的 ILogger<Program>对象创建出来。作为日志负载内容的消息模板除了可以采用0,1,...,n这样的占位符,还可以使用任意字符串(“level”)来表示。启动改写的程序之后,输出到控制台和调试输出窗口的内容与图1完全一致的。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var logger = new ServiceCollection()
    .AddLogging(builder => builder
        .AddConsole()
        .AddDebug())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>()
    .CreateLogger<Program>();
var levels = (LogLevel[])Enum
    .GetValues(typeof(LogLevel));
levels = levels.Where(it 
    => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level 
    => logger.Log(level, eventId++, 
    "This is a/an level log message.", level));
Console.Read();

[S803]注入Ilogger<T>对象

除了利用ILoggerFactory工厂来创建泛型的ILogger<Program>对象之外,我们还具有更简洁的方式,那就是按照如下的方式直接利用IServiceProvider对象来提供这个ILogger<Program>对象。换句话说,ILogger<T>实际上是可以作为依赖服务注入到消费它的类型中。

...
var logger = new ServiceCollection()
    .AddLogging(builder => builder
        .AddConsole()
        .AddDebug())
.BuildServiceProvider()
.GetRequiredService<ILogger<Program>>();
...

[S804]TraceSource和EventSource的日志输出

除了控制台和调试器这两种输出渠道,日志框架还提供针对其他输出渠道的支持。第7章重点介绍了针对TraceSource和EventSource的日志框架也是默认支持的两种输出渠道。针对这两种输出渠道的整合由如下两个NuGet包提供的。

  • Microsoft.Extensions.Logging.TraceSource

  • Microsoft.Extensions.Logging.EventSource

在添加了上述两个NuGet包的引用之后,我们对演示实例作了如下的修改。为了捕捉由EventSource分发的日志事件,我们自定义了一个FoobarEventListener类型。我们在应用启动的时候创建了这个FoobarEventListener对象并分别注册了它的EventSourceCreated和EventWritten事件。一个名为“Microsoft-Extensions-Logging”的EventSource会帮助我们完成日志的输出,所以EventSourceCreated事件的处理程序专门订阅了这个EventSource。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Diagnostics.Tracing;

var listener = new FoobarEventListener();
listener.EventSourceCreated += (sender, args) =>

    if (args.EventSource?.Name 
        == "Microsoft-Extensions-Logging")
    
        listener.EnableEvents(
            args.EventSource, EventLevel.LogAlways);
    
;

listener.EventWritten += (sender, args) =>

    var payload = args.Payload;
    var payloadNames = args.PayloadNames;
    if (args.EventName == "FormattedMessage" 
        && payload != null && payloadNames !=null)
    
        var indexOfLevel = payloadNames.IndexOf("Level");
        var indexOfCategory = payloadNames.IndexOf("LoggerName");
        var indexOfEventId = payloadNames.IndexOf("EventId");
        var indexOfMessage = payloadNames.IndexOf("FormattedMessage");
        Console.WriteLine(
            @$"(LogLevel)payload[indexOfLevel],-11: 
 payload[indexOfCategory][ payload[indexOfEventId]]");
        Console.WriteLine($""",-13payload[indexOfMessage]");
    
;

var logger = new ServiceCollection()
    .AddLogging(builder => builder
        .AddTraceSource(
            new SourceSwitch("default", "All"), 
            new DefaultTraceListener  LogFileName = "trace.log" )
        .AddEventSourceLogger())
    .BuildServiceProvider()
    .GetRequiredService<ILogger<Program>();

var levels = (LogLevel[])Enum
    .GetValues(typeof(LogLevel));
levels = levels.Where(it 
    => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level 
    => logger.Log(level, eventId++, 
    "This is a/an level log message.", level));

internal class FoobarEventListener : EventListener
 

上述的EventSource对象在进行日志分发的时候,它会采用不同的方式对将日志消息进行格式化,最终将格式化后的内容作为荷载内容的一部分通过多个事件分发出去,EventWritten事件处理程序选择的是一个名为FormattedMessage的事件,它会将包括格式化日志消息在内的内容荷载信息输出到控制台上。

基于TraceSource和EventSource日志框架的输出渠道是调用ILoggingBuilder的AddTraceSource和AddEventSourceLogger扩展方法进行注册的。针对AddTraceSource扩展方法的调用提供了两个参数,前者是作为全局过滤器的SourceSwitch对象,后者则是注册的DefaultTraceListener对象。由于我们为注册的DefaultTraceListener指定了日志文件的路径,所以输出的日志消息最终会被写入指定的文件中。程序运行后,日志消息会以如图2示的形式同时输出到控制台和指定的日志文件中(trace.log)。

图2 对TraceSource和EventSource的日志输出

[S805]针对等级的日志过滤

对于使用ILogger或者ILogger<T>对象分发的日志事件,并不能保证都会进入最终的输出渠道,因为注册的ILoggerProvider对象会对日志进行过滤,只有符合过滤条件的日志消息才会被真正地输出到对应的渠道。每一个分发的日志事件都具有一个确定的等级。一般来说,日志消息的等级越高,表明对应的日志事件越重要或者反映的问题越严重,自然就越应该被记录下来,所以在很多情况下我们指定的过滤条件只需要一个最低等级,所有不低于(等于或者高于)该等级的日志都会被记录下来。最低日志等级在默认情况下被设置为Information,这就是前面演示实例中等级为Trace和Debug的两条日志没有被真正输出的原因。如果需要将这个作为输出“门槛”的日志等级设置得更高或者更低,我们只需要将指定的等级作为参数调用ILoggingBuilder接口的SetMinimumLevel方法即可。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var logger = new ServiceCollection()
    .AddLogging(builder => builder
    .SetMinimumLevel(LogLevel.Trace)
    .AddConsole())
    .BuildServiceProvider()
.GetRequiredService<ILogger<Program>>();

var levels = (LogLevel[])Enum
    .GetValues(typeof(LogLevel));
levels = levels.Where(it 
    => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level 
    => logger.Log(level, eventId++, 
    "This is a/an level log message.", level));
Console.Read();

如上面的代码片段所示,在调用AddLogging扩展方法时,我们调用ILoggingBuilder接口的SetMinimumLevel方法将最低日志等级设置为Trace。由于设置的是最低等级,所以所有的日志消息都会以图3所示的形式输出到控制台上。


图3 通过设置最低等级控制输出的日志

[S806]针对等级和类别的日志过滤

虽然“过滤不低于指定等级的日志消息”是常用的日志过滤规则,但过滤规则的灵活度并不限于此,很多时候还会同时考虑日志的类别。在创建对应ILogger时,由于一般将当前组件、服务或者类型的名称作为日志类别,所以日志类别基本上体现了日志消息来源。如果我们只希望输出由某个组件或者服务发出的日志事件,就需要针对类别对日志事件实施过滤。综上可知,日志过滤条件其实可以通过一个类型为Func<string, LogLevel, bool>的委托对象来表示,它的两个输入参数分别代表日志事件的类别和等级。下面通过提供这样一个委托对象对日志消息做更细粒度的过滤,所以需要对演示程序做如下修改。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var loggerFactory = new ServiceCollection()
    .AddLogging(builder => builder
        .AddFilter(Filter)
        .AddConsole())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>();

Log(loggerFactory, "Foo");
Log(loggerFactory, "Bar");
Log(loggerFactory, "Baz");

Console.Read();

static void Log(
    ILoggerFactory loggerFactory, string category)

    var logger = loggerFactory.CreateLogger(category);
    var levels = (LogLevel[])Enum
        .GetValues(typeof(LogLevel));
    levels = levels.Where(it 
        => it != LogLevel.None).ToArray();
    var eventId = 1;
Array.ForEach(levels, level 
    => logger.Log(level, eventId++, 
    "This is a/an 0 log message.", level));


static bool Filter(string category, LogLevel level)

    return category switch
    
        "Foo" => level >= LogLevel.Debug,
        "Bar" => level >= LogLevel.Warning,
        "Baz" => level >= LogLevel.None,
        _ => level >= LogLevel.Information,
    ;

如上面的代码片段所示,作为日志过滤器的Func<string, LogLevel, bool>对象定义的过滤规则如下:对于日志类别Foo和Bar,我们只会选择输出等级不低于Debug和Warning的日志;对于日志类别Baz,任何等级的日志事件都不会被选择;至于其他日志类别,我们采用默认的最低等级Information。在执行AddLogging扩展方法时,我们调用ILoggerBuilder接口的AddFilter方法将Func<string, LogLevel, bool>对象注册为全局过滤器。我们利用依赖注入容器提供的ILoggerFactory工厂创建了三个ILogger对象,它们采用的类别分别为“Foo”、“Bar”和“Baz”。我们最后利用这三个ILogger对象分发针对不同等级的六次日志事件,满足过滤条件的日志消息会以图4所示的形式输出到控制台上。


图4 针对类别和等级的日志过滤

[S807]针对等级、类别和ILoggerProvider类型的日志过滤

不论是通过调用ILoggerBuilder接口的SetMinimumLevel方法设置的最低日志等级,还是通过调用AddFilter扩展方法提供的过滤器,设置的日志过滤规则针对的都是所有注册的ILoggerProvider对象,但是有时需要将过滤规则应用到某个具体的ILoggerProvider对象上。如果将ILoggerProvider对象引入日志过滤规则中,那么日志过滤器就应该表示成一个类型为Func<string, string, LogLevel, bool>的委托对象,该委托的三个输入参数分别表示ILoggerProvider类型的全名、日志类别和等级。为了演示针对LoggerProvider的日志过滤,可以将演示程序做如下改动。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Debug;

var logger = new ServiceCollection()
    .AddLogging(builder => builder
        .AddFilter(Filter)
        .AddConsole()
        .AddDebug())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>()
    .CreateLogger("App.Program");

var levels = (LogLevel[])Enum
    .GetValues(typeof(LogLevel));
levels = levels.Where(it 
    => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level 
    => logger.Log(level, eventId++,
    "This is a/an 0 log message.", level));
Console.Read();

static bool Filter(
    string provider, string category, LogLevel level) 
    => provider switch

    var p when p == typeof(ConsoleLoggerProvider).FullName 
        => level >= LogLevel.Debug,
    var p when p == typeof(DebugLoggerProvider).FullName 
        => level >= LogLevel.Warning,
    _ => true,
;

如上面的代码片段所示,我们注册的过滤器体现的过滤规则如下:ConsoleLoggerProvider,和DebugLoggerProvider的最低日志等级分别设置为Debug和Warning,至于其他的ILoggerProvider类型则不做任何的过滤。我们演示程序同时注册了ConsoleLoggerProvider和DebugLoggerProvider,对于分发的12条日志消息,5条会在控制台上输出,3条会出现在Visual Studio的调试输出窗口中。


图5 对ILoggerProvider类型的日志过滤

《ASP.NET Core 6框架揭秘》实例演示[01]:编程初体验
《ASP.NET Core 6框架揭秘》实例演示[02]:各种形式的API开发
《ASP.NET Core 6框架揭秘》实例演示[03]:Dapr初体验
《ASP.NET Core 6框架揭秘》实例演示[04]:自定义依赖注入框架
《ASP.NET Core 6框架揭秘》实例演示[05]:依赖注入基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[06]:依赖注入框架设计细节
《ASP.NET Core 6框架揭秘》实例演示[07]:文件系统
《ASP.NET Core 6框架揭秘》实例演示[08]:配置的基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[09]:将配置绑定为对象
《ASP.NET Core 6框架揭秘》实例演示[10]:Options基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[11]:诊断跟踪的几种基本编程方式 
《ASP.NET Core 6框架揭秘》实例演示[12]:诊断跟踪的进阶用法

asp.netcore6框架揭秘实例演示[01]:编程初体验

本篇提供的20个简单的演示实例基本涵盖了ASP.NETCore6基本的编程模式,我们不仅会利用它们来演示针对控制台、API、MVC、gRPC应用的构建与编程,还会演示Dapr在.NET6中的应用。除此之外,这20个实例还涵盖了针对依赖注... 查看详情

netcore6揭秘怎么样

...的后台服务...2022年3月16日这篇文章主要介绍了ASP.NETCore6框架揭秘实例演示之如何承载你的后台服6框架揭秘实例演示之如何承载你的后台服务...2022年3月16日这篇文章主要介绍了ASP.NETCore6框架揭秘实例演示之如何承载... 查看详情

《asp.netcore6框架揭秘》实例演示[20]:“数据保护”框架基于文件的密钥存储...

...对密钥的创建、撤销和回收的实现原理。[本文节选《ASP.NETCore6框架揭秘 查看详情

《asp.netcore6框架揭秘》实例演示[27]:asp.netcore6minimalapi的模拟实现

...的API,同时提供了与现有API的兼容。[本文节选《ASP.NETCore6框架揭秘》第17章]一、基础模型二、WebApplication三、WebApplicat 查看详情

《asp.netcore6框架揭秘》实例演示[31]:路由高阶用法

ASP.NET的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们在ASP.NET平台上具有举足轻重的地位,MVC和gRPC框架,Dapr的Actor和发布订阅编程模式都建立在路由系统之上。MinimalAPI更是将提升到了... 查看详情

《asp.netcore6框架揭秘》实例演示[10]:options基本编程模式

...#xff0c;这篇文章演示几种典型的编程模式。[本文节选《ASP.NETCore6框架揭秘》第6章][601]将配置绑定为Options对象(源代码&#x 查看详情

《asp.netcore6框架揭秘》实例演示[04]:自定义依赖注入框架

ASP.NETCore框架建立在一个依赖注入框架之上,已注入的方式消费服务已经成为了ASP.NETCore基本的编程模式。为了使读者能够更好地理解原生的注入框架框架,我按照类似的设计创建了一个简易版本的依赖注入框架,并... 查看详情

《asp.netcore6框架揭秘》实例演示[22]:如何承载你的后台服务[补充]

...Core应用最终也体现为这样一个承载服务。[本文节选《ASP.NETCore6框架揭秘》第14章][S1407]利用IHostAppl 查看详情

《asp.netcore6框架揭秘》实例演示[19]:数据加解密与哈希

数据保护(DataProtection)框架旨在解决数据在传输与持久化存储过程中的一致性(Integrity)和机密性(confidentiality)问题,前者用于检验接收到的数据是否经过篡改,后者通过对原始的数据进行加密... 查看详情

asp.netcore6框架揭秘实例演示[30]:利用路由开发restapi

借助路由系统提供的请求URL模式与对应终结点之间的映射关系,我们可以将具有相同URL模式的请求分发给与之匹配的终结点进行处理。ASP.NET的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们... 查看详情

《asp.netcore6框架揭秘》实例演示[28]:自定义一个服务器

作为ASP.NETCore请求处理管道的“龙头”的服务器负责监听和接收请求并最终完成对请求的响应。它将原始的请求上下文描述为相应的特性(Feature),并以此将HttpContext上下文创建出来,中间件针对HttpContext上下文的... 查看详情

《asp.netcore6框架揭秘》实例演示[34]:缓存整个响应内容

我们利用ASP.NET开发的大部分API都是为了对外提供资源,对于不易变化的资源内容,针对某个维度对其实施缓存可以很好地提供应用的性能。《内存缓存与分布式缓存的使用》介绍的两种缓存框架(本地内存缓存和分... 查看详情

《asp.netcore6框架揭秘》实例演示[25]:配置与承载环境的应用

与服务注册一样,针对配置的设置同样可以采用三种不同的编程模式。第一种是利用WebApplicationBuilder的Host属性返回的IHostBuilder对象,它可以帮助我们设置面向宿主和应用的配置。IWebHostBuilder接口上面同样提供了一系列用... 查看详情

《asp.netcore6框架揭秘》实例演示[26]:跟踪应用接收的每一次请求

很多人可能对ASP.NETCore框架自身记录的诊断日志并不关心,其实这些日志对纠错排错和性能监控提供了很有用的信息。如果需要创建一个APM(ApplicationPerformanceManagement)系统来监控ASP.NETCore应用处理请求的性能及出现的... 查看详情

asp.netcore6框架揭秘实例演示[29]:搭建文件服务器

通过HTTP请求获取的Web资源很多都来源于存储在服务器磁盘上的静态文件。对于ASP.NET应用来说,如果将静态文件存储到约定的目录下,绝大部分文件类型都是可以通过Web的形式对外发布的。“Microsoft.AspNetCore.StaticFiles”这... 查看详情

asp.netcore6框架揭秘实例演示[32]:错误页面的n种呈现方式

由于ASP.NET是一个同时处理多个请求的Web应用框架,所以在处理某个请求过程中出现异常并不会导致整个应用的中止。出于安全方面的考量,为了避免敏感信息外泄,客户端在默认情况下并不会得到详细的出错信息,这无疑会在... 查看详情

asp.netcore6框架揭秘实例演示[21]:如何承载你的后台服务(代码片段)

借助.NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NETCore应用最终也... 查看详情

asp.netcore6框架揭秘实例演示[22]:如何承载你的后台服务[补充](代码片段)

借助.NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NETCore应用最终也... 查看详情