基于.NetCore3.1系列 —— 日志记录之日志配置揭秘

500次阅读  |  发布于4年以前

前言

在项目的开发维护阶段,有时候我们关注的问题不仅仅在于功能的实现,甚至需要关注系统发布上线后遇到的问题能否及时的查找并解决。所以我们需要有一个好的解决方案来及时的定位错误的根源并做出正确及时的修复,这样才能不影响系统正常的运行状态。

这个时候我们发现,其实在asp.net core中已经内置了日志系统,并提供了各种内置和第三方日志记录提供程序的日志记录接口,在进行应用开发中,可以进行统一配置,并且利用第三方日志框架相结合,更加有效的实现日志记录。所以在这个系列中,主要是对内置日志记录系统的学习,以及后续使用第三方日志框架集成我们需要的日志系统。

说明

在这一篇中主要是对日志记录的配置进行说明,从开始配置日志,以及后续使用配置进行日志处理。

在新建项目成功之后,我们都会看到一个命名为appsettings.json配置,打开一看,短短的几行配置,

 "Logging": {
   "LogLevel": {
     "Default": "Information",
     "Microsoft": "Warning",
     "Microsoft.Hosting.Lifetime": "Information"
  }
},

然后启动运行的时候,程序会在调试面板和控制台中分别输出显示来源如下:

在控制台中:

在调试面板中:

这里的日志配置,在系统中到底都起来什么作用?让我们来一探究竟吧!

开始

默认配置

我们查看源代码发现,在程序的入口点中发现,在初始化时候,通过CreateDefaultBuilder方法来实现日志记录的默认配置。

public class Program
{
   public static void Main(string[] args)
  {
       CreateHostBuilder(args).Build().Run();
  }

   public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
          .ConfigureWebHostDefaults(webBuilder =>
          {
               webBuilder.UseStartup<Startup>();
          });
}

所以下面我们看一下CreateDefaultBuilder在源码中都对日志做了哪些默认配置?

       public static IHostBuilder CreateDefaultBuilder(string[] args)
      {
           var builder = new HostBuilder();

           builder.UseContentRoot(Directory.GetCurrentDirectory());
           builder.ConfigureHostConfiguration(config =>
          {
               config.AddEnvironmentVariables(prefix: "DOTNET_");
               if (args != null)
              {
                   config.AddCommandLine(args);
              }
          });
           builder.ConfigureAppConfiguration((hostingContext, config) =>
          {
               var env = hostingContext.HostingEnvironment;

               config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

               if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
              {
                   var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                   if (appAssembly != null)
                  {
                       config.AddUserSecrets(appAssembly, optional: true);
                  }
              }
               config.AddEnvironmentVariables();

               if (args != null)
              {
                   config.AddCommandLine(args);
              }
          })
          .ConfigureLogging((hostingContext, logging) =>
          {
               var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
               if (isWindows)
              {
                   logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
              } logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
               logging.AddConsole();
               logging.AddDebug();
               logging.AddEventSourceLogger();
               if (isWindows)
              {
                   logging.AddEventLog();
              }
          })
          .UseDefaultServiceProvider((context, options) =>
          {
               var isDevelopment = context.HostingEnvironment.IsDevelopment();
               options.ValidateScopes = isDevelopment;
               options.ValidateOnBuild = isDevelopment;
          });
           return builder;
      }

通过上面这一段源码我们可以看到一个命名为ConfigureLogging的对象,我们根据命名的意思大致可以看出,这是一个配置日志的方法,继续查看ConfigureLogging源码

   public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<HostBuilderContext, ILoggingBuilder> configureLogging)
  {
       return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));
  }

通过IServiceCollection注册服务集合容器,将日志服务添加到这个服务容器,使用AddLogging方法实现对日志服务的注册。

   public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
  {
       if (services == null)
      {
           throw new ArgumentNullException(nameof(services));
      }

       services.AddOptions();
       services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
       services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

       services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
           new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));

       configure(new LoggingBuilder(services));
       return services;
  }

通过AddLogging添加到服务集合容器,先通过添加所需的配置AddOptions,通过注入的方式实现默认的ILoggerFactory,ILogger ( 这个会在后续的篇章中进行说明),再后通过LoggingBuilder完成日志对象的创建,

public interface ILoggingBuilder
{
   IServiceCollection Services { get; }
}
internal class LoggingBuilder : ILoggingBuilder
{
   public LoggingBuilder(IServiceCollection services)
  {
   Services = services;
  }

   public IServiceCollection Services { get; }
}

对日志系统的配置,用于提供程序的接口,ILoggingBuilder后面可以对该对象进行拓展使用。

通过以上的流程CreateDefaultBuilder方法,实现对预先配置的默认值初始化,因此也发现了其中的ConfigureLogging也是其中要进行默认初始化的值,也就是系统默认的日志配置。

单独把ConfigureLogging这一块的源码拎出来再看看:

      .ConfigureLogging((hostingContext, logging) =>
      {
           var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
           if (isWindows)
          {
               logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
          }
           logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
           logging.AddConsole();
           logging.AddDebug();
           logging.AddEventSourceLogger();

           if (isWindows)
          {
               logging.AddEventLog();
          }
      })

在asp.net core启动中,根据操作系统平台适应不同的服务,在windows服务中,将EventLogLoggerProvider的默认值设置为警告或者更高的级别。

AddConfiguration : 添加系统日志的全局配置。

在配置中,可以根据提供的不同类型程序来针对实现日志记录的输出方式。而这里默认实现的AddConsole()AddDebug() 和AddEventSourceLogger()分别是将日志输出到控制台、调试窗口中,以及提供写入事件源。

AddConsole : 添加控制台到工厂方法中,用来将日志记录到控制台中。

AddDebug : 添加Debug窗口到工厂方法中,用来将日志记录到窗口中。

说明:asp.net core 内置的日志接口中,实现了多种内置的日志提供器,除了上面默认实现的ConsoleDebugEventSource,还包括下面的这几个

EventLog :

TraceSource

AzureAppServicesFile

AzureAppServicesBlob

ApplicationInsights

还记得上面提到的appsettings.json配置吗?在这里,我们来看看

{
 "Logging": {
   "LogLevel": {
     "Default": "Debug",
     "Microsoft": "Information"
  },
   "Console": {
     "LogLevel": {
       "Default": "Debug",
       "System": "Warning"
    }
  }
}
}

AddConfiguration中,

logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));

获取配置文件的Logging数据,实现全局配置,

       public static ILoggingBuilder AddConfiguration(this ILoggingBuilder builder, IConfiguration configuration)
      {
           builder.AddConfiguration();
           builder.Services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(new LoggerFilterConfigureOptions(configuration));
           builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration));
           builder.Services.AddSingleton(new LoggingConfiguration(configuration));
           return builder;
      }
internal class LoggerFilterConfigureOptions : IConfigureOptions<LoggerFilterOptions>
{
   private const string LogLevelKey = "LogLevel";
   private const string DefaultCategory = "Default";
   private readonly IConfiguration _configuration;

   public LoggerFilterConfigureOptions(IConfiguration configuration)
  {
       _configuration = configuration;
  }

   public void Configure(LoggerFilterOptions options)
  {
       LoadDefaultConfigValues(options);
  }

   private void LoadDefaultConfigValues(LoggerFilterOptions options)
  {
       if (_configuration == null)
      {
           return;
      }

       options.CaptureScopes = _configuration.GetValue(nameof(options.CaptureScopes), options.CaptureScopes);

       foreach (var configurationSection in _configuration.GetChildren())
      {
           if (configurationSection.Key.Equals(LogLevelKey, StringComparison.OrdinalIgnoreCase))
          {
               // Load global category defaults
               LoadRules(options, configurationSection, null);
          }
           else
          {
               var logLevelSection = configurationSection.GetSection(LogLevelKey);
               if (logLevelSection != null)
              {
                   // Load logger specific rules
                   var logger = configurationSection.Key;
                   LoadRules(options, logLevelSection, logger);
              }
          }
      }
  }
   private void LoadRules(LoggerFilterOptions options, IConfigurationSection configurationSection, string logger)
  {
       foreach (var section in configurationSection.AsEnumerable(true))
      {
           if (TryGetSwitch(section.Value, out var level))
          {
               var category = section.Key;
               if (category.Equals(DefaultCategory, StringComparison.OrdinalIgnoreCase))
              {
                   category = null;
              }
               var newRule = new LoggerFilterRule(logger, category, level, null);
               options.Rules.Add(newRule);
          }
      }
  }
}

以上是AddConfiguration实现的整体流程源码,默认注册实现LoggerFilterConfigureOptions对配置数据的读取,其中定义的 LogLevelKey = "LogLevel"DefaultCategory = "Default" 默认字符串,以此来获取默认全局配置数据。

在默认配置的文本格式appsettings.json中,Logging属性可以具有LogLevel和日志提供程序属性。Logging 下的 LogLevel 属性指定了用于记录所选类别的最低级别。在本例中, Microsoft 类别在 Information 级别记录,其他均在 Debug 级别记录。

日志级别说明:每一个日志都有指定的日志级别值,日志级别判断指示严重性或重要性。使用日志等级可以很好的过滤想要的日志,记录日志记录问题的同时,甚至为我们提供非常详细的日志信息。

LogLevel 严重性:Trace < Debug < Information < Warning < Error < Critical < None。

日志级别 常用场景
Trace = 0 记录一些对程序员调试问题有帮助的信息, 其中可能包含一些敏感信息, 所以应该避免在 生产环境中启用Trace日志,因此不应该用于生产环境。默认应禁用。
Debug = 1 记录一些在开发和调试阶段有用的短时变 量(Short-term usefulness), 所以除非为了临时排除生产环境的 故障,开发人员应该尽量避免在生产环境中启用Debug日志,默认情况下这是最详细的日志。
Information = 2 记录跟踪应用程序的一些流程, 例如,记录当前api请求的url。
Warning = 3 记录应用程序中发生出现错误或其它导致程序停止的流程异常信息。这些信息中可能包含错误消息或者错误产生的条件, 可供后续调查,例如, 文件未找到
Error = 4 记录应用程序中某个操作产生的错误和异常信息。这些消息应该指明当前活动或操作(比如当前的 HTTP 请求),而不是应用程序范围的故障。
Critical = 5 记录一些需要立刻修复,急需被关注的问题,应当记录关键级别的日志。例如数据丢失,磁盘空间不足等。

日志级别只需要简单的通过 AddFilter 对日志的过滤级别配置一下就行了。同时也可以通过自定义在

Logging.{providername}.LogLevel 中指定了级别,则这些级别将重写 Logging.LogLevel 中设置的所有内容。(在下文自定义中说明)

由此可以看出,日志记录提供程序配置由一个或多个配置提供程序提供,如文件格式(系统自带的appsettings.json)或者通过(已安装或已创建的)自定义提供程序(下文会说明自定义方式)。

自定义配置

看完了上面实现的默认配置之后,我们也清楚了可以修改默认配置实现不同等级日志的输出,因此,我们也可以通过自定义的方式,对默认配置的修改,实现我们想要的日志记录方式。

可以通过自行选择添加提供程序来替换默认配置的提供的程序。这样就实现自定义。自定义的方式有很多,比如

1. 代码添加提供程序

调用ClearProviders,清除默认之后,可添加所需的提供程序。如下:

   public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args) //可以看出在使用模板创建项目的时候,默认添加了控制台和调试日志组件,并从appsettings.json中读取配置。
      .ConfigureLogging((hostingContext, logging) =>
      {
           logging.ClearProviders(); //去掉默认添加的日志提供程序
           //添加控制台输出
           logging.AddConsole();
           //添加调试输出
           logging.AddDebug();
      })
      .ConfigureWebHostDefaults(webBuilder =>
      {  
           webBuilder.UseStartup<Startup>();
      });
}

由上可以发现我们可以通过在入口程序中直接对添加ConfigureLogging(在上文中源码可以看出)拓展方法来实现我们的自定义配置。

2. 代码添加过滤器

过滤器AddFilter,添加过滤规则,可以为不同的日志提供者指定不同的过滤器,实现有效的自定义日志的输出。如下代码:

.ConfigureLogging(logging =>
   logging.AddFilter("System", LogLevel.Debug)
          .AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace))

添加指定了全局的过滤器,作用于所有日志提供者,示例中的第二个 AddFilter 使用类型名称来指定调试提供程序。第一个 AddFilter 应用于全部提供程序,因为它未指定提供程序类型。

这里的AddFilter其实于之前读取配置文件信息添加配置AddConfiguration的作用相似,只是从配置文件的逻辑改成了以代码的方式实现过滤筛选,到最终也是对ConfigureOptions的配置。

3. 配置文件自定义

ASP.NET Core默认会从appSetting.json中的Logging属性读取日志的配置(当然你也可以从其他文件中读取配置),这里设置了不同的日志提供器产生的最低的日志级别,配置样例如下。

{
 "Logging": {
   "Debug": {
     "LogLevel": {
       "Default": "Information"
    }
  },
   "Console": {
     "LogLevel": {
       "Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
       "Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
       "Microsoft.AspNetCore.Mvc.Razor": "Error",
       "Default": "Information"
    }
  },
   "LogLevel": {
     "Default": "Debug"
  }
}
}

此 JSON 将创建 6 条筛选规则:Debug中1 条用于调试提供程序,Console中 4 条用于控制台提供程序,最后一条LogLevel 用于所有提供程序。创建 ILogger 对象时,为每个提供程序选择一个规则。

问题

虽然在这一节中只是对日志记录的配置进行了说明,但是在后续中也会对日志内部的核心运行机制进行说明介绍。所以,在这一篇中留下几个疑问

  1. 日志记录的输出可以在哪里查看?而又由什么实现决定的呢?
  2. 如何管理输出不同的日志呢?都有哪些方式呢?

以上的这些内容,会在下一篇进行介绍说明。

好了,今天的日志配置内容就说到这里了,希望能给大家在使用Core开发项目中对日志系统有进一步的认识。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8