65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Serilog 增强 ASP.NET Core 日志管道

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018年11月13日

CPOL

3分钟阅读

viewsIcon

6949

如何使用 Serilog 增强 ASP.NET Core 日志管道

引言

在这篇文章中,我想展示一下如何将默认的日志管道替换为 Serilog,它由社区实现了更多提供程序,并且还提供了一种记录结构化数据的方法。

背景故事

对于那些在 ASP.NET Core 1.1 或更早版本中创建过项目的人来说,您可能还记得 Program.cs 文件看起来像这样

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup()
                .UseApplicationInsights()
                .Build();

            host.Run();
        }
    }
}

正如您所看到的,在 ASP.NET Core 的早期版本中,应用程序入口点的设置方式更加明确。现在,从 ASP.NET Core 2.0 及更高版本开始,默认的 Program.cs 文件看起来像这样

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup();
    }
}

虽然默认构建器可以很好地清理代码,但它确实添加了一些默认(顾名思义)配置,这些配置并不那么明显。

如果我们看一下 WebHost.CreateDefaultBuilder 实际做了什么,我们会看到以下内容

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder();

    if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
    {
        builder.UseContentRoot(Directory.GetCurrentDirectory());
    }

    if (args != null)
    {
        builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
    }

    builder.UseKestrel((builderContext, options) =>
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"));
        })
        .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())
            {
                var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                if (appAssembly != null)
                {
                    config.AddUserSecrets(appAssembly, optional: true);
                }
            }

            config.AddEnvironmentVariables();

            if (args != null)
            {
                config.AddCommandLine(args);
            }
        })
        // THIS IS THE PART WE'RE INTERESTED IN. (INTEREST!!!)
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole();
            logging.AddDebug();
        })
        .ConfigureServices((hostingContext, services) =>
        {
            // Fallback
            services.PostConfigure(options =>
            {
                if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                {
                    // "AllowedHosts": "localhost;127.0.0.1;[::1]"
                    var hosts = hostingContext.Configuration["AllowedHosts"]?.Split
                                (new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                    // Fall back to "*" to disable.
                    options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
                }
            });
            // Change notification
            services.AddSingleton<IOptionsChangeTokenSource>(
                new ConfigurationChangeTokenSource(hostingContext.Configuration));

            services.AddTransient();
        })
        .UseIISIntegration()
        .UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
        });

    return builder;
}

好吧,对于一个开始来说,这确实是大量的配置,幸好它隐藏在像 CreateDefaultBuilder 这样的简单调用后面。

现在,如果我们查看上面的代码片段(我用 INTEREST!!! 标记了它,以便于查找),您会看到,默认情况下,配置会将日志记录发送到控制台和调试通道,我们不需要这个,因为我们将使用不同的控制台,并且没有必要让两个提供程序同时写入同一个控制台。

更改

因此,我们要做的第一个更改如下

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

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureLogging(
                (webHostBuilderContext, loggingBuilder) =>
                    {
                        loggingBuilder.ClearProviders();
                    })
            .UseStartup();
}

通过此更改,我们清除了控制台和调试提供程序,因此基本上现在我们没有设置任何日志记录。

现在我们将添加以下 Nuget 包(请注意,其中只有两个是必需的,所有其他 sinks 都取决于您自己的选择)

  • Serilog(这是主包,是必需的)
  • Serilog.Extensions.Logging(这用于与 ASP.NET Core 管道集成,它还将安装 Serilog 作为依赖项)
  • Serilog.Sinks.ColoredConsole(此包添加了一个彩色控制台输出,使区分日志级别和消息更容易,这也将安装 Serilog 作为依赖项)
  • Serilog.Enrichers.Demystify(此包处于预发布状态,但它使得来自涵盖 async 方法的异常的长堆栈跟踪变成对开发人员更友好的堆栈跟踪)

安装这些包后,我们将再次更改 Program.cs 文件,它最终看起来像这样

namespace WebApplication1
{
    using System;

    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Logging;

    using Serilog;
    using Serilog.Extensions.Logging;

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

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureLogging(
                    (webHostBuilderContext, loggingBuilder) =>
                        {
                            loggingBuilder.ClearProviders();

                            Serilog.Debugging.SelfLog.Enable(Console.Error); // this outputs 
                              // internal Serilog errors to the console in case something 
                              // breaks with one of the Serilog extensions or the framework itself

                            Serilog.ILogger logger = new LoggerConfiguration()
                                .Enrich.FromLogContext() // this adds more information 
                                    // to the output of the log, like when receiving http requests, 
                                    // it will provide information about the request
                                .Enrich.WithDemystifiedStackTraces() // this will change the 
                                    // stack trace of an exception into a more readable form 
                                    // if it involves async
                                .MinimumLevel.Verbose()   // this gives the minimum level to log, 
                                                          // in production the level would be higher
                                .WriteTo.ColoredConsole() // one of the logger pipeline elements 
                                                          // for writing out the log message
                                .CreateLogger();

                            loggingBuilder.AddProvider(new SerilogLoggerProvider
                                     (logger)); // this adds the serilog provider from the start
                        })
                .UseStartup();
    }
}

现在,我们将 Serilog 集成到 ASP.NET Core 所有组件使用的日志记录主管道中。请注意,我们还可以访问 webHostBuilderContext,它有一个 Configuration 属性,允许我们从应用程序配置中读取,以便我们可以设置更复杂的管道,并且还有一个 nuget 包允许 Serilogappsettings.json 文件中读取。

可选地,Serilog 还允许日志消息携带一些附加属性,为此,我们需要将默认的 outputTemplate"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}{NewLine}{Exception}" 更改为 "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level} {Properties} {Message}{NewLine}{Exception}";注意 Properties 模板占位符,这是 serilog 将放置所有不在实际消息中的附加信息的地方,例如来自 http 请求的数据。要查看此更改的外观,请参见以下内容

Serilog.ILogger logger = new LoggerConfiguration()
    .Enrich.FromLogContext()             // this adds more information to the output of the log, 
                                         // like when receiving http requests, it will provide 
                                         // information about the request
    .Enrich.WithDemystifiedStackTraces() // this will change the stack trace of an exception 
                                         // into a more readable form if it involves async
    .MinimumLevel.Verbose()              // this gives the minimum level to log, in production 
                                         // the level would be higher
    .WriteTo.ColoredConsole(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} 
             {Level} {Properties} {Message}{NewLine}{Exception}") // one of the logger pipeline 
                                                  // elements for writing out the log message
    .CreateLogger();

结论

请注意,设置日志记录管道的方式与应用程序一样多,这只是我的个人偏好。

另外,如果您想知道为什么我选择在 Program.cs 文件中进行更改而不是在 Startup.Configure() 方法中进行更改,正如一些示例在线显示的那样,那是因为我相信如果默认日志记录设置在它自己的专用函数中,那么这也应该设置,这也在使用 Startup 方法之前引入了 Seriloger,这反过来提供了更多信息。

我希望您喜欢这篇文章,并且它将帮助您更好地调试和维护您的应用程序。

谢谢,下次再见。 🙂
祝好!

© . All rights reserved.