Skip to main content

18. 日志记录

18.1 关于日志

通常日志指的是系统日志程序日志

系统日志 是记录系统中硬件、软件和系统问题的信息,同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因,或者寻找受到攻击时攻击者留下的痕迹。系统日志包括系统日志、应用程序日志和安全日志。

程序日志 是程序运行中产生的日志,通常由框架运行时或开发者提供的日志。包括请求日志,异常日志、审计日志、行为日志等。

18.2 日志作用

在项目开发中,都不可避免的使用到日志。没有日志虽然不会影响项目的正确运行,但是没有日志的项目可以说是不完整的。日志在调试,错误或者异常定位,数据分析中的作用是不言而喻的。

  • 调试

在项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。

  • 错误定位

不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。

  • 数据分析

大数据的兴起,使得大量的日志分析成为可能,ELK 也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。

18.3 日志级别

日志级别可以有效的对日志信息进行归类,方便准确的查看特定日志内容。通常日志类别有以下级别:

级别方法描述
Trace(跟踪)0LogTrace包含最详细的消息。 这些消息可能包含敏感的应用数据。 这些消息默认情况下处于禁用状态,并且不应在生产中启用。
Debug(调试)1LogDebug用于调试和开发。 由于量大,请在生产中小心使用。
Information(信息)2LogInformation跟踪应用的常规流。 可能具有长期值。
Warning(警告)3LogWarning对于异常事件或意外事件。 通常包括不会导致应用失败的错误或情况。
Error(错误)4LogError表示无法处理的错误和异常。 这些消息表示当前操作或请求失败,而不是整个应用失败。
Critical(严重)5LogCritical需要立即关注的失败。 例如数据丢失、磁盘空间不足。

18.4 如何使用

.NET 5 框架中,微软已经为我们内置了 日志组件,正常情况下,无需我们引用第三方包进行日志记录。.NET 5 框架为我们提供了两种日志对象创建方式。

18.4.1 ILogger<T> 泛型方式

使用非常简单,可以通过 ILogger<T> 对象进行注入,如:

public class PrivacyModel : PageModel{    private readonly ILogger<PrivacyModel> _logger;    public PrivacyModel(ILogger<PrivacyModel> logger)    {        _logger = logger;    }    public void OnGet()    {        _logger.LogInformation("GET Pages.PrivacyModel called.");    }}
小知识

通过泛型 ILogger<T> 方式写入日志,那么默认将 T 类型完整类型名称作为 日志类别

18.4.2 ILoggerFactory 工厂方式

使用工厂方式,需手动传入 日志类别,如:

public class ContactModel : PageModel{    private readonly ILogger _logger;    public ContactModel(ILoggerFactory logger)    {        _logger = logger.CreateLogger("MyCategory");    }    public void OnGet()    {        _logger.LogInformation("GET Pages.ContactModel called.");    }}

18.4.3 懒人模式 😁

Furion 框架中,提供了更懒的方式写入日志,也就是通过字符串拓展的方式写入,如:

"简单日志".LogInformation();"百小僧 新增了一条记录".LogInformation<HomeController>();"程序出现异常啦".LogError<HomeController>();"这是自定义类别日志".SetCategory("类别").LogInformation();

通过字符串拓展方式可以在任何时候方便记录日志,专门为懒人提供的。

18.5 配置日志输出介质

版本说明

以下小节仅在 Furion 3.9.0+ 版本提供。

ASP.NET Core 应用程序中,主机启动时默认注册了 ConsoleLoggerProvider 提供器,也就是控制台日志输出提供器,所以无需任何注册服务即可在控制台输出。

18.5.1 输出到控制台

info: Furion.EventBus.EventBusHostedService[0]      EventBus Hosted Service is running.info: Microsoft.Hosting.Lifetime[14]      Now listening on: https://localhost:5001info: Microsoft.Hosting.Lifetime[14]      Now listening on: http://localhost:5000info: Microsoft.Hosting.Lifetime[0]      Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]      Hosting environment: Developmentinfo: Microsoft.Hosting.Lifetime[0]      Content root path: C:\Workplaces\Furion\samples\Furion.Web.Entry\

如需其他配置可在 appsettings.json 配置即可:

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

如需自定义控制台日志模板可查看微软官方文档 https://docs.microsoft.com/zh-cn/dotnet/core/extensions/console-log-formatter#implement-a-custom-formatter

这里也提供相关 Issue 参考:#I5JJJH

18.5.2 输出到文件

  • 基础使用
// 例子一:启动层根目录输出services.AddFileLogging("application.log");// 例子二:支持路径services.AddFileLogging("logs/application.log");// 例子三:支持日志追加还是覆盖,设置 true 为追加,false 为覆盖services.AddFileLogging("application.log", true);
  • 从配置文件读取配置
特别注意

只有不在 .AddFile 第一个参数配置文件名才会自动加载配置,也就是文件名应该配置在配置文件中。

文件日志配置说明:

{  "Logging": {    "LogLevel": {      "Default": "Information"      // .... appsettings 默认配置    },    "File": {      "FileName": "application.log", // 日志文件完整路径或文件名,推荐 .log 作为拓展名      "Append": true, // 追加到已存在日志文件或覆盖它们      "MinimumLevel": "Information", // 最低日志记录级别      "FileSizeLimitBytes": 0, // 控制每一个日志文件最大存储大小,默认无限制,如果指定了该值,那么日志文件大小超出了该配置就会创建新的日志文件,新创建的日志文件命名规则:文件名+[递增序号].log      "MaxRollingFiles": 0 // 控制最大创建的日志文件数量,默认无限制,配合 FileSizeLimitBytes 使用,如果指定了该值,那么超出该值将从最初日志文件中从头写入覆盖    }  },  // 自定义配置节点  "MyLogger": {    "FileName": "application.log",    "Append": true,    "MinimumLevel": "Information",    "FileSizeLimitBytes": 0,    "MaxRollingFiles": 0  }}
// 例子一:默认读取 Logging:File 节点services.AddFileLogging();// 例子二:默认读取 Logging:File 节点,支持更多配置services.AddFileLogging(options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});// 例子三:自定义配置节点services.AddFileLogging(() => "MyLogger");// 例子四:自定义配置节点,支持更多配置services.AddFileLogging(() => "MyLogger", options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});
  • 自定义日志文件名规则
// 例子一:支持系统环境变量,如%SystemDrive%,%SystemRoot%services.AddFileLogging("application%SystemDrive%-%SystemRoot%.log");// 例子二:每天创建一个日志文件services.AddFileLogging("application-{0:yyyy}-{0:MM}-{0:dd}.log", options =>{    options.FileNameRule = fileName =>    {        return string.Format(fileName, DateTime.UtcNow);    };});// 例子三,任何自己喜欢的命名规则services.AddFileLogging("application-{0:yyyy}-{0:MM}-{0:dd}.log", options =>{    options.FileNameRule = fileName =>    {        // your rule...    };});
  • 日志过滤器/筛选器

通过日志筛选器可以对日志进行归类写入

// 例子一:根据日志级别输出services.AddFileLogging("infomation.log", options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogLevel == LogLevel.Information;    };});services.AddFileLogging("error.log", options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogLevel == LogLevel.Error;    };});// 例子二,根据任何规则,比如特定的类名services.AddFileLogging("someclass.log", options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogName.Contains("SomeClassName");    };});
  • 自定义日志模板

默认情况下,Furion 提供了标准的日志输出模板,如:

2022-07-23T20:16:29.3459053+08:00   [INF]   [Furion.EventBus.EventBusHostedService] [0] EventBus Hosted Service is running.2022-07-23T20:16:29.5827366+08:00   [INF]   [Microsoft.Hosting.Lifetime]    [0] Application started. Press Ctrl+C to shut down.2022-07-23T20:16:29.5828798+08:00   [INF]   [Microsoft.Hosting.Lifetime]    [0] Hosting environment: Development2022-07-23T20:16:29.5829377+08:00   [INF]   [Microsoft.Hosting.Lifetime]    [0] Content root path: C:\Workplaces\Furion\samples\Furion.Web.Entry\

如需自定义:

// 例子一,自定义日志模板(常用)services.AddFileLogging("mytemplate.log", options =>{    options.MessageFormat = (logMsg) =>    {        var stringBuilder = new StringBuilder();        stringBuilder.Append(DateTime.Now.ToString("o"));        // 其他的。。。自己组装        return stringBuilder.ToString();    };});// 例子二,需要输出 json 格式,比如对接阿里云日志,kibana第三方日志使用这个services.AddFileLogging("mytemplate.log", options =>{    options.MessageFormat = (logMsg) =>    {        // 高性能写入        return logMsg.WriteArray(writer =>        {            writer.WriteStringValue(DateTime.Now.ToString("o"));            writer.WriteStringValue(logMsg.LogLevel.ToString());            writer.WriteStringValue(logMsg.LogName);            writer.WriteNumberValue(logMsg.EventId.Id);            writer.WriteStringValue(logMsg.Message);            writer.WriteStringValue(logMsg.Exception?.ToString());        });    };});// 例子二,需要输出 json (自定义)格式,比如对接阿里云日志,kibana第三方日志使用这个services.AddFileLogging("mytemplate.log", options =>{    options.MessageFormat = (logMsg) =>    {        // 高性能写入        return logMsg.Write(writer =>        {            // write 对象为 Utf8JsonWriter,可通过流写入,性能极高        });    };});
  • 日志写入失败处理

有时候可能因为日志文件被打开或者其他应用程序占用了,那么就会导致日志写入失败,这时候可以进行其他相关处理:

// 例子一:其他处理services.AddFileLogging("template-obj.log", options =>{    options.HandleWriteError = (writeError) =>    {        // ~~    };});// 例子二,启用备用日志文件功能,也就是如果文件被占用了,可以创建新的备用日志继续写入,推荐!!!services.AddFileLogging("template-obj.log", options =>{    options.HandleWriteError = (writeError) =>    {        writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName));    };});

18.5.3 输出到数据库/其他存储介质

将日志输出到数据库中也是非常常见的需求,Furion 把该功能做到了非常简单,支持任何存储介质。

在写入数据库/其他存储介质之前需创建数据库日志写入器并实现 IDatabaseLoggingWriter 接口,支持多个,如:

using Furion.Logging;namespace Your.Core;public class DatabaseLoggingWriter : IDatabaseLoggingWriter{    // 支持构造函数注入任何实例,会自动释放任何服务,比如注入 IRepository,或者 SqlSugarClient    public DatabaseLoggingWriter()    {    }    public void Write(LogMessage logMsg, bool flush)    {        // 这里写你任何插入数据库的操作,无需 try catch    }}

你没看错,就这么简单!!

  • 基础使用
// 例子一,默认配置services.AddDatabaseLogging<DatabaseLoggingWriter>(options => {});// 例子二:自定义配置services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});
  • 从配置文件中读取
特别注意

只有不在 .AddDatabase 第一个参数为空才会自动加载配置。

数据库日志配置说明:

{  "Logging": {    "LogLevel": {      "Default": "Information"      // .... appsettings 默认配置    },    "Database": {      "MinimumLevel": "Information" // 最低日志记录级别    }  },  // 自定义配置节点  "MyLogger": {    "MinimumLevel": "Information"  }}
// 例子一:默认读取 Logging:Database 节点services.AddDatabaseLogging<DatabaseLoggingWriter>();// 例子二:默认读取 Logging:Database 节点,支持更多配置services.AddDatabaseLogging<DatabaseLoggingWriter>(default(string), options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});// 例子三:自定义配置节点services.AddDatabaseLogging<DatabaseLoggingWriter>("MyLogger");// 或services.AddDatabaseLogging<DatabaseLoggingWriter>(() => "MyLogger");// 例子四:自定义配置节点,支持更多配置services.AddDatabaseLogging<DatabaseLoggingWriter>("MyLogger", options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});// 或services.AddDatabaseLogging<DatabaseLoggingWriter>(() => "MyLogger", options =>{    options.MinimumLevel = LogLevel.Warning;    // 其他配置...});
  • 日志过滤器/筛选器

通过日志筛选器可以对日志进行归类写入

// 例子一:根据日志级别输出,可以分别定义 IDatabaseLoggingWriter,也可以用同一个底层进行判断services.AddDatabaseLogging<InfomationLoggingWriter>(options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogLevel == LogLevel.Information;    };});// 可以分别定义 IDatabaseLoggingWriter,也可以用同一个底层进行判断services.AddDatabaseLogging<ErrorLoggingWriter>(options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogLevel == LogLevel.Error;    };});// 例子二,根据任何规则,比如特定的类名services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>{    options.WriteFilter = (logMsg) =>    {        return logMsg.LogName.Contains("SomeClassName");    };});
  • 日志写入失败处理

有时候可能因为数据库连接异常或其他原因连接池满,那么就会导致日志写入失败,这时候可以进行其他相关处理:

// 例子一:其他处理services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>{    options.HandleWriteError = (writeError) =>    {        // ~~    };});

18.5.4 ILoggerFactory 方式

Furion 也提供了运行时动态创建日志提供器并写入:

public class ContactModel : PageModel{    private readonly ILogger _logger;    public ContactModel(ILoggerFactory logger)    {        // 支持所有 AddLoggingFile 和 AddDatabaseFile 配置        _logger = logger.AddFile(....).CreateLogger("MyCategory");    }    public void OnGet()    {        _logger.LogInformation("GET Pages.ContactModel called.");    }}

18.5.5 ILoggingBuilder 方式

Furion 也提供了原生 services.AddLogging(builder => {}) 方式配置,如

services.AddLogging(builder =>{    builder.AddFile("applicaion.log");    builder.AddDatabase<DatabaseLoggingWriter>();    //....});

18.5.6 记录请求日志

ASP.NET 6 中,框架默认提供了 app.UseHttpLogging() 记录 HTTP 请求日志功能,详细了解可查看官方文档 ASP.NET Core - HTTP 日志记录

当然也可以自定义中间件的方式写,只需要注入 ILogger<> 接口即可。

18.5.7 DebugTrace 默认不输出问题

默认情况下,微软在 appsettings.jsonappsettings.Development.json 中配置了 Default 日志级别,如需自定义:

{  "Logging": {    "LogLevel": {      "Default": "Information"    }  }}

这时候只需要修改 DefaultDebugTrace 即可,注意不同环境加载不同的配置文件。开发环境应修改 appsettings.Development.json 下的配置。

18.6 [LoggingMonitor] 监听日志

Furion 3.9.1 版本新增了 [LoggingMonitor] 特性,支持在控制器或操作中贴该特性,可以实现强大的请求日志监听,方便测试,如:

18.6.1 特性配置

using Furion.Logging;namespace Furion.Application;public class TestLoggerServices : IDynamicApiController{    [LoggingMonitor]    public PersonDto GetPerson(int id)    {        return new PersonDto        {            Id = id        };    }}

输出日志为:

┏━━━━━━━━━━━  Logging Monitor ━━━━━━━━━━━┣ Furion.Application.TestLoggerServices.GetPerson (Furion.Application)┣ 控制器名称:              TestLoggerServices┣ 操作名称:                GetPerson┣ 路由信息:                [area]: ; [controller]: test-logger; [action]: person┣ 请求地址:                https://localhost:44316/api/test-logger/person/11┣ 来源地址:                https://localhost:44316/api/index.html┣ 浏览器标识:              Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62┣ 客户端 IP 地址:          0.0.0.1┣ 服务端 IP 地址:          0.0.0.1┣ 服务端运行环境:          Development┣ 执行耗时:                31ms┣ ━━━━━━━━━━━━━━━  授权信息 ━━━━━━━━━━━━━━━┣ JWT Token:               Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY1ODcxNjc5NywibmJmIjoxNjU4NzE2Nzk3LCJleHAiOjE2NTg3MTc5OTcsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.VYZkwwqCwlUy3aJjuL-og62I0rkxNQ96kSjEm3VgXtg┣ UserId (integer)1┣ Account (string):        admin┣ iat (integer)1658716797┣ nbf (integer)1658716797┣ exp (integer)1658717997┣ iss (string):            dotnetchina┣ aud (string):            powerby Furion┣ ━━━━━━━━━━━━━━━  参数列表 ━━━━━━━━━━━━━━━┣ Content-Type:id (Int32)11┣ ━━━━━━━━━━━━━━━  返回信息 ━━━━━━━━━━━━━━━┣ 类型:                    Furion.Application.Persons.PersonDto┣ 返回值:                  {"Id":11,"Name":null,"Age":0,"Address":null,"PhoneNumber":null,"QQ":null,"CreatedTime":"0001-01-01T00:00:00+00:00","Childrens":null,"Posts":null}┗━━━━━━━━━━━  Logging Monitor ━━━━━━━━━━━

18.6.2 全局配置

如需全局启用 LoggingMonitor 功能,无需在每个控制器或者方法中贴,全局注册如下:

services.AddMvcFilter<LoggingMonitorAttribute>();
Furion 4.0.2 新推荐配置

Furion 4.0.2 版本中新增了非常灵活方便的 services.AddMonitorLogging() 服务配置,可在配置中随意控制哪个类哪个方法启用或不启用。

  • 注册服务
services.AddMonitorLogging();   // 默认读取 Logging:Monitor 下配置,支持传入参数自定义
  • 添加配置
{  "Logging": {    "Monitor": {      "GlobalEnabled": false, // 是否启用全局拦截,默认 `false`      "IncludeOfMethods": [], // 是否指定拦截特定方法,当 GlobalEnabled: false 有效      "ExcludeOfMethods": [], // 是否指定排除特定方法,当 GlobalEnabled: true 有效      "BahLogLevel": "Information" // 配置 Oops.Oh 和 Oops.Bah 业务日志输出级别,默认 Information    }  }}

IncludeOfMethodsExcludeOfMethods 方法签名格式为:类完全限定名.方法名,如:Furion.Application.TestNamedServices.GetNameFurion.Application.TestNamedServices 是类名,GetName 是方法名。

如果配置了全局请求监视日志,对个别不需要监视的接口方法只需要贴 [SuppressMonitor] 特性即可。

18.7 打印日志到 Swagger

Furion 框架中默认集成了 MiniProfiler 组件并与 Swagger 进行了结合,如需打印日志或调试代码,只需调用以下方法即可:

App.PrintToMiniProfiler("分类", "状态", "要打印的消息");

18.8 在后台任务中使用

由于 DbContext 默认注册为 Scoped 生存周期,所以在后台任务中使用 IServiceScopeFactory 获取所有服务,如:

public class JobService : BackgroundService{    // 日志对象    private readonly ILogger<JobService> _logger;    // 服务工厂    private readonly IServiceScopeFactory _scopeFactory;    public JobService(ILogger<JobService> logger        , IServiceScopeFactory scopeFactory)    {        _logger = logger;        _scopeFactory = scopeFactory;    }    protected override Task ExecuteAsync(CancellationToken stoppingToken)    {        _logger.LogInformation("写日志~~");        using (var scope = _scopeFactory.CreateScope())        {            var services = scope.ServiceProvider;            // 获取数据库上下文            var dbContext = Db.GetDbContext(services);            // 获取仓储            var respository = Db.GetRepository<Person>(services);            // 解析其他服务            var otherService = services.GetService<XXX>();        }        return Task.CompletedTask;    }}

18.9 多线程共享作用域

默认情况下,所有的 字符串实体 拓展都有自己独立维护的 ServiceProvider 作用域。

Web 请求中,默认是 HttpContext.RequestServices,但在 非 Web,如多线程操作,后台任务,事件总线等场景下会自动创建新的作用域,实际上这是非常不必要的内存开销。

这时,我们只需要通过 .SetXXXScoped(service) 共享当前服务提供器作用域即可,如:

Scoped.Create(async (fac, scope) => {   "写日志".SetLoggerScoped(scope.ServiceProvider).LogInformation();});

18.10 静态 Default 方式构建

StringLoggingPart.Default.SetMessage("这是一个日志").LogInformation();

18.11 规范日志模板

Furion v3.5.3+ 新增了 TP.Wrapper(...) 规范模板,使用如下:

// 生成模板字符串var template = TP.Wrapper("Furion 框架", "让 .NET 开发更简单,更通用,更流行。",    "##作者## 百小僧",    "##当前版本## v3.5.3",    "##文档地址## https://furion.icu",    "##Copyright## 百小僧, Baiqian Co.,Ltd.");Console.WriteLine(template);

日志打印模板如下:

┏━━━━━━━━━━━  Furion 框架 ━━━━━━━━━━━┣ 让 .NET 开发更简单,更通用,更流行。┣ 作者:        百小僧┣ 当前版本:     v3.5.3┣ 文档地址:     https://furion.icu┣ Copyright:   百小僧, Baiqian Co.,Ltd.┗━━━━━━━━━━━  Furion 框架 ━━━━━━━━━━━
关于属性生成

如果列表项以 ##属性名## 开头,自动生成 属性名: 作为行首且自动等宽对齐。

Furion 3.9.1 之前版本使用 [属性名] 开头。

18.12 日志上下文

版本说明

以下内容仅限 Furion 4.1.3 + 版本使用。

有时候我们希望为日志提供额外数据,这时候可通过 .ScopeContext() 配置,如:

// 写法一_logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10))       .LogInformation("我是一个日志 {id}", 20);// 写法二_logger.ScopeContext(new Dictionary<object, object> {    { "Name", "Furion" },    { "UserId", 10 }}).LogInformation("我是一个日志 {id}", 20);// 写法三_logger.ScopeContext(new LogContext {    // ....}).LogInformation("我是一个日志 {id}", 20)

LogMessage 对象中使用:

var value = logMsg.Context.Get("Key");// 比如在过滤中使用services.AddFileLogging("infomation.log", options =>{    options.WriteFilter = (logMsg) =>    {        // 还可以设置给运行时使用:logMsg.Context.Set(...);        return logMsg.Context.Get("Name") == "Furion";    };});

18.13 反馈与建议

与我们交流

给 Furion 提 Issue


了解更多

想了解更多 日志 知识可查阅 ASP.NET Core - 日志 章节 和 Serilog 文档。

演练场