zoukankan      html  css  js  c++  java
  • ASP.NET Core MVC 如何使用 Serilog 记录日志(1. 基于配置文件的基本配置)

    原文地址:https://www.cnblogs.com/Kit-L/p/15172128.html

    参考资料

    1. Serilog 官方文档:https://github.com/serilog/serilog/wiki
    2. serilog-aspnetcore 官方文档:https://github.com/serilog/serilog-aspnetcore#serilogaspnetcore
    3. Serilog.Settings.Configuration 官方文档:https://github.com/serilog/serilog-settings-configuration
    4. .netcore入门21:aspnetcore集成Serilog:https://blog.csdn.net/u010476739/article/details/105485315

    0. 照例吐槽

    Serilog 这个框架的中文资料相对来说比较少,中文资料还是 Log4net 和 NLog 的相对比较多。但我还是打算尝试一下 Serilog 这个库。在网上看过一些中文的资料,发现这些资料的配置方式都跟官方文档里默认的配置方式相似,差不多是这样:

    我就算没用过这个库,看过 demo 后也明白个七七八八了。看上面那段代码中的.WriteTo.Console(),估计意思就是日志写入到控制台中,那如果需要把日志写入到文件中,估计会是这样的一个方法:.WriteTo.File(xxx,xxx,xxx...),参数里配上文件路径,文件名等。

    这样配置显然不行。这样配置的话,如果我想切换一下日志输出的路径,岂不是还要改代码重新发布?即使我日志路径从配置文件中来取,那如果我想修改日志记录的等级,修改输出日志的渠道,我岂不是还要修改代码,重新发布项目?这种方式显然只能用于自己的个人小 demo 项目。

    我今天就是想实现一下完全从配置文件来读取对 Serilog 的日志配置,这样会更加灵活,在生产环境中用起来会更舒服。

    1. 准备工作

    首先,我先写一个接口,用来测试log:

    [HttpGet]
    public IActionResult LogTest()
    {
        try
        {
            throw new Exception("This is an exception");
        }
        catch (Exception ex)
        {
            _logger.LogTrace(ex, "出现异常!");
            _logger.LogDebug(ex, "出现异常!");
            _logger.LogInformation(ex, "出现异常!");
            _logger.LogWarning(ex, "出现异常!");
            _logger.LogError(ex, "出现异常!");
            _logger.LogCritical(ex, "出现异常!");
            return Ok("Exception!");
        }
    }
    

    调用一下接口,看一下输出的日志:

    可以看到 information 级别以下的日志都没有输出,需要在 appsettings.json中配置一下:

    后面日志就会全输出了。

    2. 基本配置

    引入 Serilog.AspNetCore 这个包,我这里选择稳定版本 4.1.0 。看一下这个包依赖了哪些包:

    这里截图里显示的是 .NETCoreApp 3.1 的环境下依赖的包,.NET 5 环境下也是一样的,不一样的只是包的版本号。.NET 5 的在下面,我截图截不到了。

    仅仅看依赖的这些包名,我们可以看到该有的基本都有了。前三个自然不必说,Serilog.Extensions.Hosting 这个包显然是用来在 Build ASP.NET 5 的 Host 的时候用的,我估计文档里的.UseSerilog()这个方法就在这个包里。Serilog.Formatting.Compact 这个包显然是用来格式化输出的,估计很多用户夸赞的把日志用 JSON 格式来“结构化”输出相关功能的代码就在这个包里。Serilog.Settings.Configuration 这个包显然是用来配置 Serilog 的。后面三个显然是将日志输出到控制台,Debug 窗口和文件的三个包。

    在 ASP.NET Core 的项目中配置 Serilog,自然要看 Serilog.AspNetCore 的文档,地址:https://github.com/serilog/serilog-aspnetcore

    我们顺着文档来。包安装过程就不详述了,在对应的目录执行dotnet add xxxx命令即可,文档里已经写了。

    安装完之后,打开 ASP.NET Core 项目的 Program.cs文件,来进行 Serilog 的配置。我们看一眼官方文档:

    上面黑灰色炭笔圈出来的部分我们不用管,实际用的时候也可以按照它写的这个方式来写,看上去挺靠谱的。但红圈圈出来的这部分,是 new 一个LoggerConfiguration对象,并进行一些配置,最后 Create 一个 Logger,并赋值给一个全局的变量 Log 的 Logger 属性。我估计配置好这一步之后,我们使用 ILogger 的实现的实例,也就是我前面写的代码中的private readonly ILogger<HomeController> _logger;来记录日志时,使用的就是这里的配置。这个就与我们完全依赖配置文件进行配置的想法相悖了,我也暂时觉得没什么必要来 Log 这个 Web 项目的启动和退出过程。我们暂时不写这一段代码。

    然后最下方划红线的这个 UseSerilog 是必须的,我们要在代码里写上。

    此时将项目 Run 起来,发现控制台里的日志是这样的:

    正在生成,然后就没下文了。然后我们用 Postman 调用一下接口:

    可以拿到返回值,但控制台还是什么日志都没有输出,可以说明代码已经开始跑了,记录日志的代码也都已经跑过了,只是没有日志输出出来,说明我们的项目已经丧失了记录日志的能力。而且我这个项目是一个 MVC 项目,运行的时候会自动在浏览器打开http://localhost:5000/,显示页面,这次也没有,我手动在浏览器里访问http://localhost:5000/也无法访问页面,在浏览器例访问http://localhost:5000/Home/LogTest来尝试调用接口,也会500。只有 Postman 能调用成功。估计是 ASP.NET Core MVC 日志没有配置好,导致整个项目启动出现了问题,完全无法用浏览器来访问。

    根据我长达一年的编程经验(雀实只有一年,很抱歉。幸运的是我得到了一些热心的大佬的无私指导,不然估计现在还在 hello world),我猜测是因为我们使用了.UseSerilog()来用 Serilog 接管了内置的日志功能,但这个方法没有配置好默认的配置程序,我们也没有手动进行日志的配置,所以出了这种问题。将.UseSerilog()注释掉则一切恢复正常。

    3. 更进一步

    为了寻求解决办法,我们继续阅读文档:

    这一段对我们有用的是化橙色线的几句话。上面这一句是文档让我们删除掉配置文件中的跟日志相关的配置,改用 Serilog 的日志配置。然后给了一个 Serilog configuration的链接(https://github.com/serilog/serilog-settings-configuration)和一个示例项目the Sample project的链接(https://github.com/serilog/serilog-aspnetcore/tree/dev/samples/Sample)。配置的链接指向的文档我们迟早要去看,估计这个文档是更详细的配置项。我们这里先看一下这个示例项目。我们在链接的 github 后面加个 '1s'(与那位年长的先生无关),然后将链接粘贴到浏览器中,就可以直接用在线版的 VSCode 打开这个示例:https://github1s.com/serilog/serilog-aspnetcore/tree/dev/samples/Sample

    首先看一眼配置文件:

    可以看到默认的日志相关配置文件已经被删掉,替换成了 Serilog 的。窥一斑而知全豹,我们先推测一下配置文件的作用。上面那一段配置文件MinimumLevel起到的作用应该跟默认的配置文件中的LogLevel作用相似,是确定记录的日志最低等级的相关配置。

    而下面这一端WriteTo就非常有意思了,这显然是配置了一个日志输出的渠道。其中NameFile,也就是说这段配置能够把配置输出到文件中。那我们就基本可以确定,假设我们再补一个WriteTo,把Name配置为Console,就能把日志输出到控制台。

    看到这里,我们可以猜想 Serilog 是能够只通过配置文件来进行日志配置的。

    再看一下这个示例的Program.cs文件:

    看来可以直接在这个方法里面用委托来配置。但看到红圈里最后一行还是有一条.WriteTo.Console(),我开始方了,难道一定要在这里写一个WriteTo才能指定日志输出的渠道吗?

    我们把鼠标移到UseSerilog()上看一下:

    怎么感觉弹出来的方法签名不太一样,上面弹出的代码中,这个方法的声明有一个 Action 委托参数,但这个委托只有两个参数,一个 xxxContext, 一个 xxxConfiguration,然而这个文件里却给了三个参数,中间多了个 Services。

    但是我们管不了那么多了,直接 ctrl + 鼠标左键点进去看源码:

    看源码就跟看英文阅读理解一样,先忽略掉意义不大的地方,比如说上面的判断 null 的部分,专注英文文章中有实际意义的词,也就是名词、动词、形容词、副词。在代码里,也就是实际的代码逻辑部分。(其实我英语非常弱,六级都没过,在这里斗胆用英语阅读理解做比喻真的是抱歉)。

    上面源码中,使用了 IWebHostBuilder 的 ConfigureServices 方法,跟 Startup.cs 中的同名方法作用是差不多的。在这方法里,传入了一个委托,参数是 context 和 collection。在委托体中,new 了一个 loggerConfiguration 和 loggerProviders。其中 loggerConfiguration 会被我们在 Program.cs 中调用 UseSerilog() 方法时传入的委托赋值,也就是它:

    loggerProviders 的赋值,则取决于调用 UseSerilog() 时传入的后两个 bool 类型的值 preserveStaticLogger 和 writeToProviders,默认全是 false,这里就全当 false 来看。因为 writeToProviders 是 false,loggerProviders 将不会被再次赋值,它的值会停留在null。

    后面又用已经得到值的 loggerConfiguration 来 Create 出了一个 logger,接着创建了一个 registeredLogger,初始化为 null。由于 preserveStaticLogger 为 false,所以 registeredLogger 的值也停留在了 null,而全局的 Log 变量的 Logger 属性将会被赋值为前面刚刚被 Create 出来的 logger。

    再后面就是用单例模式创建服务等操作,这部分代码不用看也能猜到是干啥了。

    这一段阅读代码的部分,用文字叙述不清楚,如果是录成视频就更好了。在看我的博客的朋友,如果跟我一样基础比较薄弱,可以认真看几遍上面的源码,慢慢理解,不要着急。

    看完这段代码,我们也基本知道了为什么刚才程序跑不起来:

    1. 我们没有调用 UseSerilog 方法时传给它 configureLogger 这个 Action 委托,导致 UseSerilog 使用了另一个可以不传任何参数的重载:

    从而导致配置出错,项目无法正常运行起来。

    1. 我们也没有跟教程中说的那样直接在 Main 方法中给 Log.Logger 赋值,就像这样:

    4. 尝试配置

    既然知道了原因,我们就传一个委托进去。我们先把配置文件改成跟上面示例中相似:

    "Serilog": {
      "MinimumLevel": {
        "Default": "Information",
        "Override": {
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information",
          "System": "Warning"
        }
      },
      "Enrich": [ "FromLogContext" ],
      "WriteTo": [
        {
          "Name": "File",
          "Args": { "path":  "./logs/log-.txt", "rollingInterval": "Minute" }
        },
        {
          "Name": "Console",
          "Args": {}
        },
        {
          "Name": "Debug",
          "Args": {}
        }
      ]
    },
    ...
    

    WriteTo 第一项为 File,即日志写入文件。这一项的参数 Args 的 rollingInterval(滚动间隔)我们暂时改为 Minute,测试完再改回 Day,目的是测试 Serilog 把日志按时间切分的功能。同时我给 WriteTo 这一节多加了两段配置,期望它能将日志输出到控制台(Console)和 Debug 窗口。

    然后"Enrich":["FromLogContext"]的含义可以从这篇文档(https://github.com/serilog/serilog/wiki/Enrichment#the-logcontext)里找到。大致翻译一下就是“Serilog.Context.LogContext 可用于从环境“执行上下文”中动态添加和删除属性;例如,在事务期间写入的所有消息都可能带有该事务的 id,等等。必须在配置时使用 .FromLogContext() 将此功能添加到记录器。”,看起来是我没有接触过的高端功能。我连事务是什么都不太清楚。先照抄到配置文件中。

    然后我们传给 UseSerilog() 方法一个委托:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog((hostBuilderContext, loggerConfiguration) => loggerConfiguration
                .ReadFrom.Configuration(hostBuilderContext.Configuration))
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
    

    其中loggerConfiguration.ReadFrom.Configuration(hostBuilderContext.Configuration)是从项目设定的配置文件,也就是默认的 appsettings.{environment}.json 中读取配置。hostBuilderContext.Configuration就是取项目配置好的的配置文件。我期望这样可以实现所有配置完全从配置文件中读取。

    5. 测试配置

    将项目跑起来,重新用 Postman 调用接口:

    可以看到日志已经可以正常输出到控制台上了,非常好。再看一下这个应用程序的根目录下有没有产生我们在配置文件中配置的 logs 文件夹,以及日志有没有被记录到对应的日志文件中:

    可以看到文件夹产生了,日志也按照分钟来分了,今天凌晨1点17分和1点29分分别都有日志生成,被分配到了两个文件,文件的后缀是202108220117和202108220129,非常好。

    6. 总结,但并未完结

    目前,日志功能是基本上能用了,我们也实现了完全从配置文件中读取配置信息来配置 Serilog,但还有几点不够完善,不够规范:

    1. 日志记录到项目根路径中不够规范。如果这样记的话,假设项目是用 Docker 发布的,会不会把日志记录到 Docker 容器中去呢?(我也不太清楚)下次发布,容器被替换,日志可能就没了。所以最好是记录到服务器的一个绝对路径中。这样也方便什么 ELK 之类的(我也暂时没用过,只是听说过可以这样用,算是个云用户吧,真是抱歉)从服务器指定位置取日志。
    2. 所有等级的日志全都记录到了一个文件中,日志并没有根据日志等级,即 Info,Error,Warning,Debug 等分到不同文件中。我认为日志分到不同文件中才更方便查阅日志来定位问题。
    3. 日志不是结构化的,如果日志被以 Json 格式,结构化地一条一条记录下来,将会相当容易查找。

    我期望后面能够实现上面的 2,3 条。我感觉这些只需要通过修改配置文件来实现。应该需要查阅这份文档:https://github.com/serilog/serilog-settings-configuration

    感谢阅读,如果这篇博客有帮到您的话,请点个赞,谢谢。

  • 相关阅读:
    USACO--2.1The Castle
    浅谈python字符串存储形式
    面向对象——一起来复习托付与事件!
    数据结构——算法之(032)(求两个串中的第一个最长子串)
    读《浪潮之巅》有感
    关于android 怎样安装 assets文件下的apk
    每日一小练——求质数
    怎样使破解网页的禁止复制黏贴
    Angularjs Nodejs Grunt 一个样例
    《TCP/IP具体解释卷2:实现》笔记--域和协议
  • 原文地址:https://www.cnblogs.com/Kit-L/p/15172128.html
Copyright © 2011-2022 走看看