程序在运行中难免会出现错误,这种错误一般非常隐蔽,不容易触发,俗称BUG。当程序的规模上去了之后,引入了各种外部类库,这种错误将出现得更加频繁,而我们程序员不可能24小时盯着程序的运行,而且出现异常之后不一定每次都能重现异常,所以这时候就需要程序自己将自己的运行活动记录下来,出现问题后供程序员分析定位问题。
所谓日志就是程序在运行时的运行记录,最常见的用途就是记录程序运行时产生的异常,此外,由于日志它记录的是运行信息,因此日志还可以当作调试工具,这在编写一些无法断点调试的程序时非常有用。
良好的日志记录能在程序出现BUG时帮助程序员快速定位问题。
在使用日志之前,我们需要对日志的几个基本概念认识一下。
日志是有等级之分的,当程序的日志等级设定好了之后,小于设定等级的日志将被忽略,只有大于等于设定等级的日志才能输出。一般来说日志的等级从小到大可以分为:Trace(轨迹)、Debug(调试)、Info(信息)、Error(错误)。不同的日志框架会对日志等级有不同的划分,划分的粒度可能会更粗也可能会更细,但是这都离不开分级的思想。
为什么要分级?因为程序在不同的运行环境会需要输出不同数量的日志信息,比如在日常调试的情况下程序需要输出更多的日志信息供程序员分析运行情况,所以会把日志等级调到最小的Trace或者Debug,而转移到实际运行环境时则只需要记录必要的信息(Info)和错误(Error)就可以了,避免大量的调试日志淹没了重要的信息和错误记录。
日志记录可是个技术活,对日志进行正确的分级很重要,千万不要一股脑往一个等级里塞日志,这样非常容易淹没重要的日志信息。
等级 | 作用 |
---|---|
Trace | 详细记录程序的运行,一般用于记录何时执行了那个方法,返回了什么等。 |
Debug | 记录程序的调试信息,可以理解为相当于断点的功能,一般用于记录执行到某行时重要变量的值 |
Info | 记录程序正常输出的有价值的信息,比如何时某个用户做了什么危险操作 |
Error | 记录程序出现错误后返回的错误信息 |
None | 不输出日志 |
当程序设置的日志等级越高,日志的数量就越少。
日志分级了之后,我们就可以对日志进行快速的检索,比如只搜索Error等级的信息,快速定位到错误。
分模块记录日志的目的和分等级记录日志差不多,都是为了能快速检索日志,比如说,数据库相关的日志记录到一起,Redis相关的日志记录到一起。一般来说日志的模块以类名命名,当然更推荐根据程序自身模块的划分来划分日志模块。
注意并不是所有程序都推荐分模块记录日志,对于一些结构和功能比较简单的程序完全是可以把所以模块的日志写在一起的,相反,如果简单的程序也分模块记录的话,可能会增加日志检索的复杂程度。
以上两个概念侧重于回答日志怎么写的问题,而日志记录的载体这个概念则回答的是日志写在哪里的问题。
日志本身也是一种信息,所以它也需要记录的载体,简单来说就是日志输出保存到哪里?一般来说日志信息的输出有3种途径。
对于程序而言,程序自己产生的日志(编写者通过日志组件产生)一般不推荐通过控制台输出,而一些框架产生的不是编写者自己写的日志则可以通过控制台直接输出。
如果是记录到文件或数据库中的日志要切记有一个清理机制,否则运行一段时间之后将产生大量日志浪费掉服务器的存储空间。
ASP.Net Core中提供了一个抽象的日志接口,我们可以通过自行实现这些接口来编写个性化的日志记录组件,也可以通过引用其他第三方日志组件的形式,使用别人实现的日志组件。本文将介绍如何在ASP.Net Core中使用第三方NLog提供的日志组件。
ASP.Net Core的默认配置(CreateDefaultBuilder)中就为我们注入了日志相关的组件,我们只需要在需要用到的地方以构造方法注入的形式将日志组件ILogger传递过去就可以了。顺带一提,默认的日志配置就是appsettings.json里面Logging节。
下面是一个非常ASP.Net Core的使用日志组件的形式。
[Route("api/")]
[ApiController]
public class ApiController : ControllerBase
{
///
/// 日志器
///
private ILogger m_Logger;
public ApiController(ILogger logger)
{
m_Logger = logger;
}
}
如果你直接这样使用,那么就很有可能出现以下的异常。
因为默认的配置里没有为我们注了ILogger< T >接口而没有注入ILogger接口,所以就会出现无法解析注入类型的异常。
其实ILogger< T >是ILogger的一个派生接口,还记得上文提到的日志的基本概念里有一个分模块记录日志,模块的名称一般以类名命名吗?这个泛型T就是日志模块,所以这里应该这样用。
[Route("api/")]
[ApiController]
public class ApiController : ControllerBase
{
///
/// 日志器
///
private ILogger m_Logger;
public ApiController(ILogger<ApiController> logger)
{
m_Logger = logger;
}
}
如果你的程序里没有划分日志模块,那么就需要在StartUp里面这样配置一下。原理很简单,就是加入一个ILogger的注入,使它固定于某个模块。
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ILogger>(sp=> {
return sp.GetService<ILogger<Program>>();
});
services.AddControllers();
}
然后在代码里这样使用。
[Route("api/")]
[ApiController]
public class ApiController : ControllerBase
{
///
/// 日志器
///
private ILogger m_Logger;
public ApiController(ILogger logger)
{
m_Logger = logger;
}
}
如果你的程序里以其他的形式划分了日志模块,或者把日志模块固定成某一个名字(下文将介绍如何配置)。那么就需要用到ILoggerFactory来获取日志器了。
ILoggerFactory,和它的名字一样,是一个日志器的工厂,专门用来获取日志器,它可以通过类型或字符串的形式获取预先配置好的日志器。
若我们预先配置好了一个名为"AppLogger"的日志器专门记录程序产生的日志。那么我们可以在程序里这样获取它。
[Route("api/")]
[ApiController]
public class ApiController : ControllerBase
{
///
/// 日志器
///
private ILogger m_Logger;
///
/// 日志器工厂
///
private ILoggerFactory m_LoggerFactory;
public ApiController(ILoggerFactory loggerFactory)
{
m_LoggerFactory = loggerFactory;
m_Logger = m_LoggerFactory.CreateLogger("AppLogger");
}
}
需要切换日志器记录其他类型的日志时,调用m_LoggerFactory对象的CreateLogger获取即可。这样也不需要在StartUp里面为ILogger写死一个模块。
我们获取到ILogger对象之后就可以愉快的记录日志了。ILogger的使用方法很简单。
///
/// 正常写日志
///
///
private void WriteLog(ILogger logger)
{
logger.LogTrace("Trace");
logger.LogDebug("Debug");
logger.LogInformation("Info");
logger.LogWarning("Warn");
logger.LogError("Error");
logger.LogCritical("Critical");
}
///
/// 异常日志
///
private void ExceptionLog()
{
try
{
throw new ArgumentNullException("Test Exception");
}
catch(Exception ex)
{
m_Logger.LogWarning(ex, "捕捉到测试异常");
}
}
上文提到的日志分级,分模块,保存方式都属于日志组件的配置,这些配置默认由appsettings.json的Logging节控制。但是,因为我们后续需要引用NLog作为日志组件的实现,会使用NLog自己的配置文件,所以会把默认的日志配置冲掉。所以这里也没必要对appsettings.json里的Logging节进行展开。
NLog是一个简单灵活的.NET日志记录类库。通过使用NLog,我们可以在任何一种.NET语言中输出带有上下文的调试诊断信息,根据喜好配置其表现样式之后发送到一个或多个输出目标中。
下面我们需要将ASP.Net Core中默认的日志组件替换成NLog的实现。
搜索并安装以下Nuget包
包名 | 备注 |
---|---|
NLog | NLog的基础包 |
NLog.Config | NLog的配置插件,使用它可以很方便地配置日志组件 |
NLog.Web.AspNetCore | 基于NLog的ASP.Net Core日志组件实现 |
修改Program.cs里面的CreateHostBuilder方法
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
var builder = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
// 使用NLog作为日志记录
builder.ConfigureLogging(logging =>
{
// 清除原有的日志器
logging.ClearProviders();
}).UseNLog();
return builder;
}
}
到此,我们程序中通过依赖注入获得的ILogger和ILoggerFactory都将替换成NLog的实现,接下来通过编写NLog.config文件来配置日志组件即可。
对于小白来说,关于ILogger可能还会有一些疑问,ASP.Net Core中的日志组件有ILogger接口(Microsoft.Extensions.Logging.ILogger),NLog基础包中也有ILogger接口(NLog.ILogger).两者都是写日志的接口,我们应该怎么选用?
这里推荐选用ASP.Net Core中的日志组件有ILogger接口(Microsoft.Extensions.Logging.ILogger).虽然它功能可能不那么强大,但是它是ASP.Net Core日志组件的接口,其他日志组件要想接入ASP.Net Core都要实现它,以后要改用其他日志组件或者需要自己实现日志组件的时候只需要在程序启动是选用其他日志组件即可,无需修改大量程序中对ILogger的引用。
引用了NLog.Config这个包之后,我们的项目中会自动多出来一个NLog.config的xml文件。通过编辑这个xml文件,我们可以简单快速地配置好日志组件。
以下是默认的配置文件,我们的关注点需要集中在targets节和rules节
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
<variable name="myvar" value="myvalue"/>
<targets>
targets>
<rules>
rules>
nlog>
日志的模块和每个模块的日志等级通过rules节来配置。
上文说过,日志模块的名称默认是以类名来命名的,所以如果我们想将命名空间 Microsoft.* (*号表示通配符)下面产生的日志单独记录到一个地方,可以添加这样的一条rule。
< logger name=“Microsoft.*” minlevel=“Info” writeTo=“console” />
其中name为命名空间的通配符,minlevel为日志等级,writeTo表示日志输出到我们定义的哪个target。
指定了特定的name之后,我们在ASP.Net Core中使用ILoggerFactory通过这个字符串获取的该日志器。
如我们指定了一个名为"AppLogger"的日志器。
< logger name=“AppLogger” minlevel=“Info” writeTo=“file” />
在程序中可以使用ILoggerFactory这样获取到它。
public class ApiController : ControllerBase
{
///
/// 日志器
///
private ILogger m_Logger;
///
/// 日志器工厂
///
private ILoggerFactory m_LoggerFactory;
public ApiController(ILoggerFactory loggerFactory)
{
m_LoggerFactory = loggerFactory;
// 获取指定名字的日志器
m_Logger = m_LoggerFactory.CreateLogger("AppLogger");
}
}
NLog中的日志等级和ASP.Net Core中的日志等级对应关系如下
NLog | ASP.Net Core |
---|---|
Trace | Trace |
Debug | Debug |
Info | Information |
Warn | Warning |
Error | Error |
Fatal | Critical |
日志的输出配置由targets节控制,关键属性是name、xsi:type和layout这三个。name表示该输出的标识,供rules节中的logger引用,xsi:type表示日志输出的类型,layout是日志输出的格式。
NLog中日志输出格式非常丰富,它提供了丰富的变量供我们组装。如果希望深入了解可以前往以下网站阅读相关文档。
NLog.Config使用文档
如果平时接触NLog比较少的话,我更推荐先认准几个好用的格式反复使用,等认识深刻一点之后再深入了解。
NLog中一般常用的有以下3种日志输出
输出到控制台配置起来非常简单,指定好类型和输出格式就可以了。
<targets>
<target name="console" xsi:type="Console"
layout="${longdate} ${level:uppercase=true} ${message} ${newline} ${exception:format=ToString:innerFormat=StackTrace}"/>
targets>
输出到文件是最常用的日志输出途径,一般放在专门的目录下以日期命名。NLog中还提供了自动归档的功能,将一段时间的日志自动归档成一个日志文件,并可以控制归档文件的数量,达到自动清理的效果。
<targets>
<target name="file" xsi:type="File" fileName="${basedir}/Log/${shortdate}.log"
layout="${longdate} ${level:uppercase=true} ${message} ${newline} ${exception:format=ToString:innerFormat=StackTrace}"
archiveFileName="${basedir}/Log/Archive.{#}.log"
archiveEvery="Day"
archiveNumbering="Date"
archiveDateFormat="yyyy-MM-dd"
maxArchiveFiles="7"
concurrentWrites="true"
keepFileOpen="false" />
targets>
NLog支持以ADO.Net的形式执行SQL语句,将日志写入到数据库中。其原理就是指定一条连接字符串和SQL语句,将NLog中预设好的变量以参数的形式组装起来执行。说起来好像很难理解,看一下它的配置马上就豁然开朗了。
<target name="database" xsi:type="Database">
<connectionString>server=localhost;Database=*****;user id=****;password=*****connectionString>
<commandText>
insert into dbo.Log (
Level, CreateTime, Message,
Callsite, Exception
) values (
@Level, @CreateTime, @Message,
@Callsite, @Exception
);
commandText>
<parameter name="@Level" layout="${level}" />
<parameter name="@CreateTime" layout="${date}" />
<parameter name="@Message" layout="${message}" />
<parameter name="@Callsite" layout="${callsite}" />
<parameter name="@Exception" layout="${exception:tostring}" />
target>
GitHub上的wiki示例
以下是本人常用的NLog配置,以文件的形式记录应用日志,以天为单位进行归档,最多保留7个归档文件。
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
<targets>
<target name="file" xsi:type="File" fileName="${basedir}/Log/${shortdate}.log"
layout="${longdate} ${level:uppercase=true} ${message} ${newline} ${exception:format=ToString:innerFormat=StackTrace}"
archiveFileName="${basedir}/Log/Archive.{#}.log"
archiveEvery="Day"
archiveNumbering="Date"
archiveDateFormat="yyyy-MM-dd"
maxArchiveFiles="7"
concurrentWrites="true"
keepFileOpen="false" />
<target name="console" xsi:type="Console"
layout="${longdate} ${level:uppercase=true} ${message} ${newline} ${exception:format=ToString:innerFormat=StackTrace}"/>
targets>
<rules>
<logger name="*" minlevel="Fatal" writeTo="console" />
<logger name="AppLogger" minlevel="Info" writeTo="file" />
<logger name="Microsoft.*" minlevel="Info" writeTo="console" />
rules>
nlog>
NLog配置说明文档
.NET Core 和 ASP.NET Core 中的日志记录