日志系统

1、什么是日志

日志(logging)是程序运行中的“黑匣子”,在程序出现问题以后,我们可以通过分析日志来查找问题。

.Net core中日志可以被记录到控制台,同时还可以使用第三方提供的程序将日志记录到文件,日志服务器等。

日志的级别:

NLog,Log4Net

1
2
3
4
5
6
Critical:最高级别,生死攸关,系统即将崩溃。
Error:错误,优先级别仅次于Critial,例如:数据库链接失败
Warning:警告,有可能会出现问题,但不一定会出现问题。
Information:用于跟踪应用程序的常规流
Debug:表示在开发和调试过程中短期有用的信息
Trace:最详细的日志,可能包含敏感信息,不建议在生产环境中使用

级别的使用情况,可以根据个人的具体情况来确定使用。

例如:系统正在链接数据库,可以使用Information,记录链接这一过程,假设这里我们最多链接3次,但是第一次没有链接成功,这里可以使用Warning,表示可能会出现问题,如果最后一次链接也没有成功,可以使用Error,表示数据库彻底链接失败.

2、日志的基本使用

在这一小结中,我们看一下怎样将日志输出到控制台中。

第一步:需要安装的包:

1
2
3
Install-Package Microsoft.Extensions.Logging // 这是日志系统的核心包,不管是将日志输出到控制台还是文件等,都需要安装

Install-Package Microsoft.Extensions.Logging.Console // 表示将日志输出到控制台中

第二步:DI注入

1
2
ServiceCollection services = new ServiceCollection();
services.AddLogging(logBuilder => logBuilder.AddConsole());

在上面的代码中,通过调用AddLogging这个方法将与日志有关的服务注入到容器中,在该方法的中,又调用了AddConsole方法,表示将日志信息输出到控制台中。

第三步:使用泛型的ILogger接口从容器中获取一个用于输出日志的对象,泛型类型一般用当前的类,这样在输出日志的时候默认会把当前类名输出,这样便于我们定义某一条输出信息来自哪个类。注意:在注入ILogger服务的时候,不能使用非泛型的ILogger接口,否则是获取不到服务的。

1
2
var logger=sp.GetRequiredService<ILogger<Program>>();
logger.LogWarning("这一条警告信息");

ILogger中有LogTrace,LogDebug,LogInformation,LogWarning,LogError,LogCritical这个6个方法用于输出不同严重性级别的消息(严重性级别依次提高)。

例如:就像我们前面提到的,需要链接数据,但是如果3次没有链接成功,则不在进行链接。

开始进行链接,这样的信息就用LogInformation,而出现了“第一次链接失败,准备进行第二次数据库的链接”,这时候可以使用LogWarning.

如果3次都链接失败,这时候就可以使用LogError记录相应的错误信息。

完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

ServiceCollection services = new ServiceCollection();
services.AddLogging(logBuilder => logBuilder.AddConsole());
using (var sp = services.BuildServiceProvider())
{
var logger=sp.GetRequiredService<ILogger<Program>>();
logger.LogWarning("这一条警告信息");
logger.LogError("这是一条错误信息");
string age = "abc";
logger.LogInformation("用户输入的年龄{0}",age);// 可以使用占位符的格式

try
{
int i=int.Parse(age);

}catch (Exception ex)
{
logger.LogError($"{ex.Message}");
}
}

通过上面的代码,可以看到日志在输出的时候可以采用占位符的格式,同时在程序遇到异常的时候,我们还可以把异常的对象信息作为参数传递给LogError方法,这样在输出信息中就可以看到异常的堆栈信息。

问题:在上面的代码中,我们是直接在Program.cs类中进行演示的,但是这里有一个新的需求,就是我们需要再TestLog.cs这个类中进行日志的记录,应该怎样做?
在项目中创建TestLog.cs这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
public class TestLog
{
private readonly ILogger<TestLog> logger;
public TestLog(ILogger<TestLog> logger)
{
this.logger = logger;
}
public void ConnectSql()
{
logger.LogInformation("开始链接数据库");
logger.LogWarning("第一次链接失败!");
logger.LogWarning("第二次链接失败!");
logger.LogError("数据库链接失败");

}
}
}

在上面的代码中,是在TestLog这个构造函数中,注入了ILogger这个泛型接口,注意:这里的泛型类型是当前的类也就是TestLog.

下面在ConnectSql这个方法中进行日志的记录。

下面返回到Program.cs类中,进行代码的修改,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
using ConsoleApp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

ServiceCollection services = new ServiceCollection();
services.AddLogging(logBuilder => logBuilder.AddConsole());
services.AddScoped<TestLog>();
using (var sp = services.BuildServiceProvider())
{
var testLog=sp.GetRequiredService<TestLog>();
testLog.ConnectSql();
}

在上面的代码中,通过AddScoped方法将TestLog注入到DI容器中,这样TestLog类中的ILogger泛型接口也完成了注入。

下面在通过GetRequiredService方法,从DI容器中获取TestLog的实例,完成对ConnectSql方法的调用。

进行代码的测试。

当然,这里我们也可以设置日志的级别。

1
2
3
4
5
6
7
ServiceCollection services = new ServiceCollection();
// 这里通过SetMinimumLevel方法设置了日志的级别,要求只能输出`Error`及以上级别的信息
services.AddLogging(logBuilder =>
{
logBuilder.AddConsole();
logBuilder.SetMinimumLevel(LogLevel.Error);
}) ;

运行以上程序,只是输出了Error错误信息,警告等信息没有在控制台中输出。

3、NLog 基本使用

在前面的课程中,我们是将日志信息输出到控制台中,将日志信息输出到控制台中的问题是:

控制台中展示的信息的长度是有一定的限制的,当信息展示的比较多以后,以前的日志信息就无法展示了,同时控制台中的日志不是持久化保存的,当控制台不小心关掉了以后,日志信息就没有了。

而且,相对于控制台的展示,开发人员和运维人员更喜欢文本格式的日志文件,而且分发起来比较方便。

.Net Core中并没有内置的文本文件日志提供程序,我们需要使用第三方的日志提供程序。

常用的第三方日志提供程序有Log4Net,NLog,Serilog.

这里我们讲解的是NLog,它的使用比较简单,功能也比较强大。

将日志信息写到文件中要注意的问题:

第一:一般情况下日志文件名称会按照日期进行区分。

方便定位问题。

第二:限制日志文件的总个数

日志文件特别多,导致磁盘被占满,处理方式,可以限制日志文件的总个数,例如:限制200个日志文件,当到了200个以后,在有新的日志文件,可以将旧的删除掉,因为当问题解决了以后,以前的日志文件没有什么用了。

第三:限制单个日志文件的大小,日志文件不要太大,查找不方便。

NLog使用的基本步骤:

第一:安装需要的包:

1
2
Install-Package Microsoft.Extensions.Logging // 这是日志系统的核心包,不管是将日志输出到控制台还是文件等,都需要安装
Install-Package NLog.Extensions.Logging

第二:需要在项目跟目录下面创建nlog.config配置文件(同时这里也要选择“如果较新则复制”)。注意文件名的大小写。当然,这里也可以修改成其他的名称,但是需要单独的配置,比较麻烦。约定大于配置。

第三部:增加logBuilder.AddNLog( )

当在项目的跟目录下面创建好了nlog.config这个配置文件以后,可以将官网的配置拷贝到该文件中。

官网地址:https://github.com/NLog/NLog/wiki/Getting-started-with-.NET-Core-2---Console-application

这个配置针对的是控制台的程序,如果针对的是网站项目,配置不太一样,具体参考官网。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogFile="console-example-internal.log" <!--这里删除了盘符-->
internalLogLevel="Info" >

<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File" name="logfile" fileName="console-example.log"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
<target xsi:type="Console" name="logconsole"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
</targets>

<!-- rules to map from logger name to target -->
<rules>
<logger name="*" minlevel="Trace" writeTo="logfile,logconsole" />
</rules>
</nlog>

以上就是默认从官网中拷贝的配置。注意:这里把配置文件中默认的磁盘盘符删除了,由于选择了较新则复制,程序启动以后,对应的日志文件会被拷贝到程序的运行目录中bin\Debug\net7.0

下面修改Program.cs文件中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using ConsoleApp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;

ServiceCollection services = new ServiceCollection();
services.AddLogging(logBuilder =>
{
// logBuilder.AddConsole();
logBuilder.AddNLog(); //只是这里增加了该行代码。
//logBuilder.SetMinimumLevel(LogLevel.Error);
}) ;
services.AddScoped<TestLog>();
using (var sp = services.BuildServiceProvider())
{
var testLog=sp.GetRequiredService<TestLog>();
testLog.ConnectSql();
}

AddLogging方法中增加了logBuilder.AddNLog();.

同时为了将错误的堆栈信息记录到日志文件中,我们将TestLog.cs类中的代码也做了简单的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void ConnectSql()
{
logger.LogInformation("开始链接数据库");
logger.LogWarning("第一次链接失败!");
logger.LogWarning("第二次链接失败!");
logger.LogError("数据库链接失败");
string age = "abc";
logger.LogInformation("用户输入的年龄{0}", age);

try
{
int i = int.Parse(age);

}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}

}

运行程序,在控制台中打印的错误信息不一样了,同时在bin\Debug\net7.0目录中有了记录错误信息的日志文件console-example.log

以上就是NLog的基本使用。

下面对配置文件做一个简单的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogFile="console-example-internal.log" <!--这里删除了盘符-->
internalLogLevel="Info" >

<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File" name="logfile" fileName="console-example.log"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
<target xsi:type="Console" name="logconsole"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
</targets>

<!-- rules to map from logger name to target -->
<rules>
<logger name="*" minlevel="Trace" writeTo="logfile,logconsole" />
</rules>
</nlog>

autoReload:”修改日志配置文件后是否允许自动加载,而无需重启启动程序,这里设置为true,表示日志修改了,无需重新启动程序”。

internalLogLevel:”把NLog这个日志程序内部的调试和异常信息写到当前属性指定的文件,也就是console-example-internal.log

internalLogLevel:指定的是NLog这个程序内部的日志级别。当然,这里我们可以将其关闭,把该属性的值设置为off.

如果这里设置了off以后,我们可以把console-example-internal.log这个日志文件删除掉,然后再重新运行程序,发现就不会再创建该文件了。也就是,不需要再记录NLog这个程序的内部日志信息了。

targets:表示日志文件输出的位置。

其中的每个target,表示对每个输出格式进行限制。

xsi:type=File表示输出到文件,fileName表示输出的日志文件名。layout表示日志信息在文件中的格式。

格式longdate:表示日期,level:表示日志的级别,message:表示日志的信息,重点关注这几项配置,其他的可以查询文档。

rules指定日志的输出规则,这里的writeTo表示日志具体输出的形式,这里的writeTo的取值是logfile,logconsole,表示日志可以输出到文件,也可以输出到控制台(也就是上面targetname属性的值)。minlevel表示了日志的最低级别。这里的name取值为*号,表示所有的日志信息都输出到logfile,logconsole.

以上就是Nlog的基本使用。

4、NLog深入

这里先来简单说两个概念,一个是日志的分类,另外一个是日志的过滤。

这两个概念,在其他的日志程序中也是存在的。

日志分类:在上一小结中,我们是将所有级别的日志信息都写到一个文件中了,例如·:“Debug”,Warning,Error等。其实我们可以对不同的级别进行分类,例如:Debug的都写到一个文件中,Error的写到一个文件中,这样查找起来也方便。

或者,也可以根据我们项目的模块来进行分类,例如,针对登录注册模块,有单独的日志文件,针对商品管理模块有单独的日志文件,这样结构非常清晰,查找方便。

日志过滤:项目不同阶段(比如,项目刚上线和项目稳定以后)需要记录的日志内容是不同的,例如:项目刚上线的时候,问题可能比较多,这时候,需要将日志的Debug,Warning等都记录下来,但是当项目运行了一段时间后,比较稳定了,这时候,Debug的日志信息我们就不需要了,只想保存Warning和Error,这样也减少了日志文件的大小。

下面,我们看一下具体的实现:

首先在项目中在创建一个类FileLog.cs,该类的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using ConsoleApp;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SystemFile //注意:这里将命名空间修改成了SystemFile
{
public class FileLog
{
private readonly ILogger<FileLog> logger;
public FileLog(ILogger<FileLog> logger)
{
this.logger = logger;
}
public void ReadFileInfo()
{

logger.LogInformation("开始读取文件");
logger.LogWarning("第一次打开文件失败!");
logger.LogWarning("第二次打开文件失败!");
logger.LogError("文件读取失败");
string age = "abc";
logger.LogInformation("用户输入的年龄{0}", age);

try
{
int i = int.Parse(age);

}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}

}
}
}

注意:在上面的代码中,我们将命名空间修改成了SystemFile,目的就是用来表示一个新的模块。

方法名称修改成了ReadFileInfo,其他的内容与前面的案例是一样的。

下面再来看一下Program.cs文件中的代码修改,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using ConsoleApp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using SystemFile;

ServiceCollection services = new ServiceCollection();
services.AddLogging(logBuilder =>
{
/* logBuilder.AddConsole();*/ //注意: 这里将.net 本身的方法注释掉了,目的是为了能够在控制台中看到更清楚的输出
logBuilder.AddNLog();
logBuilder.SetMinimumLevel(LogLevel.Error);
}) ;
services.AddScoped<TestLog>();
services.AddScoped<FileLog>();// 这里将FileLog注入到了DI容器中。
using (var sp = services.BuildServiceProvider())
{
var testLog=sp.GetRequiredService<TestLog>();
var fileLog=sp.GetRequiredService<FileLog>(); // 通过GetRequiredService方法获取容器中FileLog类的实例
testLog.ConnectSql();
fileLog.ReadFileInfo(); // 调用ReadFileInfo方法。
}

在上面的代码中,我们将FileLog注入到了DI容器中,并且通过GetRequiredService方法从容器中获取了FileLog类的实例,完成了ReadFileInfo方法的调用。

下面看一下nlog.config配置文件中的配置信息的修改,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogFile="console-example-internal.log"
internalLogLevel="off" >

<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<!--这里修改了fileName属性的值,日志文件都放在了logs目录下面,同时文件名称以log-为前缀,假设日期作为日志文件的名称-->
<target xsi:type="File" name="logfile" fileName="logs/log-${shortdate}.log"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
<!--这里添加了新的一项配置,名称是sysFileLog,同时也让日志放在了logs目录下面,只不过这里以sysFile为前缀-->
<target xsi:type="File" name="sysFileLog" fileName="logs/sysFile-${shortdate}.log" archiveAboveSize="1000"
maxArchiveFiles="3"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
<target xsi:type="Console" name="logconsole"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
</targets>

<!-- rules to map from logger name to target -->
<!--这里修改了rules规则-->
<!--SystemFile:表示命名空间-->
<rules>
<logger name="*" minlevel="Warn" maxlevel="Fatal" writeTo="logconsole" />
<logger name="SystemFile.*" minlevel="Trace" writeTo="sysFileLog" final="true"/>
<logger name="*" minlevel="Trace" writeTo="logfile"/>
</rules>
</nlog>

在上面的配置中,我们先修改了第一个target,这里我们让以前TestLog类中的信息记录到了logs目录下面以log-日期为命名的日志文件中。

下面我们又添加了一个新的target项,在该项中,指定的namesysFileLog,目的是将SystemFile命名空间下的FileLog类中的

信息记录到logs目录以sysFile-日期命名的日志文件中。

当然,要想满足以上的要求,需要修改rules中的配置。

第一项logger中指定的name取值是*,表示任何类或者是模块中的日志信息都会执行logconsole这个target,这样都会将日志信息打印在控制台中。并且指定的日志级别最小是Warn,最高的日志级别是Fatal,这些级别是ILog自已定义的,对应着.net 中的`Warning,Critical.一会我们可以在控制台中重点看一下输出的日志的级别

第二项logger,指定的nameSystemFile.*,表示SystemFile这个命名空间下的类出错的信息都会执行sysFileLog这个target.

同时这里指定了final取值为true,表示如果匹配上了这个logger,就不会再执行后面的logger项。

讲解到这,我们就明白了,针对SystemFile这个命名空间下类出错的这些日志信息,不仅会展示在控制台中,也会写到以sysFile-为前缀的文件中。

第三项logger,指定的name也是*,表示的是任何类或者模块中的日志信息都会执行logfile这个target.但是问题是在第二项中,我们指定的loggername属性的取值是SystemFile.*,并且对应的final属性的值是true,也就是说,SystemFile这个命名空间下的FileLog类中如果出错了,对应的日志信息,不会执行logfile这个target,执行的是sysFileLog这个target.总结一句话就是:当把final设置为true以后,如果匹配到了这个logger,就不会继续向下匹配其他的logger了。

讲解到这,我们也明白了,针对当前我们所创建的项目,只有TestLog.cs这个类中出错的日志信息才会执行logfile这个target.

这样在logs目录下面会以log-日期形式的文件来保存TestLog.cs这个类中出错的日志信息。

下面运行程序,先看一下控制台中的输出(可以看到在控制台中Info的日志信息没有输出,这里已经被ILog给过滤掉了),然后看一下bin\Debug\net7.0目录下的logs`目录。

下面再来看两个配置:archiveAboveSizemaxArchiveFiles``

archiveAboveSize:表示单个日志文件超过多少字节把日志存档,也就是规定单个日志文件是多大,单位是字节,通过该属性可以避免单个文件太大的情况。

maxArchiveFiles:规定日志文件的个数,如果不设定该属性,则日志文件的数量会一直增加,如果设置了该属性,则最多保存该属性指定数量的日志文件个数,旧的会被删除掉。

下面进行测试:

1
2
3
<target xsi:type="File" name="sysFileLog" fileName="logs/sysFile-${shortdate}.log" archiveAboveSize="10000" 
maxArchiveFiles="3"
layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />

可以看到,在第二个target中,我们指定的archiveAboveSize 属性的取值是10000字节,同时maxArchiveFiles属性的值是3,也就是保存三个日志文件。

返回到Program.cs文件中修改测试代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
using (var sp = services.BuildServiceProvider())
{
var testLog=sp.GetRequiredService<TestLog>();
var fileLog=sp.GetRequiredService<FileLog>();
for(int i = 0; i < 100; i++)
{
testLog.ConnectSql();
fileLog.ReadFileInfo();
}

}

这里,我们是写一个循环,然后不断的调用相应的测试方法来向文件中写日志。

然后查看bin\Debug\net7.0\logs目录中的日志文件的变化情况。

当然,这两个属性的取值根据实际情况自己来确定。

总结:NLog部分功能与.net中自带的日志管理有一定的重复,例如:分类,分级等,所以为了避免发生冲突,如果使用了NLog,建议不要在配置.net中的分级等。

所以这里,我们需要将Program.cs文件中的设置分级的代码注释掉,如下所示:

1
2
3
4
5
6
services.AddLogging(logBuilder =>
{
/* logBuilder.AddConsole();*/
logBuilder.AddNLog();
/*logBuilder.SetMinimumLevel(LogLevel.Error);*/ // 这里去掉了.net中日志的分级
}) ;

NLog的官方网站

http://nlog-project.org/config/

通过官网,可以看到NLog不仅可以将日志 输出的文件,数据库,还可以发送邮件等等。

大家可以根据自己的情况,查看相应的文档来进行学习。